Linux IO模型:阻塞/非阻塞/IO復(fù)用 同步/異步 Select/Epoll/AIO

IO概念

Linux的內(nèi)核將所有外部設(shè)備都可以看做一個(gè)文件來(lái)操作。那么我們對(duì)與外部設(shè)備的操作都可以看做對(duì)文件進(jìn)行操作务蝠。我們對(duì)一個(gè)文件的讀寫(xiě)定欧,都通過(guò)調(diào)用內(nèi)核提供的系統(tǒng)調(diào)用;內(nèi)核給我們返回一個(gè)file descriptor(fd,文件描述符)莹规。而對(duì)一個(gè)socket的讀寫(xiě)也會(huì)有相應(yīng)的描述符赔蒲,稱為socketfd(socket描述符)。描述符就是一個(gè)數(shù)字良漱,指向內(nèi)核中一個(gè)結(jié)構(gòu)體(文件路徑舞虱,數(shù)據(jù)區(qū),等一些屬性)母市。那么我們的應(yīng)用程序?qū)ξ募淖x寫(xiě)就通過(guò)對(duì)描述符的讀寫(xiě)完成矾兜。

linux將內(nèi)存分為內(nèi)核區(qū),用戶區(qū)患久。linux內(nèi)核給我們管理所有的硬件資源椅寺,應(yīng)用程序通過(guò)調(diào)用系統(tǒng)調(diào)用和內(nèi)核交互,達(dá)到使用硬件資源的目的蒋失。應(yīng)用程序通過(guò)系統(tǒng)調(diào)用read發(fā)起一個(gè)讀操作返帕,這時(shí)候內(nèi)核創(chuàng)建一個(gè)文件描述符,并通過(guò)驅(qū)動(dòng)程序向硬件發(fā)送讀指令篙挽,并將讀的的數(shù)據(jù)放在這個(gè)描述符對(duì)應(yīng)結(jié)構(gòu)體的內(nèi)核緩存區(qū)中荆萤,然后再把這個(gè)數(shù)據(jù)讀到用戶進(jìn)程空間中,這樣完成了一次讀操作铣卡;

但是大家都知道I/O設(shè)備相比cpu的速度是極慢的链韭。linux提供的read系統(tǒng)調(diào)用,也是一個(gè)阻塞函數(shù)煮落。這樣我們的應(yīng)用進(jìn)程在發(fā)起read系統(tǒng)調(diào)用時(shí)敞峭,就必須阻塞,就進(jìn)程被掛起而等待文件描述符的讀就緒蝉仇,那么什么是文件描述符讀就緒旋讹,什么是寫(xiě)就緒殖蚕?

讀就緒:就是這個(gè)文件描述符的接收緩沖區(qū)中的數(shù)據(jù)字節(jié)數(shù)大于等于套接字接收緩沖區(qū)低水位標(biāo)記的當(dāng)前大小骗村;

寫(xiě)就緒:該描述符發(fā)送緩沖區(qū)的可用空間字節(jié)數(shù)大于等于描述符發(fā)送緩沖區(qū)低水位標(biāo)記的當(dāng)前大小嫌褪。(如果是socket fd,說(shuō)明上一個(gè)數(shù)據(jù)已經(jīng)發(fā)送完成)胚股。

接收低水位標(biāo)記和發(fā)送低水位標(biāo)記:由應(yīng)用程序指定笼痛,比如應(yīng)用程序指定接收低水位為64個(gè)字節(jié)。那么接收緩沖區(qū)有64個(gè)字節(jié)琅拌,才算fd讀就緒缨伊;

綜上所述,一個(gè)基本的IO进宝,它會(huì)涉及到兩個(gè)系統(tǒng)對(duì)象刻坊,一個(gè)是調(diào)用這個(gè)IO的進(jìn)程對(duì)象,另一個(gè)就是系統(tǒng)內(nèi)核(kernel)党晋。當(dāng)一個(gè)read操作發(fā)生時(shí)谭胚,它會(huì)經(jīng)歷兩個(gè)階段:

  • 通過(guò)read系統(tǒng)調(diào)用想內(nèi)核發(fā)起讀請(qǐng)求。
  • 內(nèi)核向硬件發(fā)送讀指令未玻,并等待讀就緒灾而。
  • 內(nèi)核把將要讀取的數(shù)據(jù)復(fù)制到描述符所指向的內(nèi)核緩存區(qū)中。
  • 將數(shù)據(jù)從內(nèi)核緩存區(qū)拷貝到用戶進(jìn)程空間中扳剿。

IO模型

在linux系統(tǒng)下面旁趟,根據(jù)IO操作的是否被阻塞以及同步異步問(wèn)題進(jìn)行分類,可以得到下面五種IO模型:

1庇绽、阻塞I/O模型
最流行的I/O模型是阻塞I/O模型锡搜,缺省情形下,所有文件操作都是阻塞的瞧掺。我們以套接口為例來(lái)講解此模型耕餐。在進(jìn)程空間中調(diào)用recvfrom,其系統(tǒng)調(diào)用直到數(shù)據(jù)報(bào)到達(dá)且被拷貝到應(yīng)用進(jìn)程的緩沖區(qū)中或者發(fā)生錯(cuò)誤才返回辟狈,期間一直在等待肠缔。我們就說(shuō)進(jìn)程在從調(diào)用recvfrom開(kāi)始到它返回的整段時(shí)間內(nèi)是被阻塞的。

image

2上陕、非阻塞I/O模型
進(jìn)程把一個(gè)套接口設(shè)置成非阻塞是在通知內(nèi)核:當(dāng)所請(qǐng)求的I/O操作不能滿足要求時(shí)候桩砰,不把本進(jìn)程投入睡眠拓春,而是返回一個(gè)錯(cuò)誤释簿。也就是說(shuō)當(dāng)數(shù)據(jù)沒(méi)有到達(dá)時(shí)并不等待,而是以一個(gè)錯(cuò)誤返回硼莽。

image

3庶溶、I/O復(fù)用模型
linux提供select/poll煮纵,進(jìn)程通過(guò)將一個(gè)或多個(gè)fd傳遞給select或poll系統(tǒng)調(diào)用,阻塞在select;這樣select/poll可以幫我們偵測(cè)許多fd是否就緒偏螺。但是select/poll是順序掃描fd是否就緒行疏,而且支持的fd數(shù)量有限。linux還提供了一個(gè)epoll系統(tǒng)調(diào)用套像,epoll是基于事件驅(qū)動(dòng)方式酿联,而不是順序掃描,當(dāng)有fd就緒時(shí),立即回調(diào)函數(shù)rollback夺巩;

image

4贞让、信號(hào)驅(qū)動(dòng)異步I/O模型
首先開(kāi)啟套接口信號(hào)驅(qū)動(dòng)I/O功能, 并通過(guò)系統(tǒng)調(diào)用sigaction安裝一個(gè)信號(hào)處理函數(shù)(此系統(tǒng)調(diào)用立即返回,進(jìn)程繼續(xù)工作柳譬,它是非阻塞的)喳张。當(dāng)數(shù)據(jù)報(bào)準(zhǔn)備好被讀時(shí),就為該進(jìn)程生成一個(gè)SIGIO信號(hào)美澳。隨即可以在信號(hào)處理程序中調(diào)用recvfrom來(lái)讀數(shù)據(jù)報(bào)销部,井通知主循環(huán)數(shù)據(jù)已準(zhǔn)備好被處理中。也可以通知主循環(huán)制跟,讓它來(lái)讀數(shù)據(jù)報(bào)舅桩。

image

5、異步I/O模型
告知內(nèi)核啟動(dòng)某個(gè)操作凫岖,并讓內(nèi)核在整個(gè)操作完成后(包括將數(shù)據(jù)從內(nèi)核拷貝到用戶自己的緩沖區(qū))通知我們江咳。這種模型與信號(hào)驅(qū)動(dòng)模型的主要區(qū)別是:信號(hào)驅(qū)動(dòng)I/O:由內(nèi)核通知我們何時(shí)可以啟動(dòng)一個(gè)I/O操作;異步I/O模型:由內(nèi)核通知我們I/O操作何時(shí)完成哥放。

image

非阻塞IO詳解

通過(guò)上面歼指,我們知道,所有的IO操作在默認(rèn)情況下甥雕,都是屬于阻塞IO踩身。盡管上圖中所示的反復(fù)請(qǐng)求的非阻塞IO的效率底下(需要反復(fù)在用戶空間和進(jìn)程空間切換和判斷,把一個(gè)原本屬于IO密集的操作變?yōu)镮O密集和計(jì)算密集的操作)社露,但是在后面IO復(fù)用中挟阻,需要把IO的操作設(shè)置為非阻塞的,此時(shí)程序?qū)?huì)阻塞在select和poll系統(tǒng)調(diào)用中峭弟。把一個(gè)IO設(shè)置為非阻塞IO有兩種方式:在創(chuàng)建文件描述符時(shí)附鸽,指定該文件描述符的操作為非阻塞;在創(chuàng)建文件描述符以后瞒瘸,調(diào)用fcntl()函數(shù)設(shè)置相應(yīng)的文件描述符為非阻塞坷备。

創(chuàng)建描述符時(shí),利用open函數(shù)和socket函數(shù)的標(biāo)志設(shè)置返回的fd/socket描述符為O_NONBLOCK情臭。

int sd=socket(int domain, int type|O_NONBLOCK, int protocol);
int fd=open(const char *pathname, int flags|O_NONBLOCK);

創(chuàng)建描述符后省撑,通過(guò)調(diào)用fcntl函數(shù)設(shè)置描述符的屬性為O_NONBLOCK

#include <unistd.h>#include <fcntl.h> 
int fcntl(int fd, int cmd, ... /* arg */ ); 
  //例子
  if (fcntl(fd, F_SETFL, fcntl(sockfd, F_GETFL, 0)|O_NONBLOCK) == -1) {       
     return -1;
  } 
  return 0;
}

IO復(fù)用詳解

在IO編程過(guò)程中赌蔑,當(dāng)需要處理多個(gè)請(qǐng)求的時(shí),可以使用多線程和IO復(fù)用的方式進(jìn)行處理竟秫。上面的圖介紹了整個(gè)IO復(fù)用的過(guò)程娃惯,它通過(guò)把多個(gè)IO的阻塞復(fù)用到一個(gè)select之類的阻塞上,從而使得系統(tǒng)在單線程的情況下同時(shí)支持處理多個(gè)請(qǐng)求肥败。和多線程/進(jìn)程比較趾浅,I/O多路復(fù)用的最大優(yōu)勢(shì)是系統(tǒng)開(kāi)銷小,系統(tǒng)不需要建立新的進(jìn)程或者線程馒稍,也不必維護(hù)這些線程和進(jìn)程潮孽。IO復(fù)用常見(jiàn)的應(yīng)用場(chǎng)景:

  1. 客戶程序需要同時(shí)處理交互式的輸入和服務(wù)器之間的網(wǎng)絡(luò)連接。
  2. 客戶端需要對(duì)多個(gè)網(wǎng)絡(luò)連接作出反應(yīng)筷黔。
  3. 服務(wù)器需要同時(shí)處理多個(gè)處于監(jiān)聽(tīng)狀態(tài)和多個(gè)連接狀態(tài)的套接字
  4. 服務(wù)器需要處理多種網(wǎng)絡(luò)協(xié)議的套接字往史。

目前支持I/O復(fù)用的系統(tǒng)調(diào)用有select、pselect佛舱、poll椎例、epoll,下面幾小結(jié)分別來(lái)學(xué)習(xí)一下select和epoll的使用请祖。
Linux 2.6之前是select订歪、poll增淹,2.6之后是epoll衔掸,Windows是IOCP祝闻。

select

#include <sys/select.h> 
int select(int maxfdps, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout); 
struct timeval{
  long tv_sec; //秒
  long tv_usec; //微秒
};
void FD_CLR(int fd, fd_set *set); //將一個(gè)給定的文件描述符從集合中刪除
int  FD_ISSET(int fd, fd_set *set);  // 檢查集合中指定的文件描述符是否可以讀寫(xiě) ?
void FD_SET(int fd, fd_set *set); //將一個(gè)給定的文件描述符加入集合之中
void FD_ZERO(fd_set *set);//清空集合           返回:就緒描述字的正數(shù)目声滥,0-超時(shí),-1-出錯(cuò)

struct timeval *time結(jié)構(gòu)體告知內(nèi)核等待所指定描述字中的任何一個(gè)就緒可花多少時(shí)間讥蟆。參數(shù)取值:
(1)(struct timeval *)0:永遠(yuǎn)等待下去马胧,僅在有一個(gè)描述字準(zhǔn)備好I/O時(shí)才返回僵朗。
(2)struct timeval *time:在有一個(gè)描述字準(zhǔn)備好I/O時(shí)返回席纽,但是不超過(guò)由該參數(shù)所指向的timeval結(jié)構(gòu)中指定的秒數(shù)和微秒數(shù)捏悬。如果超過(guò)時(shí)間,沒(méi)有描述字準(zhǔn)備好润梯,那就返回0过牙。如果秒=微秒=0,檢查描述字后立即返回纺铭,此時(shí)相當(dāng)于輪詢寇钉。
中間的三個(gè)參數(shù)readset、writeset和exceptset指定我們要讓內(nèi)核測(cè)試讀舶赔、寫(xiě)和異常條件的描述字扫倡。如果我們對(duì)某一個(gè)的條件不感興趣,就可以把它設(shè)為空指針顿痪。fd_set可以理解為一個(gè)集合镊辕,這個(gè)集合中存放的是文件描述符,可通過(guò)上面的四個(gè)宏進(jìn)行設(shè)置(注意蚁袭,fd_set不是struct fd_set征懈。。揩悄。 剛剛開(kāi)始調(diào)試程序就犯錯(cuò)誤卖哎,返回storage size of 'fds' isn't known)。
第一個(gè)參數(shù)maxfdp1指定待測(cè)試的描述字個(gè)數(shù)删性,它的值是待測(cè)試的最大描述字加1(因此我們把該參數(shù)命名為maxfdp1)亏娜,描述字0、1蹬挺、2...maxfdp1-1均將被測(cè)試维贺。
下面是一個(gè)從網(wǎng)上看到的一個(gè)比較好的測(cè)試程序,太長(zhǎng)了巴帮,移到空間代碼功能里面了溯泣,click me to see

pselect

#include <sys/select.h> 
int pselect(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, const struct timespec *timeout,   const sigset_t *sigmask);
struct timespec{    
  time_t tv_sec; //秒    
  long tv_nsec; //納秒
};

比較select和pselect函數(shù),我們發(fā)現(xiàn)在原型上面有兩個(gè)不同:

1榕茧、pselect使用timespec結(jié)構(gòu)垃沦,而不使用timeval結(jié)構(gòu)。timespec結(jié)構(gòu)是POSIX的又一個(gè)發(fā)明用押。 這兩個(gè)結(jié)構(gòu)的區(qū)別在于第二個(gè)成員:新結(jié)構(gòu)的該成員tv_nsec指定納秒數(shù)肢簿,而舊結(jié)構(gòu)的該成員tv_usec指定微秒數(shù)。
2蜻拨、pselect函數(shù)增加了第六個(gè)參數(shù):一個(gè)指向信號(hào)掩碼的指針池充。該參數(shù)允許程序先禁止遞交某些信號(hào),再測(cè)試由這些當(dāng)前被禁止的信號(hào)處理函數(shù)設(shè)置的全局變量缎讼,然后調(diào)用pselect纵菌,告訴它重新設(shè)置信號(hào)掩碼。

首先我們看看信號(hào)掩碼的概念:在POSIX下休涤,每個(gè)進(jìn)程有一個(gè)信號(hào)掩碼(signal mask)咱圆。簡(jiǎn)單地說(shuō),信號(hào)掩碼是一個(gè)“位圖”功氨,其中每一位都對(duì)應(yīng)著一種信號(hào)序苏。如果位圖中的某一位為1,就表示在執(zhí)行當(dāng)前信號(hào)的處理程序期間相應(yīng)的信號(hào)暫時(shí)被“屏蔽”捷凄,使得在執(zhí)行的過(guò)程中不會(huì)嵌套地響應(yīng)那種信號(hào)忱详。為什么對(duì)某一信號(hào)進(jìn)行屏蔽呢?我們來(lái)看一下對(duì)CTRL_C的處理跺涤。大家知道匈睁,當(dāng)一個(gè)程序正在運(yùn)行時(shí)监透,在鍵盤(pán)上按一下CTRL_C,內(nèi)核就會(huì)向相應(yīng)的進(jìn)程發(fā)出一個(gè)SIGINT 信號(hào)航唆,而對(duì)這個(gè)信號(hào)的默認(rèn)操作就是通過(guò)do_exit()結(jié)束該進(jìn)程的運(yùn)行胀蛮。但是,有些應(yīng)用程序可能對(duì)CTRL_C有自己的處理糯钙,所以就要為SIGINT另行設(shè)置一個(gè)處理程序粪狼,使它指向應(yīng)用程序中的一個(gè)函數(shù),在那個(gè)函數(shù)中對(duì)CTRL_C這個(gè)事件作出響應(yīng)任岸。但是再榄,在實(shí)踐中卻發(fā)現(xiàn),兩次CTRL_C事件往往過(guò)于密集享潜,有時(shí)候剛剛進(jìn)入第一個(gè)信號(hào)的處理程序困鸥,第二個(gè)SIGINT信號(hào)就到達(dá)了,而第二個(gè)信號(hào)的默認(rèn)操作是殺死進(jìn)程剑按,這樣窝革,第一個(gè)信號(hào)的處理程序根本沒(méi)有執(zhí)行完。為了避免這種情況的出現(xiàn)吕座,就在執(zhí)行一個(gè)信號(hào)處理程序的過(guò)程中將該種信號(hào)自動(dòng)屏蔽掉虐译。所謂“屏蔽”,與將信號(hào)忽略是不同的吴趴,它只是將信號(hào)暫時(shí)“遮蓋”一下漆诽,一旦屏蔽去掉,已到達(dá)的信號(hào)又繼續(xù)得到處理锣枝。有關(guān)信號(hào)相關(guān)的知識(shí)參考我另外一個(gè)文章:《linux基礎(chǔ)編程:進(jìn)程通信之信號(hào)》厢拭。在我們開(kāi)發(fā)過(guò)程中,如果進(jìn)程阻塞與select函數(shù)撇叁,此時(shí)該阻塞被信號(hào)所打斷供鸠,select返回-1,errno=EINTR的錯(cuò)誤陨闹。由于這個(gè)原因楞捂,我們才有了pselect函數(shù)在信號(hào)上的優(yōu)化處理。因此在信號(hào)和select都被使用的系統(tǒng)里趋厉,pselect有其作用寨闹。否則和select沒(méi)有任何區(qū)別。

poll

#include <poll.h> 
int poll(struct pollfd *fds, nfds_t nfds, int timeout); 
struct pollfd { 
  int fd; /* file descriptor */ 
  short events; /* requested events to watch */ 
  short revents; /* returned events witnessed */    
}; 

和select()不一樣君账,poll()沒(méi)有使用低效的三個(gè)基于位的文件描述符set繁堡,而是采用了一個(gè)單獨(dú)的結(jié)構(gòu)體pollfd數(shù)組,由fds指針指向這個(gè)數(shù)組的第一個(gè)元素,其中nfds表示該數(shù)組的大小椭蹄。每一個(gè)pollfd 結(jié)構(gòu)體指定了一個(gè)被監(jiān)視的文件描述符闻牡,每個(gè)結(jié)構(gòu)體的events域是監(jiān)視該文件描述符的事件掩碼,由用戶來(lái)設(shè)置這個(gè)域绳矩。revents域是文件描述符的操作結(jié)果事件掩碼罩润。內(nèi)核在調(diào)用返回時(shí)設(shè)置這個(gè)域。events域中請(qǐng)求的任何事件都可能在revents域中返回埋酬,而部分事件不能出現(xiàn)在events中,詳細(xì)如下表烧栋。

| 常量 | 說(shuō)明 |
| POLLIN | 普通或優(yōu)先級(jí)帶數(shù)據(jù)可讀 |
| POLLRDNORM | 普通數(shù)據(jù)可讀 |
| POLLRDBAND | 優(yōu)先級(jí)帶數(shù)據(jù)可讀 |
| POLLPRI | 高優(yōu)先級(jí)數(shù)據(jù)可讀 |
| POLLOUT | 普通數(shù)據(jù)可寫(xiě) |
| POLLWRNORM | 普通數(shù)據(jù)可寫(xiě) |
| POLLWRBAND | 優(yōu)先級(jí)帶數(shù)據(jù)可寫(xiě) |
| POLLERR | 發(fā)生錯(cuò)誤 |
| POLLHUP | 發(fā)生掛起 |
| POLLNVAL | 描述字不是一個(gè)打開(kāi)的文件 |

注意:后三個(gè)只能作為描述字的返回結(jié)果存儲(chǔ)在revents中写妥,而不能作為測(cè)試條件用于events中。

最后一個(gè)參數(shù)timeout是指定poll函數(shù)返回前等待多長(zhǎng)時(shí)間审姓。它的取值如下:

| timeout值 | 說(shuō)明 |
| INFTIM | 永遠(yuǎn)等待 |
| 0 | 立即返回珍特,不阻塞進(jìn)程 |
| >0 | 等待指定數(shù)目的毫秒數(shù) |

成功時(shí),poll()返回結(jié)構(gòu)體中revents域不為0的文件描述符個(gè)數(shù)魔吐;如果在超時(shí)前沒(méi)有任何事件發(fā)生扎筒,poll()返回0;失敗時(shí)酬姆,poll()返回-1嗜桌,并設(shè)置errno為下列值之一:
EBADF:一個(gè)或多個(gè)結(jié)構(gòu)體中指定的文件描述符無(wú)效。EFAULT:fds指針指向的地址超出進(jìn)程的地址空間辞色。EINTR:請(qǐng)求的事件之前產(chǎn)生一個(gè)信號(hào)骨宠,調(diào)用可以重新發(fā)起。EINVAL:nfds參數(shù)超出PLIMIT_NOFILE值相满。ENOMEM:可用內(nèi)存不足层亿,無(wú)法完成請(qǐng)求。

關(guān)鍵代碼如下:

client[0].fd = listenfd;         /*將數(shù)組中的第一個(gè)元素設(shè)置成監(jiān)聽(tīng)描述字*/    
client[0].events = POLLIN;       
while(1)    {        
  nready = poll(client, maxi+1,INFTIM); 
  //將進(jìn)程阻塞在poll上       
  if( client[0].revents & POLLIN/*POLLRDNORM*/ )
  /*先測(cè)試監(jiān)聽(tīng)描述字*/

epoll

在linux的網(wǎng)絡(luò)編程中立美,很長(zhǎng)的一段時(shí)間都在使用select來(lái)做事件觸發(fā)匿又。然而select逐漸暴露出了一些缺陷,使得linux不得不在新的內(nèi)核中尋找出替代方案建蹄,那就是epoll碌更。其實(shí),epoll與select原理類似洞慎,只不過(guò)针贬,epoll作出了一些重大改進(jìn),即:
1.支持一個(gè)進(jìn)程打開(kāi)大數(shù)目的socket描述符(FD)
select 最不能忍受的是一個(gè)進(jìn)程所打開(kāi)的FD是有一定限制的拢蛋,由FD_SETSIZE設(shè)置桦他,默認(rèn)值是2048。對(duì)于那些需要支持的上萬(wàn)連接數(shù)目的IM服務(wù)器來(lái)說(shuō)顯然太少了。這時(shí)候你一是可以選擇修改這個(gè)宏然后重新編譯內(nèi)核快压,不過(guò)資料也同時(shí)指出這樣會(huì)帶來(lái)網(wǎng)絡(luò)效率的下降圆仔,二是可以選擇多進(jìn)程的解決方案(傳統(tǒng)的 Apache方案),不過(guò)雖然linux上面創(chuàng)建進(jìn)程的代價(jià)比較小蔫劣,但仍舊是不可忽視的坪郭,加上進(jìn)程間數(shù)據(jù)同步遠(yuǎn)比不上線程間同步的高效,所以也不是一種完美的方案脉幢。不過(guò) epoll則沒(méi)有這個(gè)限制歪沃,它所支持的FD上限是最大可以打開(kāi)文件的數(shù)目,這個(gè)數(shù)字一般遠(yuǎn)大于2048,舉個(gè)例子,在1GB內(nèi)存的機(jī)器上大約是10萬(wàn)左右嫌松,具體數(shù)目可以cat /proc/sys/fs/file-max察看,一般來(lái)說(shuō)這個(gè)數(shù)目和系統(tǒng)內(nèi)存關(guān)系很大沪曙。
2.IO效率不隨FD數(shù)目增加而線性下降
傳統(tǒng)的select/poll另一個(gè)致命弱點(diǎn)就是當(dāng)你擁有一個(gè)很大的socket集合,不過(guò)由于網(wǎng)絡(luò)延時(shí)萎羔,任一時(shí)間只有部分的socket是"活躍"的液走,但是select/poll每次調(diào)用都會(huì)線性掃描全部的集合,導(dǎo)致效率呈現(xiàn)線性下降贾陷。但是epoll不存在這個(gè)問(wèn)題缘眶,它只會(huì)對(duì)"活躍"的socket進(jìn)行操作---這是因?yàn)樵趦?nèi)核實(shí)現(xiàn)中epoll是根據(jù)每個(gè)fd上面的callback函數(shù)實(shí)現(xiàn)的。那么髓废,只有"活躍"的socket才會(huì)主動(dòng)的去調(diào)用 callback函數(shù)巷懈,其他idle狀態(tài)socket則不會(huì),在這點(diǎn)上慌洪,epoll實(shí)現(xiàn)了一個(gè)"偽"AIO砸喻,因?yàn)檫@時(shí)候推動(dòng)力在os內(nèi)核。在一些 benchmark中蒋譬,如果所有的socket基本上都是活躍的---比如一個(gè)高速LAN環(huán)境割岛,epoll并不比select/poll有什么效率,相反犯助,如果過(guò)多使用epoll_ctl,效率相比還有稍微的下降癣漆。但是一旦使用idle connections模擬WAN環(huán)境,epoll的效率就遠(yuǎn)在select/poll之上了。
3.使用mmap加速內(nèi)核與用戶空間的消息傳遞剂买。
這點(diǎn)實(shí)際上涉及到epoll的具體實(shí)現(xiàn)了惠爽。無(wú)論是select,poll還是epoll都需要內(nèi)核把FD消息通知給用戶空間,如何避免不必要的內(nèi)存拷貝就很重要瞬哼,在這點(diǎn)上婚肆,epoll是通過(guò)內(nèi)核于用戶空間mmap同一塊內(nèi)存實(shí)現(xiàn)的。
4.內(nèi)核微調(diào)
這一點(diǎn)其實(shí)不算epoll的優(yōu)點(diǎn)了坐慰,而是整個(gè)linux平臺(tái)的優(yōu)點(diǎn)较性。也許你可以懷疑linux平臺(tái),但是你無(wú)法回避linux平臺(tái)賦予你微調(diào)內(nèi)核的能力。比如赞咙,內(nèi)核TCP/IP協(xié)議棧使用內(nèi)存池管理sk_buff結(jié)構(gòu)责循,那么可以在運(yùn)行時(shí)期動(dòng)態(tài)調(diào)整這個(gè)內(nèi)存pool(skb_head_pool)的大小 --- 通過(guò)echo XXXX>/proc/sys/net/core/hot_list_length完成。再比如listen函數(shù)的第2個(gè)參數(shù)(TCP完成3次握手的數(shù)據(jù)包隊(duì)列長(zhǎng)度)攀操,也可以根據(jù)你平臺(tái)內(nèi)存大小動(dòng)態(tài)調(diào)整院仿。更甚至在一個(gè)數(shù)據(jù)包面數(shù)目巨大但同時(shí)每個(gè)數(shù)據(jù)包本身大小卻很小的特殊系統(tǒng)上嘗試最新的NAPI網(wǎng)卡驅(qū)動(dòng)架構(gòu)。

epoll api函數(shù)比較簡(jiǎn)單速和,包括創(chuàng)建一個(gè)epoll描述符歹垫,添加監(jiān)聽(tīng)事件,阻塞等待所監(jiān)聽(tīng)的事件發(fā)生,關(guān)閉epoll描述符颠放,如下:

#include <sys/epoll.h>
#include <unistd.h> 
int epoll_create(int size); //epoll描述符
int close(int fd);//關(guān)閉epoll描述符

創(chuàng)建一個(gè)epoll的句柄排惨,size用來(lái)告訴內(nèi)核這個(gè)監(jiān)聽(tīng)的數(shù)目一共有多大。需要注意的是慈迈,當(dāng)創(chuàng)建好epoll句柄后若贮,epoll本身就占用一個(gè)fd值省有,所以用完后必須調(diào)用close()關(guān)閉痒留,以防止fd被耗盡。

#include <sys/epoll.h>
#include <unistd.h> 
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);//添加監(jiān)聽(tīng)事件 typedef union epoll_data {    
  void        *ptr;    
  int          fd;    
  uint32_t     u32;    
  uint64_t     u64;
} 
epoll_data_t; 
struct epoll_event {    
  uint32_t     events;      /* Epoll events */    
  epoll_data_t data;        /* User data variable */
};

epoll_ctl為事件注冊(cè)函數(shù)蠢沿,第一個(gè)參數(shù)是epoll_create()的返回值伸头,第二個(gè)參數(shù)表示動(dòng)作,用三個(gè)宏來(lái)表示:

  • EPOLL_CTL_ADD:注冊(cè)新的fd到epfd中舷蟀;
  • EPOLL_CTL_MOD:修改已經(jīng)注冊(cè)的fd的監(jiān)聽(tīng)事件恤磷;
  • EPOLL_CTL_DEL:從epfd中刪除一個(gè)fd;

第三個(gè)參數(shù)是需要監(jiān)聽(tīng)的描述符fd,第四個(gè)參數(shù)是告訴內(nèi)核需要監(jiān)聽(tīng)什么事件,struct epoll_event結(jié)構(gòu)如上所示野宜,其中events為需要注冊(cè)的事件扫步,可以為下面幾個(gè)宏的集合:

  • EPOLLIN:表示對(duì)應(yīng)的文件描述符可以讀(包括對(duì)端SOCKET正常關(guān)閉);
  • EPOLLOUT:表示對(duì)應(yīng)的文件描述符可以寫(xiě)匈子;
  • EPOLLPRI:表示對(duì)應(yīng)的文件描述符有緊急的數(shù)據(jù)可讀(這里應(yīng)該表示有帶外數(shù)據(jù)到來(lái))河胎;
  • EPOLLERR:表示對(duì)應(yīng)的文件描述符發(fā)生錯(cuò)誤;
  • EPOLLHUP:表示對(duì)應(yīng)的文件描述符被掛斷虎敦;
  • EPOLLET:將EPOLL設(shè)為邊緣觸發(fā)(Edge Triggered)模式游岳,這是相對(duì)于水平觸發(fā)(Level Triggered)來(lái)說(shuō)的。詳細(xì)見(jiàn)下面描述
  • EPOLLONESHOT:只監(jiān)聽(tīng)一次事件其徙,當(dāng)監(jiān)聽(tīng)完這次事件之后胚迫,如果還需要繼續(xù)監(jiān)聽(tīng)這個(gè)socket的話,需要再次把這個(gè)socket加入到EPOLL隊(duì)列里唾那。

操作系統(tǒng)對(duì)被注冊(cè)的文件描述符的事件發(fā)送以后有兩種處理方式访锻,分別有LT和ET模式,默認(rèn)情況下為L(zhǎng)T,如果需要設(shè)置為ET模式朗若,設(shè)置struct epoll_event.events|EPOLLET恼五。

  • LT(level triggered)是缺省的工作方式,并且同時(shí)支持block和no-block socket.在這種做法中哭懈,內(nèi)核告訴你一個(gè)文件描述符是否就緒了灾馒,然后你可以對(duì)這個(gè)就緒的fd進(jìn)行IO操作。如果你不作任何操作遣总,內(nèi)核還是會(huì)繼續(xù)通知你的睬罗,所以,這種模式編程出錯(cuò)誤可能性要小一點(diǎn)旭斥,傳統(tǒng)的select/poll都是這種模型的代表容达。
  • ET (edge-triggered)是高速工作方式,只支持no-block socket垂券。在這種模式下花盐,當(dāng)描述符從未就緒變?yōu)榫途w時(shí),內(nèi)核通過(guò)epoll告訴你菇爪。然后它會(huì)假設(shè)你知道文件描述符已經(jīng)就緒,并且不會(huì)再為那個(gè)文件描述符發(fā)送更多的就緒通知凳宙,直到你做了某些操作導(dǎo)致那個(gè)文件描述符不再為就緒狀態(tài)了(比如熙揍,你在發(fā)送,接收或者接收請(qǐng)求氏涩,或者發(fā)送接收的數(shù)據(jù)少于一定量時(shí)導(dǎo)致了一個(gè)EWOULDBLOCK 錯(cuò)誤)届囚。但是請(qǐng)注意,如果一直不對(duì)這個(gè)fd作IO操作(從而導(dǎo)致它再次變成未就緒)是尖,內(nèi)核不會(huì)發(fā)送更多的通知(only once),不過(guò)在TCP協(xié)議中意系,ET模式的加速效用仍需要更多的benchmark確認(rèn)。

struct epoll_event.data為一個(gè)epoll_data_t類型的聯(lián)合體饺汹,詳細(xì)結(jié)構(gòu)如上蛔添,其中可以保存指針,描述符首繁,32/64位的整數(shù)作郭。如果在監(jiān)聽(tīng)過(guò)程中,該描述符上面有相應(yīng)的事件發(fā)生弦疮,系統(tǒng)將會(huì)把該字段返回夹攒。先看監(jiān)聽(tīng)函數(shù)吧⌒踩看完就知道

#include <sys/epoll.h>
#include <unistd.h> 
int epoll_wait(int epfd, struct epoll_event *events,  int maxevents, int timeout);//阻塞等待所監(jiān)聽(tīng)的事件發(fā)生

阻塞監(jiān)聽(tīng)函數(shù)咏尝,類似于select()調(diào)用压语。參數(shù)events用來(lái)從內(nèi)核得到事件的集合,返回的結(jié)構(gòu)也是struct epoll_event编检,其中event為相應(yīng)的事件胎食,data為注冊(cè)時(shí),設(shè)置的值(常見(jiàn)情況允懂,data設(shè)置為注冊(cè)的描述符厕怜,這樣就可以對(duì)相應(yīng)的描述符進(jìn)行IO操作)。maxevents告之內(nèi)核這個(gè)events有多大蕾总,這個(gè)maxevents的值不能大于創(chuàng)建epoll_create()時(shí)的size粥航,參數(shù)timeout是超時(shí)時(shí)間(毫秒,0會(huì)立即返回生百,-1將不確定递雀,也有說(shuō)法說(shuō)是永久阻塞)。該函數(shù)返回需要處理的事件數(shù)目蚀浆,如返回0表示已超時(shí)缀程。

struct epoll_event ev, *events; int kdpfd = epoll_create(100); 
ev.events = EPOLLIN | EPOLLET; 
// 注意這個(gè)EPOLLET,指定了邊緣觸發(fā) 
ev.data.fd =listener; 
epoll_ctl(kdpfd, EPOLL_CTL_ADD, listener, &ev); 
for(;;) {   
nfds = epoll_wait(kdpfd, events, maxevents, -1);    
for(n = 0; n < nfds; ++n)    {      
if(events[n].data.fd == listener)       {                   
client = accept(listener, (struct sockaddr *) &local, &addrlen);                    if(client < 0){                         
perror("accept");                           
continue;                   }                   
setnonblocking(client);                  
ev.events = EPOLLIN | EPOLLET;                   
ev.data.fd = client;                    
if (epoll_ctl(kdpfd, EPOLL_CTL_ADD, client, &ev) < 0)           {                           fprintf(stderr, "epoll set insertion error: fd=%d0, client);                            return -1;                  
}  }else{           
do_use_fd(events[n].data.fd);       
}                        
} }

最后我們說(shuō)一下市俊,用來(lái)克服select/poll缺點(diǎn)的方法不只有epoll杨凑。epoll只是一種linux 的方案,在freeBSD下有kqueue秕衙,而dev/poll 是最古老的Solaris的方案蠢甲,使用難度依次遞增僵刮。 kqueue 是freebsd 的寵兒据忘,kqueue 實(shí)際上是一個(gè)功能相當(dāng)豐富的kernel 事件隊(duì)列,它不僅僅是select/poll 的升級(jí)搞糕,而且可以處理 signal 勇吊、目錄結(jié)構(gòu)變化、進(jìn)程等多種事件窍仰。Kqueue 是邊緣觸發(fā)的汉规。/dev/poll 是Solaris的產(chǎn)物,是這一系列高性能API 中最早出現(xiàn)的驹吮。Kernel 提供一個(gè)特殊的設(shè)備文件/dev/poll 针史。應(yīng)用程序打開(kāi)這個(gè)文件得到操縱fd_set 的句柄,通過(guò)寫(xiě)入pollfd 來(lái)修改它碟狞,一個(gè)特殊ioctl 調(diào)用用來(lái)替換select 啄枕。由于出現(xiàn)的年代比較早,所以/dev/poll 的接口現(xiàn)在看上去比較笨拙可笑族沃。

信號(hào)驅(qū)動(dòng)異步IO詳解

上面介紹了不管是阻塞/非阻塞或者IO復(fù)用频祝,在應(yīng)用程序?qū)用娑夹枰M(jìn)行阻塞輪詢指定的IO上面是否有相應(yīng)的事件發(fā)生。本節(jié)我們將將介紹一個(gè)全新的模型:異步模型常空,異步則意味著不需要用戶層進(jìn)行輪詢沽一,在特定IO或者事件發(fā)生時(shí)候,會(huì)主動(dòng)通知應(yīng)用程序IO就緒漓糙,本節(jié)介紹的通知機(jī)制即信號(hào)铣缠,我們把這個(gè)模型叫著信號(hào)驅(qū)動(dòng)的異步I/O。在《linux基礎(chǔ)編程:進(jìn)程通信之信號(hào)》文章中昆禽,我們?cè)敿?xì)介紹信號(hào)操作攘残。

Unix上有定義了許多信號(hào),源自Berkeley的實(shí)現(xiàn)使用的是SIGIO信號(hào)來(lái)支持套接字和終端設(shè)備上的信號(hào)驅(qū)動(dòng)IO为狸。在套接字IO中歼郭,信號(hào)驅(qū)動(dòng)IO模型主要是在UDP套接字上使用,在TCP套接字上幾乎是沒(méi)有什么使用的(在TCP上辐棒,由于TCP是雙工的病曾,它的信號(hào)產(chǎn)生過(guò)于平凡,并且信號(hào)的出現(xiàn)幾乎沒(méi)有告訴我們發(fā)生了什么事情漾根。因此對(duì)于TCP套接字泰涂,SIGIO信號(hào)是沒(méi)有什么使用的)。

使用信號(hào)驅(qū)動(dòng)異步IO主要有下面幾個(gè)步驟:

為了讓套接字描述符可以工作于信號(hào)驅(qū)動(dòng)I/O模式辐怕,應(yīng)用進(jìn)程必須完成如下三步設(shè)置:

  • 1.注冊(cè)SIGIO信號(hào)處理程序逼蒙。(安裝信號(hào)處理器)
  • 2.使用fcntl的F_SETOWN命令,設(shè)置套接字所有者寄疏。
  • 3.使用fcntl的F_SETFL命令是牢,置O_ASYNC和O_NONBLOCK標(biāo)志,允許套接字信號(hào)驅(qū)動(dòng)I/O陕截。

注意驳棱,必須保證在設(shè)置套接字所有者之前,向系統(tǒng)注冊(cè)信號(hào)處理程序农曲,否則就有可能在fcntl調(diào)用后社搅,信號(hào)處理程序注冊(cè)前內(nèi)核向應(yīng)用交付SIGIO信號(hào)姨谷,導(dǎo)致應(yīng)用丟失此信號(hào)捂蕴。

代碼片段如下:

struct sigaction sigio_action;
memset(&sigio_action, 0, sizeof(sigio_action));
sigio_action.sa_flags = 0;sigio_action.sa_handler = do_sigio;
//信號(hào)發(fā)生時(shí)的處理函數(shù)sigaction(SIGIO, &sigio_action, NULL); 
fcntl(listenfd1, F_SETOWN, getpid());
int flags;flags = fcntl(listenfd1, F_GETFL, 0);
flags |= O_ASYNC | O_NONBLOCK;fcntl(listenfd1, F_SETFL, flags);

異步IO詳解

Linux 2.6內(nèi)核包含對(duì)一套新的異步IO,即AIO 的支持為用戶空間提供統(tǒng)一的異步I/O 接口码荔。目前好像只支持本地IO暮的。等有時(shí)間再去學(xué)習(xí)吧笙以。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市青扔,隨后出現(xiàn)的幾起案子源织,更是在濱河造成了極大的恐慌翩伪,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,378評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件谈息,死亡現(xiàn)場(chǎng)離奇詭異缘屹,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)侠仇,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門轻姿,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人逻炊,你說(shuō)我怎么就攤上這事互亮。” “怎么了余素?”我有些...
    開(kāi)封第一講書(shū)人閱讀 152,702評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵豹休,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我桨吊,道長(zhǎng)威根,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,259評(píng)論 1 279
  • 正文 為了忘掉前任视乐,我火速辦了婚禮洛搀,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘佑淀。我一直安慰自己留美,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,263評(píng)論 5 371
  • 文/花漫 我一把揭開(kāi)白布伸刃。 她就那樣靜靜地躺著谎砾,像睡著了一般。 火紅的嫁衣襯著肌膚如雪奕枝。 梳的紋絲不亂的頭發(fā)上棺榔,一...
    開(kāi)封第一講書(shū)人閱讀 49,036評(píng)論 1 285
  • 那天瓶堕,我揣著相機(jī)與錄音隘道,去河邊找鬼。 笑死郎笆,一個(gè)胖子當(dāng)著我的面吹牛谭梗,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播宛蚓,決...
    沈念sama閱讀 38,349評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼激捏,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了凄吏?” 一聲冷哼從身側(cè)響起远舅,我...
    開(kāi)封第一講書(shū)人閱讀 36,979評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤闰蛔,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后图柏,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體序六,經(jīng)...
    沈念sama閱讀 43,469評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,938評(píng)論 2 323
  • 正文 我和宋清朗相戀三年蚤吹,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了例诀。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,059評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡裁着,死狀恐怖繁涂,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情二驰,我是刑警寧澤扔罪,帶...
    沈念sama閱讀 33,703評(píng)論 4 323
  • 正文 年R本政府宣布,位于F島的核電站桶雀,受9級(jí)特大地震影響步势,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜背犯,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,257評(píng)論 3 307
  • 文/蒙蒙 一坏瘩、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧漠魏,春花似錦倔矾、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,262評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至禁熏,卻和暖如春壤巷,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背瞧毙。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,485評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工胧华, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人宙彪。 一個(gè)月前我還...
    沈念sama閱讀 45,501評(píng)論 2 354
  • 正文 我出身青樓矩动,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親释漆。 傳聞我的和親對(duì)象是個(gè)殘疾皇子悲没,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,792評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容