當從一個描述符讀,然后又寫到另一個描述符時,可以在下列形式的循環中使用阻塞I/O:
while ((n = read(STDIN_FILENO, buf, BUFSIZ)) > 0) if (write(STDOUT_FILENO, buf, n) != n) err_sys("write error");
這種形式的阻塞I/O到處可見。但是如果必須從兩個描述符讀,又將如何呢?如果仍舊使用阻塞I/O,那么就可能長時間阻塞在一個描述符上,而另一個描述符雖有很多數據卻不能得到及時處理。所以為了處理這種情況顯然需要另一種不同的技術。
讓我們觀察telnet(1)命令的結構。該程序讀終端(標準輸入),將所得數據寫到網絡連接上;同時讀網絡連接,將所得數據寫到終端上(標準輸出)。在網絡連接的另一端,telnetd守護進程讀用戶在終端上所鍵入的內容,并將其送給shell,這如同用戶登錄在遠程機器上一樣。telnetd守護進程將執行用戶鍵入命令,而產生的輸出通過telnet命令回送給用戶,并顯示在用戶終端上。圖14-6顯示這種工作情景。
圖14-6 telnet程序概觀
telnet進程有兩個輸入、兩個輸出。對于這兩個輸入中的任一個都不能使用阻塞read,因為我們永遠不知道哪一個輸入有我們需要的數據。
(參考方法一)處理這種特殊問題的一種方法是,用fork將一個進程變成兩個進程,每個進程處理一條數據通路。圖14-7顯示了這種安排。
圖14-7 使用兩個進程實現telnet程序
如果使用兩個進程,則可使每個進程都執行阻塞read。但是這也產生了問題:操作什么時候終止?如果子進程接收到文件結束符,telnetd守護進程使網絡連接斷開,那么該子進程終止,然后父進程接收到SIGCHILD信號。但是,如若父進程終止(用戶在終端上鍵入了文件結束符),那么父進程應通知子進程停止。為此可以使用一個信號(例如SIGUSR1),但這使程序變得更加復雜。
(參考方法二)我們可以不使用兩個進程,而是用一個進程中的兩個線程。這避免了終止的復雜性,但卻要求處理線程之間的同步,在減少復雜性方面可能會是得不償失。
(參考方法三)另一個方法是仍舊使用一個進程執行該程序,但使用非阻塞I/O讀取數據。基本方法是將兩個輸入描述符都設置為非阻塞的,對第一個描述符發一個read。如果該輸入上有數據,則讀數據并處理它;如果無數據可讀,則read立即返回。然后對第二個描述符作同樣的處理。在此之后,等待若干秒,然后再讀第一個描述符(這里用一個無限循環)。這種形式的循環稱為輪詢(polling)。這種方法的不足之處是浪費CPU時間。因為大多數時間實際上是無數據可讀的,但是仍花費時間不斷反復執行read系統調用。在每次循環后要等多長時間再執行下一輪循環也很難確定。雖然輪詢技術在支持非阻塞I/O的系統上都可使用,但是在多任務系統中應當避免使用這種方法。
(參考方法四)還有一種技術稱之為異步I/O(asynchronous I/O)。其基本思想是進程告訴內核,當一個描述符已準備好可以進行I/O時,用一個信號通知它。這種技術有兩個問題。第一,并非所有系統都支持這種機制(在Single UNIX Specification中這是一個可選擇的設施)。系統V為此技術提供了SIGPOLL信號,但是僅當描述符引用STREAMS設備時,此信號才能工作。BSD有一個類似的信號SIGIO,但也有類似的限制,僅當描述符引用終端設備或網絡時才能工作。其次,這種信號對每個進程而言只有1一個(SIGPOLL或SIGIO)。如果使該信號對兩個描述符都起作用,那么在接到此信號時進程無法判別是哪一個描述符已準備好可以進行I/O。為了確定是哪一個,仍需將這兩個描述符都設置為非阻塞的,并順序試執行I/O。
(有效方法)一種比較好的技術是使用I/O多路轉接(I/O multiplexing)。先構造一張有關描述符的列表,然后調用一個函數,直到這些描述符中的一個已準備好進行I/O時,該函數才返回。在返回時,它告訴進程哪些描述符已準備好可以進行I/O。
poll、pselect和select這三個函數使我們能夠執行I/O多路轉接。注意基本POSIX.1標準定義了select函數,而poll則是對該基本部分的XSI擴展。
POSIX指定,為了在程序中使用select,必須包括<sys/select.h>。但是歷史上,為了在程序中使用select,還要包括另外三個頭文件,而且某些實現至今還落后在標準之后。為此,要查看select手冊頁,弄清楚你所用的系統對它支持到何種程度。較老的系統要求在程序中包括<sys/types.h>、<sys/time.h>和<unistd.h>。
1、select和pselect函數
在所有依從POSIX的平臺上,select函數使我們可以執行I/O多路轉接。傳向select的參數告訴內核:
從select返回時,內核告訴我們:
使用這些返回信息,就可調用相應的I/O函數(一般是read或write),并且確知該函數不會阻塞。
#include <sys/select.h>int select(int maxfdpl, fd_set *restrict readfds, fd_set *restrict writefds, fd_set *restrict exceptfds, struct timeval *restrict tvptr);返回值:準備就緒的描述符數,若超時則返回0,若出錯則返回-1
先說明最后一個參數,它指定愿意等待的時間:
struct timeval { long tv_sec; /* seconds */ long tv_usec; /* and microseconds */};
有三種情況:
tvptr==NULL
永遠等待。如果捕捉到一個信號則中斷此無限期等待。當所指定的描述符中的一個已準備好或捕捉到一個信號則返回。如果捕捉到一個信號,則select返回-1,errno設置為EINTR。
tvptr->tv_sec==0 && tvptr->tv_usec==0
完全不等待。測試所有指定的描述符并立即返回。這是得到多個描述符的狀態而不阻塞select函數的輪詢方法。
tvptr->tv_sec!=0 || tvptr->tv_usec!=0
等待指定的秒數和微秒數。當指定的描述符之一已準備好,或當指定的時間值已經超過時立即返回。如果在超時時還沒有一個描述符準備好,則返回值是0(如果系統不提供微秒分辨率,則tvptr->tv_usec值取整到最近的支持值)。與第一種情況一樣,這種等待可被捕捉到的信號中斷。
POSIX.1允許在實現中修改timeval結構中的值,所以在select返回后,你不能指望該結構仍舊保持調用select之前它所包含的值。
中間的三個參數readfds、writefds和exceptfds是指向描述符集的指針。這三個描述符集說明了我們關心的可讀、可寫或處于異常條件的各個描述符。每個描述符集存放在一個fd_set數據類型中。這種數據類型為每一可能的描述符保持了一位,其實現可如圖14-8中所示。
圖14-8 對select指定讀、寫和異常條件描述符
對fd_set數據類型可以進行的處理是:分配一個這種類型的變量;將這種類型的一個變量值賦予同類型的另一個變量;或對于這種類型的變量使用下列四個函數中的一個。
#include <sys/select.h>int FD_ISSET(int fd, fd_set *fdset);返回值:若fd在描述符集中則返回非0值,否則返回0void FD_CLR(int fd, fd_set *fdset);void FD_SET(int fd, fd_set *fdset);void FD_ZERO(fd_set *fdset);
這些接口可實現為宏或函數。調用FD_ZERO將一個指定的fd_set變量的所有位設置為0。調用FD_SET設置一個fd_set變量的指定位。調用FD_CLR則將一指定位清除。最后,調用FD_ISSET測試一指定位是否設置。
聲明了一個描述符集后,必須用FD_ZERO清除其所有位,然后在其中設置我們關心的各個位。這種操作序列如下所示:
fd_set rset;int fd;FD_ZERO(&rset);FD_SET(fd, &rset);FD_SET(STDIN_FILENO, &rset);
從select返回時,用FD_ISSET測試該集中的一個給定位是否仍舊設置:
if (FD_ISSET(fd, &rset)){ ...}
select的中間三個參數(指向描述符集的指針)中的任意一個或全部都可以是空指針,這表示對相應狀態并不關心。如果所有三個指針都是空指針,則select提供了較sleep更精確的計時器。(回憶http://www.CUOXin.com/nufangrensheng/p/3517365.html中,sleep等待整數秒,而對于select,其等待的時間可以小于1s;其實際分辨率取決于系統時鐘。)
select的第一個參數maxfdpl的意思是“最大描述符加1”。在三個描述符集中找出最大描述符編號值,然后加1,這就是第一個參數值。也可將第一個參數設置為FD_SETSIZE,這是<sys/select.h>中的一個常量,它說明了最大的描述符數(經常是1024)。但是對大多數應用程序而言,此值太大了,多數應用程序只使用3-10個描述符。(某些應用程序使用更多的描述符,但這種UNIX程序并不具代表性。) 如果將第一個參數設置為我們所關注的最大描述符編號值加1,內核就只需在此范圍內尋址打開的位,而不必在三個描述符集中的百位內搜索。
例如,若編寫如下代碼:
fd_set readset, writeset;FD_ZERO(&readset);FD_ZERO(&writeset);FD_SET(0, &readset);FD_SET(3, &readset);FD_SET(1, &writeset);FD_SET(2, &writeset);select(4, &readset, &writeset, NULL, NULL);
那么,下圖顯示了這兩個描述符集的情況。
因為描述符編號從0開始,所以要在最大描述符編號值上加1。第一個參數實際上是要檢查的描述符數(從描述符0開始)。
select有三個可能的返回值。
(1)返回值-1表示出錯。出錯是有可能的,例如在指定的描述符都沒有準備好時捕捉到一個信號。在此種情況下,將不修改其中任何描述符集。
(2)返回值0表示沒有描述符準備好。若指定的描述符都沒有準備好,而且指定的時間已經超過,則發生這種情況。此時,所有描述符集皆被清0。
(3)正返回值表示已經準備好的描述符數,該值是三個描述符集中已準備好的描述符數之和,所以如果同一描述符已準備好讀和寫,那么在返回值中將其記為2。在這種情況下,三個描述符集仍舊打開的位對應于已準備好的描述符。
對于“準備好”的意思要作一些更具體的說明:
現在,異常狀態包括(a)在網絡連接上到達的帶外數據(http://blog.chinaunix.net/uid-27164517-id-3275870.html),或者(b)在處于數據包模式的偽終端上發生了某些狀態。
應當理解,一個描述符阻塞與否并不影響select是否阻塞。也就是說,如果希望讀一個非阻塞描述符,并且以超時值為5s調用select,則select最多阻塞5s。相類似地,如果指定一個無限超時值,則在該描述符數據準備好或捕捉到一個信號之前,select一直阻塞。
如果在一個描述符上碰到了文件結尾處,則select認為該描述符是可讀的。然后調用read,它返回0,這是UNIX系統指示到達文件結尾處的方法。(很多人錯誤地認為,當到達文件結尾處時,select會指示一個異常狀態。)
POSIX.1也定義了一個select的變體,它被稱為pselect。
#include <sys/select.h>int pselect(int maxffdpl, fd_set *restrict readfds, fd_set *restrict writefds, fd_set *restrict exceptfds, const struct timespec *restrict tsptr, const sigset_t *restrict sigmask);返回值:準備就緒的描述符數,若超時則返回0,若出錯則返回-1
除下列幾點外,pselect與select相同:
select的超時值用timeval結構指定,但pselect使用timespec結構。(回憶http://www.CUOXin.com/nufangrensheng/p/3521654.html中timespec結構的定義。) timespec結構以秒和納秒表示超時值,而非秒和微秒。如果平臺支持這樣精細的粒度,那么timespec就提供了更精準的超時時間。
pselect的超時值被聲明為const,這保證了調用pselect不會改變此值。
對于pselect可使用一可選擇的信號屏蔽字。若sigmask為空,那么在與信號有關的方面,pselect的運行狀況和select相同。否則,sigmask指向一信號屏蔽字,在調用pselect時,以原子操作的方式安裝該信號屏蔽字。在返回時回復以前的信號屏蔽字。
2、poll函數
poll函數類似于select,但是其程序員接口則有所不同。我們將會看到,雖然poll函數可用于任何類型的文件描述符,但它起源于系統V,所以poll與STREAMS系統僅僅相關。
#include <poll.h>int poll(struct pollfd fdarray[], nfds_t nfds, int timeout);返回值:準備就緒的描述符數,若超時則返回0,若出錯則返回-1
與select不同,poll不是為每個狀態(可讀性、可寫性和異常狀態)構造一個描述符集,而是構造一個pollfd結構數組,每個數組元素指定一個描述符編號以及其所關心的狀態。
struct pollfd { int fd; /* file descriptor to check, or <0 to ignore */ short events; /* events of interest on fd */ short revents; /* events that occurred on fd */};
fdarray數組中的元素數由nfds說明。
應將每個數組元素的events成員設置為表14-6中所示的值。通過這些值告訴內核我們對該描述符關心的是什么。返回時,內核設置revents成員,以說明對于該描述符已經發生了什么事件。(注意,poll沒有更改events成員,這與select不同,select修改其參數以指示哪一個描述符已準備好了。)
表14-6 poll的events和revents標志
表14-6中頭四行測試可讀性,接著三行測試可寫性,最后三行則是測試異常狀態。最后三行是由內核在返回時設置的。即使在events字段中沒有指定這三個值,如果相應條件發生,則在revents中也會返回它們。
當一個描述符被掛斷(POLLHUP)后,就不能在寫向該描述符。但是仍可能從該描述符讀取到數據。
poll的最后一個參數說明我們愿意等待多少時間。如同select一樣,有三種不同的情形:
timeout == –1 永遠等待。(某些系統在<stropts.h>中定義了常量INFTIM,其值通常是-1。)當所指定的描述符中的一個已準備好,或捕捉到一個信號時則返回。如果捕捉到一個信號,則poll返回-1,errno設置為EINTR。
timeout == 0 不等待。測試所有描述符并立即返回。這是得到很多個描述符的狀態而不阻塞poll函數的輪詢方法。
timeout > 0 等待timeout毫秒。當指定的描述符之一已經準備好,或指定的時間值已超過時立即返回。如果已超過但是還沒有一個描述符準備好,則返回值是0.(如果系統不提供毫秒分辨率,則timeout值取整到最近的支持值。)
應當理解文件結束與掛斷之間的區別。如果正從終端輸入數據,并鍵入文件結束字符,POLLIN被打開,于是就可讀文件結束指示(read返回0)。POLLHUP在revents中沒有打開。如果正在讀調制解調器,并且電話已掛斷,則在revents中將接到POLLHUP通知。
與select一樣,不論一個描述符是否阻塞,都不影響poll是否阻塞。
select和poll的可中斷性
中斷的系統調用的自動再啟動是由4.2BSD引進的(見http://www.CUOXin.com/nufangrensheng/p/3515035.html) ,但當時select函數是不再啟動的。這種特性在大多數系統中一直延續了下來,即使指定了SA_RESTART也是如此。但是,在SVR4之下,如果指定了SA_RESTART,那么select和poll也是自動再啟動的。為了在將軟件移植到SVR4派生的系統上時防止這一點,如果信號可能終端對select或poll的調用,則總是使用signal_intr函數(見http://www.CUOXin.com/nufangrensheng/p/3515945.html中的程序清單10-13)。
本篇博文內容摘自《UNIX環境高級編程》(第二版),僅作個人學習記錄所用。關于本書可參考:http://www.apuebook.com/。
新聞熱點
疑難解答