?
6.3??底層文件I/O操作
本節(jié)主要介紹文件I/O操作的系統(tǒng)調(diào)用,主要用到5個函數(shù):open()、read()、write()、lseek()和close()。這些函數(shù)的特點(diǎn)是不帶緩存,直接對文件(包括設(shè)備)進(jìn)行讀寫操作。這些函數(shù)雖然不是ANSI?C的組成部分,但是是POSIX的組成部分。
6.3.1??基本文件操作
(1)函數(shù)說明。
open()函數(shù)用于打開或創(chuàng)建文件,在打開或創(chuàng)建文件時可以指定文件的屬性及用戶的權(quán)限等各種參數(shù)。
close()函數(shù)用于關(guān)閉一個被打開的文件。當(dāng)一個進(jìn)程終止時,所有被它打開的文件都由內(nèi)核自動關(guān)閉,很多程序都使用這一功能而不顯示地關(guān)閉一個文件。
read()函數(shù)用于將從指定的文件描述符中讀出的數(shù)據(jù)放到緩存區(qū)中,并返回實(shí)際讀入的字節(jié)數(shù)。若返回0,則表示沒有數(shù)據(jù)可讀,即已達(dá)到文件尾。讀操作從文件的當(dāng)前指針位置開始。當(dāng)從終端設(shè)備文件中讀出數(shù)據(jù)時,通常一次最多讀一行。
write()函數(shù)用于向打開的文件寫數(shù)據(jù),寫操作從文件的當(dāng)前指針位置開始。對磁盤文件進(jìn)行寫操作,若磁盤已滿或超出該文件的長度,則write()函數(shù)返回失敗。
lseek()函數(shù)用于在指定的文件描述符中將文件指針定位到相應(yīng)的位置。它只能用在可定位(可隨機(jī)訪問)文件操作中。管道、套接字和大部分字符設(shè)備文件是不可定位的,所以在這些文件的操作中無法使用lseek()調(diào)用。
(2)函數(shù)格式。
open()函數(shù)的語法格式如表6.1所示。
表6.1 open()函數(shù)語法要點(diǎn)
所需頭文件 |
#include?<sys/types.h>?/*?提供類型pid_t的定義?*/
#include?<sys/stat.h> |
||
函數(shù)原型 |
int?open(const?char?*pathname,?int?flags,?int?perms) |
||
函數(shù)傳入值 |
pathname |
被打開的文件名(可包括路徑名) |
|
flag:文件打開的方式 |
O_RDONLY:以只讀方式打開文件 |
||
O_WRONLY:以只寫方式打開文件 |
|||
O_RDWR:以讀寫方式打開文件 |
|||
O_CREAT:如果該文件不存在,就創(chuàng)建一個新的文件,并用第三個參數(shù)為其設(shè)置權(quán)限 |
|||
O_EXCL:如果使用O_CREAT時文件存在,則可返回錯誤消息。這一參數(shù)可測試文件是否存在。此時open是原子操作,防止多個進(jìn)程同時創(chuàng)建同一個文件 |
|||
O_NOCTTY:使用本參數(shù)時,若文件為終端,那么該終端不會成為調(diào)用open()的那個進(jìn)程的控制終端 |
|||
O_TRUNC:若文件已經(jīng)存在,那么會刪除文件中的全部原有數(shù)據(jù),并且設(shè)置文件大小為0。 |
|||
O_APPEND:以添加方式打開文件,在打開文件的同時,文件指針指向文件的末尾,即將寫入的數(shù)據(jù)添加到文件的末尾 |
|||
perms |
被打開文件的存取權(quán)限 可以用一組宏定義:S_I(R/W/X)(USR/GRP/OTH) 其中R/W/X分別表示讀/寫/執(zhí)行權(quán)限 USR/GRP/OTH分別表示文件所有者/文件所屬組/其他用戶 例如,S_IRUSR?|?S_IWUSR表示設(shè)置文件所有者的可讀可寫屬性。八進(jìn)制表示法中600也表示同樣的權(quán)限 |
||
函數(shù)返回值 |
成功:返回文件描述符 |
在open()函數(shù)中,flag參數(shù)可通過“|”組合構(gòu)成,但前3個標(biāo)志常量(O_RDONLY、O_WRONLY以及O_RDWR)不能相互組合。perms是文件的存取權(quán)限,既可以用宏定義表示法,也可以用八進(jìn)制表示法。
close()函數(shù)的語法格式表6.2所示。
表6.2 close()函數(shù)語法要點(diǎn)
所需頭文件 |
#include?<unistd.h> |
函數(shù)原型 |
int?close(int?fd) |
函數(shù)輸入值 |
fd:文件描述符 |
函數(shù)返回值 |
0:成功 |
read()函數(shù)的語法格式如表6.3所示。
表6.3 read()函數(shù)語法要點(diǎn)
所需頭文件 |
#include?<unistd.h> |
函數(shù)原型 |
ssize_t?read(int?fd,?void?*buf,?size_t?count) |
函數(shù)傳入值 |
fd:文件描述符 |
buf:指定存儲器讀出數(shù)據(jù)的緩沖區(qū) |
|
count:指定讀出的字節(jié)數(shù) |
|
函數(shù)返回值 |
成功:讀到的字節(jié)數(shù) |
在讀普通文件時,若讀到要求的字節(jié)數(shù)之前已到達(dá)文件的尾部,則返回的字節(jié)數(shù)會小于希望讀出的字節(jié)數(shù)。
write()函數(shù)的語法格式如表6.4所示。
表6.4 write()函數(shù)語法要點(diǎn)
所需頭文件 |
#include?<unistd.h> |
函數(shù)原型 |
ssize_t?write(int?fd,?void?*buf,?size_t?count) |
函數(shù)傳入值 |
fd:文件描述符 |
buf:指定存儲器寫入數(shù)據(jù)的緩沖區(qū) |
|
count:指定讀出的字節(jié)數(shù) |
|
函數(shù)返回值 |
成功:已寫的字節(jié)數(shù) |
在寫普通文件時,寫操作從文件的當(dāng)前指針位置開始。
lseek()函數(shù)的語法格式如表6.5所示。
表6.5 lseek()函數(shù)語法要點(diǎn)
所需頭文件 |
#include?<unistd.h> #include?<sys/types.h> |
||
函數(shù)原型 |
off_t?lseek(int?fd,?off_t?offset,?int?whence) |
||
函數(shù)傳入值 |
fd:文件描述符 |
||
offset:偏移量,每一讀寫操作所需要移動的距離,單位是字節(jié),可正可負(fù)(向前移,向后移) |
|||
whence: 當(dāng)前位置的基點(diǎn) |
SEEK_SET:當(dāng)前位置為文件的開頭,新位置為偏移量的大小 |
||
SEEK_CUR:當(dāng)前位置為文件指針的位置,新位置為當(dāng)前位置加上偏移量 |
|||
SEEK_END:當(dāng)前位置為文件的結(jié)尾,新位置為文件的大小加上偏移量的大小 |
|||
函數(shù)返回值 |
成功:文件的當(dāng)前位移 |
?
(3)函數(shù)使用實(shí)例。
下面實(shí)例中的open()函數(shù)帶有3個flag參數(shù):O_CREAT、O_TRUNC和O_WRONLY,這樣就可以對不同的情況指定相應(yīng)的處理方法。另外,這里對該文件的權(quán)限設(shè)置為0600。其源碼如下所示:
下面列出文件基本操作的實(shí)例,基本功能是從一個文件(源文件)中讀取最后10KB數(shù)據(jù)并到另一個文件(目標(biāo)文件)。在實(shí)例中源文件是以只讀方式打開,目標(biāo)文件是以只寫方式打開(可以是讀寫方式)。若目標(biāo)文件不存在,可以創(chuàng)建并設(shè)置權(quán)限的初始值為644,即文件所有者可讀可寫,文件所屬組和其他用戶只能讀。
讀者需要留意的地方是改變每次讀寫的緩存大?。▽?shí)例中為1KB)會怎樣影響運(yùn)行效率。
/*?copy_file.c?*/
#include?<unistd.h>
#include?<sys/types.h>
#include?<sys/stat.h>
#include?<fcntl.h>
#include?<stdlib.h>
#include?<stdio.h>
#define?BUFFER_SIZE????1024??????????????/*?每次讀寫緩存大小,影響運(yùn)行效率*/
#define?SRC_FILE_NAME??"src_file"??/*?源文件名?*/
#define?DEST_FILE_NAME?"dest_file"?/*?目標(biāo)文件名文件名?*/
#define?OFFSE????????10240?????????????/*?復(fù)制的數(shù)據(jù)大小?*/
?????
int?main()
{
?????int?src_file,?dest_file;
?????unsigned?char?buff[BUFFER_SIZE];
?????int?real_read_len;
?????
?????/*?以只讀方式打開源文件?*/
?????src_file?=?open(SRC_FILE_NAME,?O_RDONLY);
?????
?????/*?以只寫方式打開目標(biāo)文件,若此文件不存在則創(chuàng)建該文件,?訪問權(quán)限值為644?*/
?????dest_file?=?open(DEST_FILE_NAME,?
?????????????????????O_WRONLY|O_CREAT,?S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH);
?????if?(src_file?<?0?||?dest_file?<?0)
?????{
??????????printf("Open?file?errorn");
??????????exit(1);
?????}
?????
?????/*?將源文件的讀寫指針移到最后10KB的起始位置*/
?????lseek(src_file,?-OFFSET,?SEEK_END);
?????
?????/*?讀取源文件的最后10KB數(shù)據(jù)并寫到目標(biāo)文件中,每次讀寫1KB?*/
?????while?((real_read_len?=?read(src_file,?buff,?sizeof(buff)))?>?0)
?????{
??????????write(dest_file,?buff,?real_read_len);
?????}
?????close(dest_file);
?????close(src_file);?
?????return?0;
}
$?./copy_file
$?ls?-lh??dest_file
-rw-r--r--?1?david?root?10K?14:06?dest_file
注意 |
open()函數(shù)返回的文件描述符一定是最小的未用文件描述符。由于一個進(jìn)程在啟動時自動打開了0、1、2三個文件描述符,因此,該文件運(yùn)行結(jié)果中返回的文件描述符為3。讀者可以嘗試在調(diào)用open()函數(shù)之前,加一句close(0),則此后在調(diào)用open()函數(shù)時返回的文件描述符為0(若關(guān)閉文件描述符1,則在程序執(zhí)行時會由于沒有標(biāo)準(zhǔn)輸出文件而無法輸出)。 |
6.3.2??文件鎖
(1)fcntl()函數(shù)說明。
前面的這5個基本函數(shù)實(shí)現(xiàn)了文件的打開、讀寫等基本操作,本小節(jié)將討論的是,在文件已經(jīng)共享的情況下如何操作,也就是當(dāng)多個用戶共同使用、操作一個文件的情況,這時,Linux通常采用的方法是給文件上鎖,來避免共享的資源產(chǎn)生競爭的狀態(tài)。
文件鎖包括建議性鎖和強(qiáng)制性鎖。建議性鎖要求每個上鎖文件的進(jìn)程都要檢查是否有鎖存在,并且尊重已有的鎖。在一般情況下,內(nèi)核和系統(tǒng)都不使用建議性鎖。強(qiáng)制性鎖是由內(nèi)核執(zhí)行的鎖,當(dāng)一個文件被上鎖進(jìn)行寫入操作的時候,內(nèi)核將阻止其他任何文件對其進(jìn)行讀寫操作。采用強(qiáng)制性鎖對性能的影響很大,每次讀寫操作都必須檢查是否有鎖存在。
在Linux中,實(shí)現(xiàn)文件上鎖的函數(shù)有l(wèi)ockf()和fcntl(),其中l(wèi)ockf()用于對文件施加建議性鎖,而fcntl()不僅可以施加建議性鎖,還可以施加強(qiáng)制鎖。同時,fcntl()還能對文件的某一記錄上鎖,也就是記錄鎖。
記錄鎖又可分為讀取鎖和寫入鎖,其中讀取鎖又稱為共享鎖,它能夠使多個進(jìn)程都能在文件的同一部分建立讀取鎖。而寫入鎖又稱為排斥鎖,在任何時刻只能有一個進(jìn)程在文件的某個部分上建立寫入鎖。當(dāng)然,在文件的同一部分不能同時建立讀取鎖和寫入鎖。
注意 |
fcntl()是一個非常通用的函數(shù),它可以對已打開的文件描述符進(jìn)行各種操作,不僅包括管理文件鎖,還包括獲得和設(shè)置文件描述符和文件描述符標(biāo)志、文件描述符的復(fù)制等很多功能。在本節(jié)中,主要介紹建立記錄鎖的方法。 |
?
(2)fcntl()函數(shù)格式。
用于建立記錄鎖的fcntl()函數(shù)格式如表6.6所示。
表6.6 fcntl()函數(shù)語法要點(diǎn)
所需頭文件 |
#include?<sys/types.h> #include?<unistd.h> #include?<fcntl.h> |
|
函數(shù)原型 |
int?fcnt1(int?fd,?int?cmd,?struct?flock?*lock) |
|
函數(shù)傳入值 |
fd:文件描述符 |
|
cmd |
F_DUPFD:復(fù)制文件描述符 |
|
F_GETFD:獲得fd的close-on-exec標(biāo)志,若標(biāo)志未設(shè)置,則文件經(jīng)過exec()函數(shù)之后仍保持打開狀態(tài) |
||
F_SETFD:設(shè)置close-on-exec標(biāo)志,該標(biāo)志由參數(shù)arg的FD_CLOEXEC位決定 |
||
F_GETFL:得到open設(shè)置的標(biāo)志 |
||
F_SETFL:改變open設(shè)置的標(biāo)志 |
||
F_GETLK:根據(jù)lock參數(shù)值,決定是否上文件鎖 |
||
F_SETLK:設(shè)置lock參數(shù)值的文件鎖 |
||
F_SETLKW:這是F_SETLK的阻塞版本(命令名中的W表示等待(wait))。 在無法獲取鎖時,會進(jìn)入睡眠狀態(tài);如果可以獲取鎖或者捕捉到信號則會返回 |
||
lock:結(jié)構(gòu)為flock,設(shè)置記錄鎖的具體狀態(tài) |
||
函數(shù)返回值 |
0:成功 |
這里,lock的結(jié)構(gòu)如下所示:
struct?flock
{
?????short?l_type;
?????off_t?l_start;
?????short?l_whence;
?????off_t?l_len;
?????pid_t?l_pid;
}
lock結(jié)構(gòu)中每個變量的取值含義如表6.7所示。
表6.7 lock結(jié)構(gòu)變量取值
l_type |
F_RDLCK:讀取鎖(共享鎖) |
||
F_WRLCK:寫入鎖(排斥鎖) |
|||
F_UNLCK:解鎖 |
|||
l_stat |
相對位移量(字節(jié)) |
||
l_whence:相對位移量的起點(diǎn)(同lseek的whence) |
SEEK_SET:當(dāng)前位置為文件的開頭,新位置為偏移量的大小 |
||
SEEK_CUR:當(dāng)前位置為文件指針的位置,新位置為當(dāng)前位置加上偏移量 |
|||
SEEK_END:當(dāng)前位置為文件的結(jié)尾,新位置為文件的大小加上偏移量的大小 |
|||
l_len |
加鎖區(qū)域的長度 |
||
小技巧 |
為加鎖整個文件,通常的方法是將l_start設(shè)置為0,l_whence設(shè)置為SEEK_SET,l_len設(shè)置為0。 |
(3)fcntl()使用實(shí)例
下面首先給出了使用fcntl()函數(shù)的文件記錄鎖功能的代碼實(shí)現(xiàn)。在該代碼中,首先給flock結(jié)構(gòu)體的對應(yīng)位賦予相應(yīng)的值。接著使用兩次fcntl()函數(shù),分別用于判斷文件是否可以上鎖和給相關(guān)文件上鎖,這里用到的cmd值分別為F_GETLK和F_SETLK(或F_SETLKW)。
用F_GETLK命令判斷是否可以進(jìn)行flock結(jié)構(gòu)所描述的鎖操作:若可以進(jìn)行,則flock結(jié)構(gòu)的l_type會被設(shè)置為F_UNLCK,其他域不變;若不可行,則l_pid被設(shè)置為擁有文件鎖的進(jìn)程號,其他域不變。
用F_SETLK和F_SETLKW命令設(shè)置flock結(jié)構(gòu)所描述的鎖操作,后者是前者的阻塞版。
文件記錄鎖功能的源代碼如下所示:
/*?lock_set.c?*/
int?lock_set(int?fd,?int?type)
{
?????struct?flock?old_lock,?lock;
?????lock.l_whence?=?SEEK_SET;
?????lock.l_start?=?0;
?????lock.l_len?=?0;
?????lock.l_type?=?type;
?????lock.l_pid?=?-1;
?????
?????/*?判斷文件是否可以上鎖?*/
?????fcntl(fd,?F_GETLK,?&lock);
?????if?(lock.l_type?!=?F_UNLCK)
?????{
??????????/*?判斷文件不能上鎖的原因?*/
??????????if?(lock.l_type?==?F_RDLCK)?/*?該文件已有讀取鎖?*/
??????????{
???????????????printf("Read?lock?already?set?by?%dn",?lock.l_pid);
??????????}
??????????else?if?(lock.l_type?==?F_WRLCK)?/*?該文件已有寫入鎖?*/
??????????{
???????????????printf("Write?lock?already?set?by?%dn",?lock.l_pid);
??????????}???????????????
?????}
?????
?????/*?l_type?可能已被F_GETLK修改過?*/
?????lock.l_type?=?type;
?????
?????/*?根據(jù)不同的type值進(jìn)行阻塞式上鎖或解鎖?*/
?????if?((fcntl(fd,?F_SETLKW,?&lock))?<?0)
?????{
??????????printf("Lock?failed:type?=?%dn",?lock.l_type);
??????????return?1;
?????}
??????????
?????switch(lock.l_type)
?????{
??????????case?F_RDLCK:
??????????{
??????????printf("Read?lock?set?by?%dn",?getpid());
??????????}
??????????break;
??????????case?F_WRLCK:
??????????{
??????????printf("Write?lock?set?by?%dn",?getpid());
??????????}
??????????break;
??????????case?F_UNLCK:
??????????{
???????????????printf("Release?lock?by?%dn",?getpid());
???????????????return?1;
??????????}
??????????break;
??????????default:
??????????break;
?????}/*?end?of?switch??*/
?????return?0;
}
?
下面的實(shí)例是文件寫入鎖的測試用例,這里首先創(chuàng)建了一個hello文件,之后對其上寫入鎖,最后釋放寫入鎖,代碼如下所示:
/*?write_lock.c?*/
#include?<unistd.h>
#include?<sys/file.h>
#include?<sys/types.h>
#include?<sys/stat.h>
#include?<stdio.h>
#include?<stdlib.h>
#include?"lock_set.c"
int?main(void)
{
?????int?fd;
?????
?????/*?首先打開文件?*/
?????fd?=?open("hello",O_RDWR?|?O_CREAT,?0644);
?????if(fd?<?0)
?????{
??????????printf("Open?file?errorn");
??????????exit(1);
?????}
?????
?????/*?給文件上寫入鎖?*/
?????lock_set(fd,?F_WRLCK);
?????getchar();
?????/*?給文件解鎖?*/
?????lock_set(fd,?F_UNLCK);
?????getchar();
?????close(fd);
?????exit(0);
}
為了能夠使用多個終端,更好地顯示寫入鎖的作用,本實(shí)例主要在PC機(jī)上測試,讀者可將其交叉編譯,下載到目標(biāo)板上運(yùn)行。下面是在PC機(jī)上的運(yùn)行結(jié)果。為了使程序有較大的靈活性,筆者采用文件上鎖后由用戶鍵入一任意鍵使程序繼續(xù)運(yùn)行。建議讀者開啟兩個終端,并且在兩個終端上同時運(yùn)行該程序,以達(dá)到多個進(jìn)程操作一個文件的效果。在這里,筆者首先運(yùn)行終端一,請讀者注意終端二中的第一句。
終端一:
$?./write_lock
write?lock?set?by?4994
release?lock?by?4994
終端二:
$?./write_lock
write?lock?already?set?by?4994
write?lock?set?by?4997
release?lock?by?4997
由此可見,寫入鎖為互斥鎖,同一時刻只能有一個寫入鎖存在。
接下來的程序是文件讀取鎖的測試用例,原理和上面的程序一樣。
/*?fcntl_read.c?*/
#include?<unistd.h>
#include?<sys/file.h>
#include?<sys/types.h>
#include?<sys/stat.h>
#include?<stdio.h>
#include?<stdlib.h>
#include?"lock_set.c"
int?main(void)
{
?????int?fd;
?????fd?=?open("hello",O_RDWR?|?O_CREAT,?0644);
?????if(fd?<?0)
?????{
??????????printf("Open?file?errorn");
??????????exit(1);
?????}
?????
?????/*?給文件上讀取鎖?*/
?????lock_set(fd,?F_RDLCK);
?????getchar();
?????/*?給文件解鎖?*/
?????lock_set(fd,?F_UNLCK);
?????getchar();
?????close(fd);
?????exit(0);
}
同樣開啟兩個終端,并首先啟動終端一上的程序,其運(yùn)行結(jié)果如下所示:
終端一:
$?./read_lock
read?lock?set?by?5009
release?lock?by?5009
終端二:
$?./read_lock
read?lock?set?by?5010
release?lock?by?5010
讀者可以將此結(jié)果與寫入鎖的運(yùn)行結(jié)果相比較,可以看出,讀取鎖為共享鎖,當(dāng)進(jìn)程5009已設(shè)置讀取鎖后,進(jìn)程5010仍然可以設(shè)置讀取鎖。
思考 |
如果在一個終端上運(yùn)行設(shè)置讀取鎖的程序,則在另一個終端上運(yùn)行設(shè)置寫入鎖的程序,會有什么結(jié)果呢?? |
6.3.3??多路復(fù)用
(1)函數(shù)說明。
前面的fcntl()函數(shù)解決了文件的共享問題,接下來該處理I/O復(fù)用的情況了。
總的來說,I/O處理的模型有5種。
n 阻塞I/O模型:在這種模型下,若所調(diào)用的I/O函數(shù)沒有完成相關(guān)的功能,則會使進(jìn)程掛起,直到相關(guān)數(shù)據(jù)到達(dá)才會返回。對管道設(shè)備、終端設(shè)備和網(wǎng)絡(luò)設(shè)備進(jìn)行讀寫時經(jīng)常會出現(xiàn)這種情況。
n 非阻塞模型:在這種模型下,當(dāng)請求的I/O操作不能完成時,則不讓進(jìn)程睡眠,而且立即返回。非阻塞I/O使用戶可以調(diào)用不會阻塞的I/O操作,如open()、write()和read()。如果該操作不能完成,則會立即返回出錯(例如:打不開文件)或者返回0(例如:在緩沖區(qū)中沒有數(shù)據(jù)可以讀取或者沒有空間可以寫入數(shù)據(jù))。
n I/O多路轉(zhuǎn)接模型:在這種模型下,如果請求的I/O操作阻塞,且它不是真正阻塞I/O,而是讓其中的一個函數(shù)等待,在這期間,I/O還能進(jìn)行其他操作。本節(jié)要介紹的select()和poll函數(shù)()就是屬于這種模型。
n 信號驅(qū)動I/O模型:在這種模型下,通過安裝一個信號處理程序,系統(tǒng)可以自動捕獲特定信號的到來,從而啟動I/O。這是由內(nèi)核通知用戶何時可以啟動一個I/O操作決定的。
n 異步I/O模型:在這種模型下,當(dāng)一個描述符已準(zhǔn)備好,可以啟動I/O時,進(jìn)程會通知內(nèi)核?,F(xiàn)在,并不是所有的系統(tǒng)都支持這種模型。
可以看到,select()和poll()的I/O多路轉(zhuǎn)接模型是處理I/O復(fù)用的一個高效的方法。它可以具體設(shè)置程序中每一個所關(guān)心的文件描述符的條件、希望等待的時間等,從select()和poll()函數(shù)返回時,內(nèi)核會通知用戶已準(zhǔn)備好的文件描述符的數(shù)量、已準(zhǔn)備好的條件等。通過使用select()和poll()函數(shù)的返回結(jié)果,就可以調(diào)用相應(yīng)的I/O處理函數(shù)。
?
(2)函數(shù)格式。
select()函數(shù)的語法格式如表6.8所示。
表6.8 select()函數(shù)語法要點(diǎn)
所需頭文件 |
#include?<sys/types.h> #include?<sys/time.h> #include?<unistd.h> |
|
函數(shù)原型 |
int?select(int?numfds,?fd_set?*readfds,?fd_set?*writefds, fd_set?*exeptfds,?struct?timeval?*timeout) |
|
函數(shù)傳入值 |
numfds:該參數(shù)值為需要監(jiān)視的文件描述符的最大值加1 |
|
readfds:由select()監(jiān)視的讀文件描述符集合 |
||
writefds:由select()監(jiān)視的寫文件描述符集合 |
||
exeptfds:由select()監(jiān)視的異常處理文件描述符集合 |
||
timeout? |
NULL:永遠(yuǎn)等待,直到捕捉到信號或文件描述符已準(zhǔn)備好為止 |
|
具體值:struct?timeval類型的指針,若等待了timeout時間還沒有檢測到任何文件描符準(zhǔn)備好,就立即返回 |
||
0:從不等待,測試所有指定的描述符并立即返回 |
||
函數(shù)返回值 |
大于0:成功,返回準(zhǔn)備好的文件描述符的數(shù)目 |
思考 |
請讀者考慮一下如何確定被監(jiān)視的文件描述符的最大值? |
可以看到,select()函數(shù)根據(jù)希望進(jìn)行的文件操作對文件描述符進(jìn)行了分類處理,這里,對文件描述符的處理主要涉及4個宏函數(shù),如表6.9所示。
表6.9 select()文件描述符處理函數(shù)
FD_ZERO(fd_set?*set) |
清除一個文件描述符集 |
FD_SET(int?fd,?fd_set?*set) |
將一個文件描述符加入文件描述符集中 |
FD_CLR(int?fd,?fd_set?*set) |
將一個文件描述符從文件描述符集中清除 |
FD_ISSET(int?fd,?fd_set?*set) |
如果文件描述符fd為fd_set集中的一個元素,則返回非零值,可以用于調(diào)用select()之后測試文件描述符集中的文件描述符是否有變化 |
一般來說,在使用select()函數(shù)之前,首先使用FD_ZERO()和FD_SET()來初始化文件描述符集,在使用了select()函數(shù)時,可循環(huán)使用FD_ISSET()來測試描述符集,在執(zhí)行完對相關(guān)文件描述符的操作之后,使用FD_CLR()來清除描述符集。
另外,select()函數(shù)中的timeout是一個struct?timeval類型的指針,該結(jié)構(gòu)體如下所示:
struct?timeval?
{
????????long?tv_sec;?/*?秒?*/
????????long?tv_unsec;?/*?微秒?*/
}
可以看到,這個時間結(jié)構(gòu)體的精確度可以設(shè)置到微秒級,這對于大多數(shù)的應(yīng)用而言已經(jīng)足夠了。
poll()函數(shù)的語法格式如表6.10所示。
表6.10 poll()函數(shù)語法要點(diǎn)
所需頭文件 |
#include?<sys/types.h> #include?<poll.h> |
函數(shù)原型 |
int?poll(struct?pollfd?*fds,?int?numfds,?int?timeout) |
函數(shù)傳入值 |
fds:struct?pollfd結(jié)構(gòu)的指針,用于描述需要對哪些文件的哪種類型的操作進(jìn)行監(jiān)控。 struct?pollfd { int????fd;???????/*?需要監(jiān)聽的文件描述符?*/ short??events;????/*?需要監(jiān)聽的事件?*/ short??revents;???/*?已發(fā)生的事件?*/ } events成員描述需要監(jiān)聽哪些類型的事件,可以用以下幾種標(biāo)志來描述。 POLLIN:文件中有數(shù)據(jù)可讀,下面實(shí)例中使用到了這個標(biāo)志 POLLPRI::文件中有緊急數(shù)據(jù)可讀 POLLOUT:可以向文件寫入數(shù)據(jù) POLLERR:文件中出現(xiàn)錯誤,只限于輸出 POLLHUP:與文件的連接被斷開了,只限于輸出 POLLNVAL:文件描述符是不合法的,即它并沒有指向一個成功打開的文件 |
numfds:需要監(jiān)聽的文件個數(shù),即第一個參數(shù)所指向的數(shù)組中的元素數(shù)目 |
|
timeout:表示poll阻塞的超時時間(毫秒)。如果該值小于等于0,則表示無限等待 |
|
函數(shù)返回值 |
成功:返回大于0的值,表示事件發(fā)生的pollfd結(jié)構(gòu)的個數(shù)? 0:超時;-1:出錯 |
(3)使用實(shí)例。
由于多路復(fù)用通常用于I/O操作可能會被阻塞的情況,而對于可能會有阻塞I/O的管道、網(wǎng)絡(luò)編程,本書到現(xiàn)在為止還沒有涉及。這里通過手動創(chuàng)建(用mknod命令)兩個管道文件,重點(diǎn)說明如何使用兩個多路復(fù)用函數(shù)。
在本實(shí)例中,分別用select()函數(shù)和poll()函數(shù)實(shí)現(xiàn)同一個功能,以下功能說明是以select()函數(shù)為例描述的。
本實(shí)例中主要實(shí)現(xiàn)通過調(diào)用select()函數(shù)來監(jiān)聽3個終端的輸入(分別重定向到兩個管道文件的虛擬終端以及主程序所運(yùn)行的虛擬終端),并分別進(jìn)行相應(yīng)的處理。在這里我們建立了一個select()函數(shù)監(jiān)視的讀文件描述符集,其中包含3個文件描述符,分別為一個標(biāo)準(zhǔn)輸入文件描述符和兩個管道文件描述符。通過監(jiān)視主程序的虛擬終端標(biāo)準(zhǔn)輸入來實(shí)現(xiàn)程序的控制(例如:程序結(jié)束);以兩個管道作為數(shù)據(jù)輸入,主程序?qū)膬蓚€管道讀取的輸入字符串寫入到標(biāo)準(zhǔn)輸出文件(屏幕)。
為了充分表現(xiàn)select()調(diào)用的功能,在運(yùn)行主程序的時候,需要打開3個虛擬終端:首先用mknod命令創(chuàng)建兩個管道in1和in2。接下來,在兩個虛擬終端上分別運(yùn)行cat>in1和cat>in2。同時在第三個虛擬終端上運(yùn)行主程序。在程序運(yùn)行之后,如果在兩個管道終端上輸入字符串,則可以觀察到同樣的內(nèi)容將在主程序的虛擬終端上逐行顯示。如果想結(jié)束主程序,只要在主程序的虛擬終端下輸入以‘q’或‘Q’字符開頭的字符串即可。如果三個文件一直在無輸入狀態(tài)中,則主程序一直處于阻塞狀態(tài)。為了防止無限期的阻塞,在select程序中設(shè)置超時值(本實(shí)例中設(shè)置為60s),當(dāng)無輸入狀態(tài)持續(xù)到超時值時,主程序主動結(jié)束運(yùn)行并退出。而poll程序中依然無限等待,當(dāng)然poll()函數(shù)也可以設(shè)置超時參數(shù)。
該程序的流程圖如圖6.2所示。
圖6.2??select實(shí)例流程圖
?
使用select()函數(shù)實(shí)現(xiàn)的代碼如下所示:
/*?multiplex_select?*/
#include?<fcntl.h>
#include?<stdio.h>
#include?<unistd.h>
#include?<stdlib.h>
#include?<time.h>
#include?<errno.h>
#define?MAX_BUFFER_SIZE?????1024???????????????/*?緩沖區(qū)大小*/
#define?IN_FILES??????????????3??????????????????/*?多路復(fù)用輸入文件數(shù)目*/
#define?TIME_DELAY????????????60?????????????????/*?超時值秒數(shù)?*/
#define?MAX(a,?b)?????????????((a?>?b)?(a):(b))
int?main(void)
{
?????int?fds[IN_FILES];
?????char?buf[MAX_BUFFER_SIZE];
?????int?i,?res,?real_read,?maxfd;
?????struct?timeval?tv;
?????fd_set?inset,tmp_inset;
?????
?????/*首先以只讀非阻塞方式打開兩個管道文件*/
?????fds[0]?=?0;
?????
?????if((fds[1]?=?open?("in1",?O_RDONLY|O_NONBLOCK))?<?0)
?????{
??????????printf("Open?in1?errorn");
??????????return?1;
?????}
??????????????
?????if((fds[2]?=?open?("in2",?O_RDONLY|O_NONBLOCK))?<?0)
?????{
??????????printf("Open?in2?errorn");
??????????return?1;
?????}
?????/*取出兩個文件描述符中的較大者*/
?????maxfd?=?MAX(MAX(fds[0],?fds[1]),?fds[2]);
?????/*初始化讀集合inset,并在讀集合中加入相應(yīng)的描述集*/
?????FD_ZERO(&inset);?
?????for?(i?=?0;?i?<?IN_FILES;?i++)
?????{
?????????FD_SET(fds[i],?&inset);
?????}
?????FD_SET(0,?&inset);
?????tv.tv_sec?=?TIME_DELAY;
?????tv.tv_usec?=?0;
?????
?????/*循環(huán)測試該文件描述符是否準(zhǔn)備就緒,并調(diào)用select函數(shù)對相關(guān)文件描述符做對應(yīng)操作*/
?????while(FD_ISSET(fds[0],&inset)?
????????????||?FD_ISSET(fds[1],&inset)?||?FD_ISSET(fds[2],?&inset))
?????{?
??????????/*?文件描述符集合的備份,?這樣可以避免每次進(jìn)行初始化?*/
??????????tmp_inset?=?inset;?
??????????res?=?select(maxfd?+?1,?&tmp_inset,?NULL,?NULL,?&tv);
??????????
??????????switch(res)
??????????{
??????????case?-1:
??????????????{
???????????????????printf("Select?errorn");
???????????????????return?1;
??????????????}
??????????????break;
???????????????
???????????????case?0:?/*?Timeout?*/
???????????????{
???????????????????printf("Time?outn");
???????????????????return?1;
??????????????}??????????
??????????????break;
???????????????
???????????????default:
???????????????{
???????????????????for?(i?=?0;?i?<?IN_FILES;?i++)
???????????????????{
????????????????????????f?(FD_ISSET(fds[i],?&tmp_inset))
????????????????????????{
????????????????????????????memset(buf,?0,?MAX_BUFFER_SIZE);
????????????????????????????real_read?=?read(fds[i],?buf,?MAX_BUFFER_SIZE);
????????????????????
????????????????????????????if?(real_read?<?0)
????????????????????????????{
?????????????????????????????????if?(errno?!=?EAGAIN)
?????????????????????????????????{
?????????????????????????????????????return?1;
?????????????????????????????????}
????????????????????????????}
?????????????????????????????else?if?(!real_read)
?????????????????????????????{
?????????????????????????????????close(fds[i]);
?????????????????????????????????FD_CLR(fds[i],?&inset);
????????????????????????????}
????????????????????????????else
????????????????????????????{
?????????????????????????????????if?(i?==?0)
?????????????????????????????????{/*?主程序終端控制?*/
??????????????????????????????????????if?((buf[0]?==?'q')?||?(buf[0]?==?'Q'))
??????????????????????????????????????{
???????????????????????????????????????????return?1;
??????????????????????????????????????}
?????????????????????????????????}
?????????????????????????????????else
?????????????????????????????????{/*?顯示管道輸入字符串?*/
??????????????????????????????????????buf[real_read]?=?'