I/O多路復(fù)用
應(yīng)用程序常常需要在多于一個(gè)文件描述符上阻塞寻狂。在不使用線程,尤其是獨(dú)立處理每一個(gè)文件的情況下朋沮,進(jìn)程無(wú)法在多個(gè)文件描述符上同時(shí)阻塞蛇券。尤其是對(duì)于網(wǎng)絡(luò)應(yīng)用程序而言,同時(shí)打開(kāi)的多個(gè)套接字樊拓,會(huì)誘發(fā)潛在的問(wèn)題纠亚。
非阻塞IO可以作為這個(gè)問(wèn)題的一個(gè)解決方案,但這種方法效率較差筋夏。首先菜枷,進(jìn)程要以某種不確定的方式不斷發(fā)起I/O操作,直到某個(gè)打開(kāi)的文件描述符準(zhǔn)備好進(jìn)行I/O叁丧。其次,如果程序可以睡眠的話將更加有效岳瞭,可以讓處理器進(jìn)行其他工作拥娄,直到一個(gè)或更多文件描述符可以進(jìn)行I/O時(shí)再喚醒。
- I/O多路復(fù)用
I/O多路復(fù)用允許應(yīng)用在多個(gè)文件描述符上同時(shí)阻塞瞳筏,并在其中某個(gè)可以讀寫(xiě)時(shí)收到通知稚瘾。
這時(shí)I/O多路復(fù)用就成了應(yīng)用的關(guān)鍵所在,一般來(lái)講I/O多路復(fù)用的設(shè)計(jì)遵循以下原則:
- I/O多路復(fù)用:當(dāng)任何文件描述符準(zhǔn)備好I/O時(shí)告訴我
- 在一個(gè)或更多文件描述符就緒前始終處于睡眠狀態(tài)
- 喚醒:哪個(gè)準(zhǔn)備好了姚炕?
- 在不阻塞的情況下處理所有I/O就緒的文件描述符摊欠。
- 返回第一步,重新開(kāi)始柱宦。
Linux提供了三種I/O多路復(fù)用方案:select, poll和epoll些椒。其中epoll是Linux特有的高級(jí)方法。
-
select( )
select( )系統(tǒng)調(diào)用提供了一種實(shí)現(xiàn)同步I/O多路復(fù)用的機(jī)制:
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int select(int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
在指定的文件描述符準(zhǔn)備好I/O之前或者超過(guò)一定的時(shí)間限制掸刊,select( )調(diào)用就會(huì)阻塞免糕。
監(jiān)測(cè)的文件描述符可以分為三類,分別等待不同的時(shí)間。
- 監(jiān)測(cè)readfds集合中的fd石窑,確認(rèn)其中是否有可讀數(shù)據(jù)(也就是說(shuō)牌芋,讀操作可以無(wú)阻塞的完成)
- 檢測(cè)writefds集合中的fd,確認(rèn)其中是否有一個(gè)寫(xiě)操作可以不阻塞地完成
- 監(jiān)測(cè)exceptfds集合中的fd松逊,確認(rèn)其中是否有出現(xiàn)異常發(fā)生或者出現(xiàn)帶外(out-of-band)數(shù)據(jù)(這種情況只適用于套接字)躺屁。
如果指定的集合為NULL,則seletc( )不對(duì)此類時(shí)間進(jìn)行監(jiān)視经宏。
成功返回時(shí)犀暑,每個(gè)集合只包含對(duì)應(yīng)類型的I/O就緒的文件描述符。
第一個(gè)參數(shù)n烛恤,等于所有集合中文件描述符的最大值加一母怜。這樣,select( )的調(diào)用者需要找到最大的文件描述符值缚柏,并將其加一后傳給第一個(gè)參數(shù)苹熏。
最后的timeout參數(shù)是一個(gè)指向timeval結(jié)構(gòu)體的指針,定義如下:
#include <sys/time.h>
struct timeval {
long tv_sec; /* seconds */
long tv_usec; /* microseconds */
};
如果這個(gè)參數(shù)不是NULL币喧,即時(shí)此時(shí)沒(méi)有文件描述符處于I/O就緒狀態(tài)轨域,select( )調(diào)用也將在tv_sec秒tv_usec微秒后返回。返回時(shí)杀餐,這個(gè)結(jié)構(gòu)圖的狀態(tài)是未定義的干发。
- 較新版本的Linux會(huì)自動(dòng)將該值改為剩余的時(shí)間。
- 如果時(shí)限中的兩個(gè)值都是零史翘,調(diào)用會(huì)立即返回枉长,并報(bào)告調(diào)用時(shí)所有事件對(duì)應(yīng)的文件描述符均不可用,且不等待任何后續(xù)事件琼讽。
三個(gè)集合中的fds并不直接操作必峰,而是通過(guò)以下輔助宏來(lái)進(jìn)行管理:
- FD_ZERO從指定的集合中移除所有文件描述符:
fd_set writefds;
FD_ZERO(&writefds);
- FD_SET想指定集合中添加一個(gè)文件描述符
FD_SET(fd, &writefds);
- FD_CLR從指定集合中移除一個(gè)文件描述符,設(shè)計(jì)良好的代碼應(yīng)該從不使用FD_CLR钻蹬,一般來(lái)講吼蚁,很少使用該宏
FD_CLR(fd, &writefds);
- FD_ISSET測(cè)試一個(gè)文件描述符在不在給定集合中。如果在问欠,返回一個(gè)非0值肝匆,不在,返回0顺献。一般在select( )調(diào)用返回后使用FD_ISSET來(lái)檢查一個(gè)文件描述符是否就緒旗国。
if (FD_ISSET(fd, &readfds))
/* 'fd' is readable without blocking! */
- 由于文件描述符集合是靜態(tài)建立的,所以對(duì)于文件描述符數(shù)量的上限和文件描述符的最大值均有限制注整。二者都由FD_SETSIZE設(shè)定粗仓。在Linux上嫁怀,這個(gè)值是1024。
用select( )實(shí)現(xiàn)可移植的sleep( )
由于select( )在各種Unix系統(tǒng)中都很容易實(shí)現(xiàn)借浊,相對(duì)于微妙級(jí)精度的睡眠機(jī)制來(lái)講塘淑,經(jīng)常將select( )做為一種可移植的微秒級(jí)的睡眠機(jī)制。
- 該方法通過(guò)將三個(gè)集合值設(shè)為空NULL蚂斤,將超時(shí)值設(shè)置為non-NULL來(lái)實(shí)現(xiàn)存捺。
struct timeval tv;
tv.tv_sec = 0;
tv.tv_usec = 500;
/* sleep for 500 microseconds */
select(0, NULL, NULL, NULL, &tv);
pselect( )
POSIX定義了自己的方法--pselect( ):
#define _XOPEN_SOURCE 600
#include <sys/select.h>
int pselect(int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, const struct timespec *timeout, const sigset_t *sigmask);
四個(gè)宏定義與select的相同。
pselect( )和select( )有三點(diǎn)不同:
- pselect( )的timeout參數(shù)使用了timespec結(jié)構(gòu)曙蒸。timespec使用秒和納秒捌治,而select的timeval使用秒和豪秒。理論上timespec更精準(zhǔn)一些纽窟。
- pselect( )調(diào)用并不修改timeout參數(shù)肖油。
- pselect( )比select( )多一個(gè)sigmask參數(shù)。當(dāng)這個(gè)參數(shù)被設(shè)置為零時(shí)臂港,pselect( )的行為等同于select( ).
- 添加pselect( )到Unix工具箱的主要原因是為了增加sigmask參數(shù)森枪,以此來(lái)解決信號(hào)和等待文件描述符之間的競(jìng)爭(zhēng)關(guān)系。
- pselect( )提供了一組可阻塞的信號(hào)审孽,阻塞的信號(hào)直到解除阻塞才會(huì)被處理县袱。
- 如果不考慮pselect( )中的改進(jìn),大多數(shù)應(yīng)用會(huì)繼續(xù)使用select( )佑力,部分是出于習(xí)慣式散,其他則是考慮可移植性。
poll( )
poll( )系統(tǒng)調(diào)用是system V的I/O多路復(fù)用解決方案打颤。
#include <sys/poll.h>
int poll(struct pollfd *fds, unsigned int nfds, int timeout);
poll( )使用一個(gè)簡(jiǎn)單的nfds個(gè)pollfd結(jié)構(gòu)體構(gòu)成的數(shù)組暴拄,fds指向該數(shù)組
#include <sys/poll.h>
struct pollfd {
int fd; /* file descriptor */
short events; /* requested events to watch */
short revents; /* returned events witnessed */
};
每個(gè)pollfd結(jié)構(gòu)體指定監(jiān)視單一的文件描述符”嘟龋可以傳遞多個(gè)結(jié)構(gòu)體乖篷,使得poll( )監(jiān)視多個(gè)文件描述符。
- events是要監(jiān)視的fd事件的一組位掩碼反肋,用戶設(shè)置這個(gè)字段
- revents是發(fā)生在該fd上的事件的位掩碼,內(nèi)核在返回時(shí)設(shè)置這個(gè)字段
下面是合法的事件:
- POLLIN:沒(méi)有數(shù)據(jù)可讀
- POLLRDNORM:有正常數(shù)據(jù)可讀
- POLLRDBAND:有優(yōu)先數(shù)據(jù)可讀
- POLLPRI:有高優(yōu)先級(jí)數(shù)據(jù)可讀
- POLLOUT:寫(xiě)操作不會(huì)阻塞
- POLLWRNORM:寫(xiě)正常數(shù)據(jù)不會(huì)阻塞
- POLLBAND:寫(xiě)優(yōu)先數(shù)據(jù)不會(huì)阻塞
- POLLMSG:有一個(gè)SIGPOLL消息可用
另外踏施,如下事件可能在revents中返回:
- POLLER:給出文件描述符上有錯(cuò)誤
- POLLHUP:文件描述符上有掛起事件
- POLLNVAL:給出的文件描述符非法
POLLIN|POLLPRI等價(jià)于select( )的讀事件石蔗,而POLLOUT|POLLWRBAND等價(jià)于select( )的寫(xiě)事件。POLLIN等價(jià)于POLLRDNORM|POLLRDBAND畅形, 而POLLOUT等價(jià)于POLLWRNORM养距。
- 舉例來(lái)說(shuō),監(jiān)視一個(gè)文件描述符是否可讀寫(xiě)日熬,需設(shè)置events為POLLIN|POLLOUT棍厌。返回時(shí),將在revents中檢查是否有相應(yīng)的標(biāo)志。
- timeout參數(shù)指定在任何I/O就緒前需要等待時(shí)間的長(zhǎng)度耘纱,以毫秒計(jì)敬肚。負(fù)值表示永遠(yuǎn)等待。一個(gè)零值表示調(diào)用立即返回束析,列出所有未準(zhǔn)備好的I/O艳馒,但不等待任何其他事件。這種情況下员寇,poll( )就如圖其名弄慰,輪詢一次后立即返回。
ppoll( ):
Linux提供了一個(gè)poll( )的近似調(diào)用——ppoll( )蝶锋。ppoll( )和pselect( )同源陆爽,然而和pselect( )不同的是,ppoll( )是Linux的專有調(diào)用:
#define _GNU_SOURCE
#include <sys/poll.h>
int ppoll(struct pollfd *fds, nfds_t nfds, const struct timespec *timeout, const sigset_t *sigmask);
像pselect( )那樣扳缕,timeout參數(shù)以秒和納秒計(jì)指定了時(shí)限慌闭,而sigmask參數(shù)提供了一組等待處理的信號(hào)。
對(duì)比poll( )與select( ):
一般來(lái)說(shuō)第献,poll( )系統(tǒng)調(diào)用優(yōu)于select( ):
- poll( )無(wú)需使用者計(jì)算最大的文件描述符值加一和傳遞該參數(shù)
- poll( )應(yīng)對(duì)較大值的fd時(shí)更具效率贡必。
- 使用poll( )可以創(chuàng)建合適大小的數(shù)組,只需要監(jiān)視一項(xiàng)或僅僅傳遞一個(gè)結(jié)構(gòu)體庸毫。
- poll( )系統(tǒng)調(diào)用分離了輸入events字段和輸出revents字段仔拟,數(shù)組無(wú)需改變即而重用。
- select的timeout參數(shù)在返回時(shí)是未定義的飒赃±ǎ可移植的代碼需要重新初始化它。然而pselect沒(méi)有這個(gè)問(wèn)題载佳。