UNIX环境高级编程第二版_扫描 版笔记
生活随笔
收集整理的這篇文章主要介紹了
UNIX环境高级编程第二版_扫描 版笔记
小編覺得挺不錯的,現在分享給大家,幫大家做個參考.
pread/pwrite
相當于 lseek,read/write ?,只是pread/pwrite具有原子性,定位和讀寫操作不可中斷
dup(fd); = fcntl(fd,F_DUPFD,0);
dup2(fd1,fd2); = close(fd2); fcntl(fd1,F_DUPFD,fd2);
減少磁盤讀寫次數: 延遲寫
sync 交所有修改過的塊緩沖區排入寫隊列
fsync 只對fd指定文件時行sync
fdatasync 類似fsync,但只是對數據部分,而fsync還會對文件屬性進行更新
/dev/fd
打開文件/dev/fd/n等效于復制描述符
第四章:文件和目錄
stat(const char *restrict pathname,struct stat *restrict buf);//返回與文件有關的信息結構。
fstat(int fd,struct stat *buf);
lstat(const char *restrict pathname,struct stat *restrict buf);//類似于stat,但是當命名的文件是一個符號鏈接時,
lstat返回該符號鏈接的有關信息,而不是由該符號鏈接引用文件的信息。
第五章:標準I/O庫
對于ascii字符集,一個字符用一個字節表示,對于國際字符集,一個字符可用多個字節表示,標準io文件流可用于單字節或多字節("寬“)字符集。
流的定向(stream‘s orientation)決定了所讀,寫的字符是單字節還是多字節,當一個流最初被創建時,它并沒有定向。如若在未定向的流上使用一個多字節io函數,
則將流的定向設置為寬定向的,如若在未定向的流上使用一個單字節io函數,則將流的定向設置為字節定向的,
freopen函數清除一個流的定向,fwide函數設置流的定向
int fwide(FILE *stream, int mode);
When mode is nonzero, ?the ?fwide() ?function ?first ?attempts ?to ?set
? ? ? ?stream's ?orientation ?(to ?wide-character ?oriented if mode is greater
? ? ? ?than 0, or to byte oriented if mode is less than 0). ?It then returns a
? ? ? ?value denoting the current orientation, as above.
標準I/O庫提供緩沖的目的是盡可能減少使用read和write調用的次數。
標準I/O庫提供了三種類型的緩沖
1、全緩沖 相關標準I/O函數通常調用malloc獲得需使用的緩沖區。
? ? flush(沖洗)函數fflush 一、在標準io庫方面,flush(沖洗)意味著緩沖區中的內容寫到磁盤上。
? ? 二、終端驅動程序方面,flush(刷清)表示丟棄已存儲在緩沖區中的數據。
2、行緩沖。輸入和輸出遇到換行符時,標準io庫執行io操作。當流涉及一個終端時(例如標準輸入和標準輸出),通常使用行緩沖。
3、不帶緩沖。標準io庫不對字符進行緩沖存儲。例如fputs函數就是,標準出錯流stderr通常是不帶緩沖的,這就使得出錯信息可以忙顯示出來,而不管它們是否含有一個換行符。
void setbuf(FILE *fp,char *buf)
int setvbuf(FILE *fp,char *buf,int mode)
fp已打開的文件指針,buf為必須指向一個長度為BUFSIZ的緩沖區,為了關閉緩沖將buf設置為NULL
使用setvbuf,我們可以精確地指定所需要的緩沖類型,這里用mode參數實現的:
__IOFBF 全緩沖
__IOLBF 行緩沖
__IONBF 不帶緩沖
如果指定一個不帶緩沖的流,則忽略buf和size參數,如果該流是帶緩沖的,而buf是NULL,則標準IO庫將自動地為該流分配適當長度的緩沖區。
int fflush(FILE *fp);
作為一個特例,如若fp是NULL,則此函數將導致所有輸出流被沖洗。
fopen freopen fdopen
int getc(FILE *fp); 可被實現為宏
int fgetc(FILE *fp); 不能實現宏,是一個函數,則可以把其地址作為參數傳遞。
int getchar(void); = getc(stdin)
不管是出錯還是到達文件尾端,上面三個函數都返回同樣的值,為了區分這兩種不同的情況,必須調用ferror或feof。
在大多數實現中,為每個流在FILE對象中維持了兩個標志:
1、出錯標志 2、文件結束標志。
void clearerr(FILE *fp);清除這兩個標志。
int ungetc(int c,FILE *fp); 壓送回字符到流中
回頭的字符不必一定是上一次讀到的字符。不能回送EOF,但是當已經到達文件尾端時,仍可以回頭一字符。下次讀將返回該字符,再次讀則返回EOF。
之所以能這樣做的原因是一次成功的ungetc調用會清除該流的文件結束標志。
char *fgets(char *buf,int n,FILE *fp);
char *gets(char *buf);
fgets,必須指定緩沖區的長度n,此函數一直讀到下一個換行符為止,但是不超過n-1個字符,讀入的字符被卷入緩沖區。該緩沖區心null字符結尾,如若該行(包括最后一個換行符)的字符超過n-1,則fgets只返回一個不完整的行,但是,緩沖區總是以null字符結尾。對fgets的下一次調用會繼續讀該行。
gets是一個不推薦使用的函數,因不能指定緩沖區的長度,可能造成緩沖區溢出。
gets與fgets的另一個區別是,gets并不將換行符存入緩沖區中。
int fputs(char *str,FILE *fp);
int puts(char *str);
fputs將一個以null符終止的字符串寫到指定的流中,尾端的終止符null不寫出。
puts將一個以null符終止的字符串寫到標準輸出,終止符不寫出,但是,puts然后又將一個換行符寫到標準輸出。
fread fwrite
ftell fseek
ftello fseeko 用off_t代替long修飾偏移量
輸出轉換說明
%[flags][fldwidth][precision][lenmodifier]convtype
flags: 1.-左對齊 2.+顯示帶符號 3.(空格)如果第一個不是符號,則在其前面加上一個空格 4.#進制 5.0添加前導
fldwidth:最小字段寬度
precision:最少輸出數字位數(整數)、小數位數(浮點數)、最大字符數(字符串)。精度是一個句點(.),后接一個可選的非負十進制整數或一個星號(*)。
寬度和精度都可以為*.
lenmodifier:說明參數長度
? hh:char
? h:short
? l:long
? ll:long long
? j:intmax_t
? z:size_t
? t:ptrdiff_t
? L:long double
convtype:
? d,i
? o
? u
? x 十六進制
? f,F double精度浮點數
? e,E 指數格式的double
? g,G 解釋為f,F,e或E,取決被轉換的值
? A,a 十六進制指數格式的double
? c
? s
? p
? n 將到目前為止,所寫的字符數寫入到指針所指向的無符號整型中。
? %
臨時文件:
char *tmpnam(char *ptr);
FILE *tmpfile(void);
使用fgets和fputs通常需要復制兩次數據:一次是在內核和標準io緩沖之間(當調用read和write時),第二次是在標準io緩沖區和用戶程序中的行緩沖區之間。
快速io庫其方法是使讀一行的函數返回指向該行的指針,而不是將該行復制到另一個緩沖區中。
第六章:系統數據文件和信息
歷史原因,口令文件存儲在/etc/passwd中,而且是一個ASCII文件,每一行包含系統支持的各字段,字段之間用冒號分隔。eg:
root:x:0:0:root:/root:/bin/bash
口令用(x)占位符表示,實際存儲在另一個位置。
某些系統提供了vipw命令,允許管理員使用該命令編輯口令文件。vipw命令串行化對口令文件所做的更改,并且確保所做的更改與其他相關文件一致。
? ? ? ?struct passwd *getpwnam(const char *name);
? ? ? ?struct passwd *getpwuid(uid_t uid);
getpwuid函數由ls程序使用,它將i節點中的數值用戶id映射為用戶登錄名,在鍵入登錄名時,getpwnam函數由login程序使用。
查看整個口令文件,有以下三個函數:
struct passwd *getpwent(void);
void setpwent(void);
void endpwent(void);
加密口令是經意向加密算法處理過的用戶口令副本,因為單向,所以不能從加密口令推測到原來的文本口令。
? ? 猜測文本口令單向加密算法轉換成加密形式,再和加密口令比較,為防止這,把加密口令放到稱為陰影口令(shadow password)的文件中。
? ??
?int gettimeofday(struct timeval *tv, struct timezone *tz); //微秒級的
time_t time(time_t *t); //秒級
第七章 進程環境
進程終止:
?正常終止:1、從main返回;2、調用exit;3、調用_exit或_Exit;4、最后一個線程從其啟動例程返回;5、最后一個線程對取消請求做出響應
?異常終止:6、調用abort;7、接到一個信號并終止;8、最后一個線程對取消請求做出響應。
?_exit/_Exit立即進入內核,exit則先執行一些清理處理(包括調用執行各終止處理程序,關閉所有標準io流等)再進入內核。
a、 三個exit函數被調用時不帶終止狀態;b、main執行沒有返回值的return語句;c、main沒有聲明返回類型為整型,則該進程的終止狀態是未定義的。
但是,若main的返回類型是整型,并且main執行到最后一條語句時返回(隱式返回),那么該進程的終止狀態是0。
在main函數中exit(0) 等價于 return(0)
?
atexit函數
按照iso c的規定,一個進程可以登記多達32個函數,這些函數將由exit自動調用。我們稱這些函數為終止處理程序(exit handler),并調用atexit函數來登記這些函數。
int atexit(void (*function)(void));
exit調用終止處理程序順序和登記順序相反。同一函數登記多次,則也會被調用多次。
程序調用exec函數族中的任一函數,則將清除所有已安裝的終止處理程序。
注意:內核使程序執行的唯一方法是調用一個exec函數。
每個程序都會接收到一張環境表,與參數表一樣,環境表也是一個字符指針數組。全局變量environ則包含了該指針數組的地址:
extern char **environ;
malloc 初始值不確定
calloc 初始為0
realloc
在C中,goto語句是不能跨越函數的,而執行跨越函數類跳轉功能的是函數setjmp和longjmp。這兩個函數對于處理發生在深層嵌套調用中的出錯情況是非常有用的。
#include <setjmp.h>
int setjmp(jmp_buf env);
void longjmp(jmp_buf env,int val);
第八章 進程控制
pid為0的進程通常是調度進程,常常被稱為交換進程,是內核的一部分。
pid為1的進程init,
pid為2的進程是頁守護進程,負責支持虛擬存儲系統的分頁操作。
strlen:計算不包含終止null字節的字符串長度,是一個函數調用
sizeof:計算包括終止null字節的緩沖區長度,空間已知,所以sizeof在編譯時計算緩沖區長度。
fork:父子進程共享一個文件表項。父子進程各自關閉不使用的文件描述符,不相互干擾,這是網絡服務進程中經常使用的。
出于安全性方面的考慮,有些人要求在搜索路徑PATH中決還要包括當前目錄。
exec l:list v:vector
函數execl,execlp,execle要求將新程序的每個命令行參數都說明為一個單獨的參數,這種參數表以空指針結尾。
函數execv,execvp,execve則應先構造一個指向各參數的指針數組,然后將該數組地址作為這三個函數的參數。
execlp,execvp以文件名作參數,其余4個以路徑名作參數。
execle,execve可以傳遞一個指向環境表字符串指針數組的指針,其余四個函數則使用調用進程中的environ變量為新程序復制現有的環境。
每個系統對參數表和環境表的總長度都有個限制。這種限制是由ARG_MAX給出的。
我們可以將argv[0]設置為任何字符串,當login命令執行shell時就是這樣做的。
全局變量char **environ包括PATH等很多環境變量。
解釋器文件(interpreter file),其起始行的形式是: #! pathname[optional-argument]
pathname通常是絕對路徑名。
int system(const char *cmdstring); ISO C定義了system函數,但是其操作對系統的信賴性很強,在unix中,system總是可用的。
system在實現中調用了fork,exec和waitpid。
進程會計(process accounting):通過acct函數啟用禁用功能,每當進程結束時內核就寫一個會計記錄。
int setpgid(pid_t pid, pid_t pgid);
setpgid() ?sets ?the ?PGID of the process specified by pid to pgid. ?If
? ? ? ?pid is zero, then the process ID of the calling process ?is ?used. ? If
? ? ? ?pgid is zero, then the PGID of the process specified by pid is made the
? ? ? ?same as its process ID.
setpgid函數將pid進程的進程組id設置為pgid。如果這兩個參數相等,則由pid指定的進程變成進程組組長。如果pid是0,則使用調用者的進程id。
另外,如果pgid為0,則由pid指定的進程id將用作進程組id。
? ? ? ?
dup(fd); = fcntl(fd,F_DUPFD,0);
dup2(fd1,fd2); = close(fd2); fcntl(fd1,F_DUPFD,fd2);
減少磁盤讀寫次數: 延遲寫
sync 交所有修改過的塊緩沖區排入寫隊列
fsync 只對fd指定文件時行sync
fdatasync 類似fsync,但只是對數據部分,而fsync還會對文件屬性進行更新
/dev/fd
打開文件/dev/fd/n等效于復制描述符
第四章:文件和目錄
stat(const char *restrict pathname,struct stat *restrict buf);//返回與文件有關的信息結構。
fstat(int fd,struct stat *buf);
lstat(const char *restrict pathname,struct stat *restrict buf);//類似于stat,但是當命名的文件是一個符號鏈接時,
lstat返回該符號鏈接的有關信息,而不是由該符號鏈接引用文件的信息。
第五章:標準I/O庫
對于ascii字符集,一個字符用一個字節表示,對于國際字符集,一個字符可用多個字節表示,標準io文件流可用于單字節或多字節("寬“)字符集。
流的定向(stream‘s orientation)決定了所讀,寫的字符是單字節還是多字節,當一個流最初被創建時,它并沒有定向。如若在未定向的流上使用一個多字節io函數,
則將流的定向設置為寬定向的,如若在未定向的流上使用一個單字節io函數,則將流的定向設置為字節定向的,
freopen函數清除一個流的定向,fwide函數設置流的定向
int fwide(FILE *stream, int mode);
When mode is nonzero, ?the ?fwide() ?function ?first ?attempts ?to ?set
? ? ? ?stream's ?orientation ?(to ?wide-character ?oriented if mode is greater
? ? ? ?than 0, or to byte oriented if mode is less than 0). ?It then returns a
? ? ? ?value denoting the current orientation, as above.
標準I/O庫提供緩沖的目的是盡可能減少使用read和write調用的次數。
標準I/O庫提供了三種類型的緩沖
1、全緩沖 相關標準I/O函數通常調用malloc獲得需使用的緩沖區。
? ? flush(沖洗)函數fflush 一、在標準io庫方面,flush(沖洗)意味著緩沖區中的內容寫到磁盤上。
? ? 二、終端驅動程序方面,flush(刷清)表示丟棄已存儲在緩沖區中的數據。
2、行緩沖。輸入和輸出遇到換行符時,標準io庫執行io操作。當流涉及一個終端時(例如標準輸入和標準輸出),通常使用行緩沖。
3、不帶緩沖。標準io庫不對字符進行緩沖存儲。例如fputs函數就是,標準出錯流stderr通常是不帶緩沖的,這就使得出錯信息可以忙顯示出來,而不管它們是否含有一個換行符。
void setbuf(FILE *fp,char *buf)
int setvbuf(FILE *fp,char *buf,int mode)
fp已打開的文件指針,buf為必須指向一個長度為BUFSIZ的緩沖區,為了關閉緩沖將buf設置為NULL
使用setvbuf,我們可以精確地指定所需要的緩沖類型,這里用mode參數實現的:
__IOFBF 全緩沖
__IOLBF 行緩沖
__IONBF 不帶緩沖
如果指定一個不帶緩沖的流,則忽略buf和size參數,如果該流是帶緩沖的,而buf是NULL,則標準IO庫將自動地為該流分配適當長度的緩沖區。
int fflush(FILE *fp);
作為一個特例,如若fp是NULL,則此函數將導致所有輸出流被沖洗。
fopen freopen fdopen
int getc(FILE *fp); 可被實現為宏
int fgetc(FILE *fp); 不能實現宏,是一個函數,則可以把其地址作為參數傳遞。
int getchar(void); = getc(stdin)
不管是出錯還是到達文件尾端,上面三個函數都返回同樣的值,為了區分這兩種不同的情況,必須調用ferror或feof。
在大多數實現中,為每個流在FILE對象中維持了兩個標志:
1、出錯標志 2、文件結束標志。
void clearerr(FILE *fp);清除這兩個標志。
int ungetc(int c,FILE *fp); 壓送回字符到流中
回頭的字符不必一定是上一次讀到的字符。不能回送EOF,但是當已經到達文件尾端時,仍可以回頭一字符。下次讀將返回該字符,再次讀則返回EOF。
之所以能這樣做的原因是一次成功的ungetc調用會清除該流的文件結束標志。
char *fgets(char *buf,int n,FILE *fp);
char *gets(char *buf);
fgets,必須指定緩沖區的長度n,此函數一直讀到下一個換行符為止,但是不超過n-1個字符,讀入的字符被卷入緩沖區。該緩沖區心null字符結尾,如若該行(包括最后一個換行符)的字符超過n-1,則fgets只返回一個不完整的行,但是,緩沖區總是以null字符結尾。對fgets的下一次調用會繼續讀該行。
gets是一個不推薦使用的函數,因不能指定緩沖區的長度,可能造成緩沖區溢出。
gets與fgets的另一個區別是,gets并不將換行符存入緩沖區中。
int fputs(char *str,FILE *fp);
int puts(char *str);
fputs將一個以null符終止的字符串寫到指定的流中,尾端的終止符null不寫出。
puts將一個以null符終止的字符串寫到標準輸出,終止符不寫出,但是,puts然后又將一個換行符寫到標準輸出。
fread fwrite
ftell fseek
ftello fseeko 用off_t代替long修飾偏移量
輸出轉換說明
%[flags][fldwidth][precision][lenmodifier]convtype
flags: 1.-左對齊 2.+顯示帶符號 3.(空格)如果第一個不是符號,則在其前面加上一個空格 4.#進制 5.0添加前導
fldwidth:最小字段寬度
precision:最少輸出數字位數(整數)、小數位數(浮點數)、最大字符數(字符串)。精度是一個句點(.),后接一個可選的非負十進制整數或一個星號(*)。
寬度和精度都可以為*.
lenmodifier:說明參數長度
? hh:char
? h:short
? l:long
? ll:long long
? j:intmax_t
? z:size_t
? t:ptrdiff_t
? L:long double
convtype:
? d,i
? o
? u
? x 十六進制
? f,F double精度浮點數
? e,E 指數格式的double
? g,G 解釋為f,F,e或E,取決被轉換的值
? A,a 十六進制指數格式的double
? c
? s
? p
? n 將到目前為止,所寫的字符數寫入到指針所指向的無符號整型中。
? %
臨時文件:
char *tmpnam(char *ptr);
FILE *tmpfile(void);
使用fgets和fputs通常需要復制兩次數據:一次是在內核和標準io緩沖之間(當調用read和write時),第二次是在標準io緩沖區和用戶程序中的行緩沖區之間。
快速io庫其方法是使讀一行的函數返回指向該行的指針,而不是將該行復制到另一個緩沖區中。
第六章:系統數據文件和信息
歷史原因,口令文件存儲在/etc/passwd中,而且是一個ASCII文件,每一行包含系統支持的各字段,字段之間用冒號分隔。eg:
root:x:0:0:root:/root:/bin/bash
口令用(x)占位符表示,實際存儲在另一個位置。
某些系統提供了vipw命令,允許管理員使用該命令編輯口令文件。vipw命令串行化對口令文件所做的更改,并且確保所做的更改與其他相關文件一致。
? ? ? ?struct passwd *getpwnam(const char *name);
? ? ? ?struct passwd *getpwuid(uid_t uid);
getpwuid函數由ls程序使用,它將i節點中的數值用戶id映射為用戶登錄名,在鍵入登錄名時,getpwnam函數由login程序使用。
查看整個口令文件,有以下三個函數:
struct passwd *getpwent(void);
void setpwent(void);
void endpwent(void);
加密口令是經意向加密算法處理過的用戶口令副本,因為單向,所以不能從加密口令推測到原來的文本口令。
? ? 猜測文本口令單向加密算法轉換成加密形式,再和加密口令比較,為防止這,把加密口令放到稱為陰影口令(shadow password)的文件中。
? ??
?int gettimeofday(struct timeval *tv, struct timezone *tz); //微秒級的
time_t time(time_t *t); //秒級
第七章 進程環境
進程終止:
?正常終止:1、從main返回;2、調用exit;3、調用_exit或_Exit;4、最后一個線程從其啟動例程返回;5、最后一個線程對取消請求做出響應
?異常終止:6、調用abort;7、接到一個信號并終止;8、最后一個線程對取消請求做出響應。
?_exit/_Exit立即進入內核,exit則先執行一些清理處理(包括調用執行各終止處理程序,關閉所有標準io流等)再進入內核。
a、 三個exit函數被調用時不帶終止狀態;b、main執行沒有返回值的return語句;c、main沒有聲明返回類型為整型,則該進程的終止狀態是未定義的。
但是,若main的返回類型是整型,并且main執行到最后一條語句時返回(隱式返回),那么該進程的終止狀態是0。
在main函數中exit(0) 等價于 return(0)
?
atexit函數
按照iso c的規定,一個進程可以登記多達32個函數,這些函數將由exit自動調用。我們稱這些函數為終止處理程序(exit handler),并調用atexit函數來登記這些函數。
int atexit(void (*function)(void));
exit調用終止處理程序順序和登記順序相反。同一函數登記多次,則也會被調用多次。
程序調用exec函數族中的任一函數,則將清除所有已安裝的終止處理程序。
注意:內核使程序執行的唯一方法是調用一個exec函數。
每個程序都會接收到一張環境表,與參數表一樣,環境表也是一個字符指針數組。全局變量environ則包含了該指針數組的地址:
extern char **environ;
malloc 初始值不確定
calloc 初始為0
realloc
在C中,goto語句是不能跨越函數的,而執行跨越函數類跳轉功能的是函數setjmp和longjmp。這兩個函數對于處理發生在深層嵌套調用中的出錯情況是非常有用的。
#include <setjmp.h>
int setjmp(jmp_buf env);
void longjmp(jmp_buf env,int val);
第八章 進程控制
pid為0的進程通常是調度進程,常常被稱為交換進程,是內核的一部分。
pid為1的進程init,
pid為2的進程是頁守護進程,負責支持虛擬存儲系統的分頁操作。
strlen:計算不包含終止null字節的字符串長度,是一個函數調用
sizeof:計算包括終止null字節的緩沖區長度,空間已知,所以sizeof在編譯時計算緩沖區長度。
fork:父子進程共享一個文件表項。父子進程各自關閉不使用的文件描述符,不相互干擾,這是網絡服務進程中經常使用的。
出于安全性方面的考慮,有些人要求在搜索路徑PATH中決還要包括當前目錄。
exec l:list v:vector
函數execl,execlp,execle要求將新程序的每個命令行參數都說明為一個單獨的參數,這種參數表以空指針結尾。
函數execv,execvp,execve則應先構造一個指向各參數的指針數組,然后將該數組地址作為這三個函數的參數。
execlp,execvp以文件名作參數,其余4個以路徑名作參數。
execle,execve可以傳遞一個指向環境表字符串指針數組的指針,其余四個函數則使用調用進程中的environ變量為新程序復制現有的環境。
每個系統對參數表和環境表的總長度都有個限制。這種限制是由ARG_MAX給出的。
我們可以將argv[0]設置為任何字符串,當login命令執行shell時就是這樣做的。
全局變量char **environ包括PATH等很多環境變量。
解釋器文件(interpreter file),其起始行的形式是: #! pathname[optional-argument]
pathname通常是絕對路徑名。
int system(const char *cmdstring); ISO C定義了system函數,但是其操作對系統的信賴性很強,在unix中,system總是可用的。
system在實現中調用了fork,exec和waitpid。
進程會計(process accounting):通過acct函數啟用禁用功能,每當進程結束時內核就寫一個會計記錄。
int setpgid(pid_t pid, pid_t pgid);
setpgid() ?sets ?the ?PGID of the process specified by pid to pgid. ?If
? ? ? ?pid is zero, then the process ID of the calling process ?is ?used. ? If
? ? ? ?pgid is zero, then the PGID of the process specified by pid is made the
? ? ? ?same as its process ID.
setpgid函數將pid進程的進程組id設置為pgid。如果這兩個參數相等,則由pid指定的進程變成進程組組長。如果pid是0,則使用調用者的進程id。
另外,如果pgid為0,則由pid指定的進程id將用作進程組id。
? ? ? ?
總結
以上是生活随笔為你收集整理的UNIX环境高级编程第二版_扫描 版笔记的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 汇编笔记一
- 下一篇: 正则表达式 perl