本文摘抄自linux基礎(chǔ)編程
IO概念
Linux的內(nèi)核將所有外部設(shè)備都可以看做一個(gè)文件來操作直砂。那么我們對(duì)與外部設(shè)備的操作都可以看做對(duì)文件進(jìn)行操作。
我們對(duì)一個(gè)文件的讀寫唯袄,都通過調(diào)用內(nèi)核提供的系統(tǒng)調(diào)用诗茎;內(nèi)核給我們返回一個(gè)filede scriptor(fd,文件描述符)。而對(duì)一個(gè)socket的讀寫也會(huì)有相應(yīng)的描述符献汗,稱為socketfd(socket描述符)敢订。描述符就是一個(gè)數(shù)字,指向內(nèi)核中一個(gè)結(jié)構(gòu)體(文件路徑罢吃,數(shù)據(jù)區(qū)楚午,等一些屬性)。那么我們的應(yīng)用程序?qū)ξ募淖x寫就通過對(duì)描述符的讀寫完成尿招。
linux將內(nèi)存分為內(nèi)核區(qū)矾柜,用戶區(qū)。linux內(nèi)核給我們管理所有的硬件資源就谜,應(yīng)用程序通過調(diào)用系統(tǒng)調(diào)用和內(nèi)核交互怪蔑,達(dá)到使用硬件資源的目的。應(yīng)用程序通過系統(tǒng)調(diào)用read發(fā)起一個(gè)讀操作丧荐,這時(shí)候內(nèi)核創(chuàng)建一個(gè)文件描述符缆瓣,并通過驅(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)程被掛起而等待文件描述符的讀就緒,那么什么是文件描述符讀就緒,什么是寫就緒呼奢?
- 讀就緒:就是這個(gè)文件描述符的接收緩沖區(qū)中的數(shù)據(jù)字節(jié)數(shù)大于等于套接字接收緩沖區(qū)低水位標(biāo)記的當(dāng)前大幸巳浮;
- 寫就緒:該描述符發(fā)送緩沖區(qū)的可用空間字節(jié)數(shù)大于等于描述符發(fā)送緩沖區(qū)低水位標(biāo)記的當(dāng)前大小握础。(如果是socket fd辐董,說明上一個(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è)階段:
1:通過read系統(tǒng)調(diào)用想內(nèi)核發(fā)起讀請(qǐng)求型将。內(nèi)核向硬件發(fā)送讀指令,并等待讀就緒荐虐。
2:內(nèi)核把將要讀取的數(shù)據(jù)復(fù)制到描述符所指向的內(nèi)核緩存區(qū)中七兜。將數(shù)據(jù)從內(nèi)核緩存區(qū)拷貝到用戶進(jìn)程空間中。
IO模型
在linux系統(tǒng)下面福扬,根據(jù)IO操作的是否被阻塞以及同步異步問題進(jìn)行分類腕铸,可以得到下面五種IO模型:
1、阻塞I/O模型
最流行的I/O模型是阻塞I/O模型铛碑,缺省情形下狠裹,所有文件操作都是阻塞的。我們以套接口為例來講解此模型亚茬。在進(jìn)程空間中調(diào)用recvfrom酪耳,其系統(tǒng)調(diào)用直到數(shù)據(jù)報(bào)到達(dá)且被拷貝到應(yīng)用進(jìn)程的緩沖區(qū)中或者發(fā)生錯(cuò)誤才返回,期間一直在等待刹缝。我們就說進(jìn)程在從調(diào)用recvfrom開始到它返回的整段時(shí)間內(nèi)是被阻塞的碗暗。
2、非阻塞I/O模型
進(jìn)程把一個(gè)套接口設(shè)置成非阻塞是在通知內(nèi)核:當(dāng)所請(qǐng)求的I/O操作不能滿足要求時(shí)候梢夯,不把本進(jìn)程投入睡眠言疗,而是返回一個(gè)錯(cuò)誤。也就是說當(dāng)數(shù)據(jù)沒有到達(dá)時(shí)并不等待颂砸,而是以一個(gè)錯(cuò)誤返回噪奄。
3死姚、I/O復(fù)用模型
linux提供select/poll,進(jìn)程通過將一個(gè)或多個(gè)fd傳遞給select或poll系統(tǒng)調(diào)用勤篮,阻塞在select;這樣select/poll可以幫我們偵測許多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瀑焦;
4、信號(hào)驅(qū)動(dòng)異步I/O模型
首先開啟套接口信號(hào)驅(qū)動(dòng)I/O功能, 并通過系統(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來讀數(shù)據(jù)報(bào),井通知主循環(huán)數(shù)據(jù)已準(zhǔn)備好被處理中坝锰。也可以通知主循環(huán)匆绣,讓它來讀數(shù)據(jù)報(bào)。
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í)完成。
非阻塞IO詳解
通過上面森爽,我們知道恨豁,所有的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)建描述符后,通過調(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編程過程中陶冷,當(dāng)需要處理多個(gè)請(qǐng)求的時(shí)钙姊,可以使用多線程和IO復(fù)用的方式進(jìn)行處理。上面的圖介紹了整個(gè)IO復(fù)用的過程埂伦,它通過把多個(gè)IO的阻塞復(fù)用到一個(gè)select之類的阻塞上煞额,從而使得系統(tǒng)在單線程的情況下同時(shí)支持處理多個(gè)請(qǐng)求。和多線程/進(jìn)程比較赤屋,I/O多路復(fù)用的最大優(yōu)勢是系統(tǒng)開銷小立镶,系統(tǒng)不需要建立新的進(jìn)程或者線程,也不必維護(hù)這些線程和進(jìn)程类早。IO復(fù)用常見的應(yīng)用場景:
- 客戶程序需要同時(shí)處理交互式的輸入和服務(wù)器之間的網(wǎng)絡(luò)連接媚媒。
- 客戶端需要對(duì)多個(gè)網(wǎng)絡(luò)連接作出反應(yīng)。
- 服務(wù)器需要同時(shí)處理多個(gè)處于監(jiān)聽狀態(tài)和多個(gè)連接狀態(tài)的套接字
服務(wù)器需要處理多種網(wǎng)絡(luò)協(xié)議的套接字涩僻。
目前支持I/O復(fù)用的系統(tǒng)調(diào)用有select缭召、pselect、poll逆日、epoll嵌巷,下面幾小結(jié)分別來學(xué)習(xí)一下select和epoll的使用。
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); // 檢查集合中指定的文件描述符是否可以讀寫 ?
void FD_SET(int fd, fd_set *set); //將一個(gè)給定的文件描述符加入集合之中
void FD_ZERO(fd_set *set);//清空集合
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í)返回坪圾,但是不超過由該參數(shù)所指向的timeval結(jié)構(gòu)中指定的秒數(shù)和微秒數(shù)晓折。如果超過時(shí)間,沒有描述字準(zhǔn)備好兽泄,那就返回0漓概。如果秒=微秒=0,檢查描述字后立即返回病梢,此時(shí)相當(dāng)于輪詢胃珍。
中間的三個(gè)參數(shù)readset、writeset和exceptset指定我們要讓內(nèi)核測試讀蜓陌、寫和異常條件的描述字觅彰。如果我們對(duì)某一個(gè)的條件不感興趣,就可以把它設(shè)為空指針钮热。
fd_set可以理解為一個(gè)集合缔莲,這個(gè)集合中存放的是文件描述符,可通過上面的四個(gè)宏進(jìn)行設(shè)置(注意霉旗,fd_set不是struct fd_set痴奏。蛀骇。。 剛剛開始調(diào)試程序就犯錯(cuò)誤读拆,返回storage size of 'fds' isn't known)擅憔。
第一個(gè)參數(shù)maxfdp1指定待測試的描述字個(gè)數(shù),它的值是待測試的最大描述字加1(因此我們把該參數(shù)命名為maxfdp1)檐晕,描述字0暑诸、1、2...maxfdp1-1均將被測試辟灰。
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)西采。t
imespec結(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)霹崎,再測試由這些當(dāng)前被禁止的信號(hào)處理函數(shù)設(shè)置的全局變量,然后調(diào)用pselect冶忱,告訴它重新設(shè)置信號(hào)掩碼尾菇。
首先我們看看信號(hào)掩碼的概念:在POSIX下,每個(gè)進(jìn)程有一個(gè)信號(hào)掩碼(signal mask)囚枪。簡單地說错沽,信號(hào)掩碼是一個(gè)“位圖”,其中每一位都對(duì)應(yīng)著一種信號(hào)眶拉。如果位圖中的某一位為1,就表示在執(zhí)行當(dāng)前信號(hào)的處理程序期間相應(yīng)的信號(hào)暫時(shí)被“屏蔽”憔儿,使得在執(zhí)行的過程中不會(huì)嵌套地響應(yīng)那種信號(hào)忆植。為什么對(duì)某一信號(hào)進(jìn)行屏蔽呢?我們來看一下對(duì)CTRL_C的處理谒臼。大家知道朝刊,當(dāng)一個(gè)程序正在運(yùn)行時(shí),在鍵盤上按一下CTRL_C蜈缤,內(nèi)核就會(huì)向相應(yīng)的進(jìn)程發(fā)出一個(gè)SIGINT 信號(hào)拾氓,而對(duì)這個(gè)信號(hào)的默認(rèn)操作就是通過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事件往往過于密集疲酌,有時(shí)候剛剛進(jìn)入第一個(gè)信號(hào)的處理程序蜡峰,第二個(gè)SIGINT信號(hào)就到達(dá)了,而第二個(gè)信號(hào)的默認(rèn)操作是殺死進(jìn)程朗恳,這樣湿颅,第一個(gè)信號(hào)的處理程序根本沒有執(zhí)行完。為了避免這種情況的出現(xiàn)粥诫,就在執(zhí)行一個(gè)信號(hào)處理程序的過程中將該種信號(hào)自動(dòng)屏蔽掉油航。所謂“屏蔽”,與將信號(hào)忽略是不同的臀脏,它只是將信號(hào)暫時(shí)“遮蓋”一下劝堪,一旦屏蔽去掉,已到達(dá)的信號(hào)又繼續(xù)得到處理揉稚。
在我們開發(fā)過程中秒啦,如果進(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沒有任何區(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()沒有使用低效的三個(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)視該文件描述符的事件掩碼,由用戶來設(shè)置這個(gè)域全景。revents域是文件描述符的操作結(jié)果事件掩碼耀石。內(nèi)核在調(diào)用返回時(shí)設(shè)置這個(gè)域**。events域中請(qǐng)求的任何事件都可能在revents域中返回爸黄,而部分事件不能出現(xiàn)在events中滞伟,詳細(xì)如下表揭鳞。
POLLIN:普通或優(yōu)先級(jí)帶數(shù)據(jù)可讀
POLLRDNORM:普通數(shù)據(jù)可讀
POLLRDBAND:優(yōu)先級(jí)帶數(shù)據(jù)可讀
POLLPRI:高優(yōu)先級(jí)數(shù)據(jù)可讀
POLLOUT:普通數(shù)據(jù)可寫
POLLWRNORM:普通數(shù)據(jù)可寫
POLLWRBAND:優(yōu)先級(jí)帶數(shù)據(jù)可寫
POLLERR:發(fā)生錯(cuò)誤
POLLHUP:發(fā)生掛起
POLLNVAL:描述字不是一個(gè)打開的文件
注意:后三個(gè)只能作為描述字的返回結(jié)果存儲(chǔ)在revents中,而不能作為測試條件用于events中诗良。
最后一個(gè)參數(shù)timeout是指定poll函數(shù)返回前等待多長時(shí)間
成功時(shí)汹桦,**poll()返回結(jié)構(gòu)體中revents域不為0的文件描述符個(gè)數(shù);如果在超時(shí)前沒有任何事件發(fā)生鉴裹,poll()返回0舞骆;失敗時(shí),poll()返回-1径荔。
關(guān)鍵代碼如下:
client[0].fd = listenfd; /*將數(shù)組中的第一個(gè)元素設(shè)置成監(jiān)聽描述字*/
client[0].events = POLLIN;
while(1)
{
nready = poll(client, maxi+1,INFTIM); //將進(jìn)程阻塞在poll上
if( client[0].revents & POLLIN/*POLLRDNORM*/ ) /*先測試監(jiān)聽描述字*/
epoll
在linux的網(wǎng)絡(luò)編程中督禽,很長的一段時(shí)間都在使用select來做事件觸發(fā)。然而select逐漸暴露出了一些缺陷总处,使得linux不得不在新的內(nèi)核中尋找出替代方案狈惫,那就是epoll。其實(shí)鹦马,epoll與select原理類似胧谈,只不過,epoll作出了一些重大改進(jìn)荸频,即:
1.支持一個(gè)進(jìn)程打開大數(shù)目的socket描述符(FD)select 最不能忍受的是一個(gè)進(jìn)程所打開的FD是有一定限制的菱肖,由FD_SETSIZE設(shè)置,默認(rèn)值是2048旭从。對(duì)于那些需要支持的上萬連接數(shù)目的IM服務(wù)器來說顯然太少了稳强。這時(shí)候你一是可以選擇修改這個(gè)宏然后重新編譯內(nèi)核,不過資料也同時(shí)指出這樣會(huì)帶來網(wǎng)絡(luò)效率的下降和悦,二是可以選擇多進(jìn)程的解決方案(傳統(tǒng)的 Apache方案)退疫,不過雖然linux上面創(chuàng)建進(jìn)程的代價(jià)比較小,但仍舊是不可忽視的鸽素,加上進(jìn)程間數(shù)據(jù)同步遠(yuǎn)比不上線程間同步的高效褒繁,所以也不是一種完美的方案。不過 epoll則沒有這個(gè)限制馍忽,它所支持的FD上限是最大可以打開文件的數(shù)目棒坏,這個(gè)數(shù)字一般遠(yuǎn)大于2048,舉個(gè)例子,在1GB內(nèi)存的機(jī)器上大約是10萬左右,具體數(shù)目可以cat /proc/sys/fs/file-max察看,一般來說這個(gè)數(shù)目和系統(tǒng)內(nèi)存關(guān)系很大舵匾。
2.IO效率不隨FD數(shù)目增加而線性下降傳統(tǒng)的select/poll另一個(gè)致命弱點(diǎn)就是當(dāng)你擁有一個(gè)很大的socket集合,不過由于網(wǎng)絡(luò)延時(shí)谁不,任一時(shí)間只有部分的socket是"活躍"的坐梯,但是select/poll每次調(diào)用都會(huì)線性掃描全部的集合,導(dǎo)致效率呈現(xiàn)線性下降刹帕。但是epoll不存在這個(gè)問題吵血,它只會(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)核秩命。
3.使用mmap加速內(nèi)核與用戶空間的消息傳遞。這點(diǎn)實(shí)際上涉及到epoll的具體實(shí)現(xiàn)了褒傅。無論是select,poll還是epoll都需要內(nèi)核把FD消息通知給用戶空間弃锐,如何避免不必要的內(nèi)存拷貝就很重要,在這點(diǎn)上殿托,epoll是通過內(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)旋廷,但是你無法回避linux平臺(tái)賦予你微調(diào)內(nèi)核的能力。
epoll api函數(shù)比較簡單礼搁,包括
- 創(chuàng)建一個(gè)epoll描述符
- 添加監(jiān)聽事件
- 阻塞等待所監(jiān)聽的事件發(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用來告訴內(nèi)核這個(gè)監(jiān)聽的數(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)聽事件
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è)宏來表示:
EPOLL_CTL_ADD:注冊(cè)新的fd到epfd中笤休;
EPOLL_CTL_MOD:修改已經(jīng)注冊(cè)的fd的監(jiān)聽事件建峭;
EPOLL_CTL_DEL:從epfd中刪除一個(gè)fd;
第三個(gè)參數(shù)是需要監(jiān)聽的描述符fd,第四個(gè)參數(shù)是告訴內(nèi)核需要監(jiān)聽什么事件,struct epoll_event結(jié)構(gòu)如上所示启泣,其中events為需要注冊(cè)的事件涣脚,可以為下面幾個(gè)宏的集合:
EPOLLIN:表示對(duì)應(yīng)的文件描述符可以讀(包括對(duì)端SOCKET正常關(guān)閉);
EPOLLOUT:表示對(duì)應(yīng)的文件描述符可以寫寥茫;
EPOLLPRI:表示對(duì)應(yīng)的文件描述符有緊急的數(shù)據(jù)可讀(這里應(yīng)該表示有帶外數(shù)據(jù)到來)遣蚀;
EPOLLERR:表示對(duì)應(yīng)的文件描述符發(fā)生錯(cuò)誤;
EPOLLHUP:表示對(duì)應(yīng)的文件描述符被掛斷;
EPOLLET:將EPOLL設(shè)為邊緣觸發(fā)(Edge Triggered)模式芭梯,這是相對(duì)于水平觸發(fā)(Level Triggered)來說的险耀。詳細(xì)見下面描述
EPOLLONESHOT:只監(jiān)聽一次事件,當(dāng)監(jiān)聽完這次事件之后玖喘,如果還需要繼續(xù)監(jiān)聽這個(gè)socket的話甩牺,需要再次把這個(gè)socket加入到EPOLL隊(duì)列里。
操作系統(tǒng)對(duì)被注冊(cè)的文件描述符的事件發(fā)送以后有兩種處理方式累奈,分別有LT和ET模式贬派,默認(rèn)情況下為LT,如果需要設(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)核通過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),不過在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)聽過程中膏孟,該描述符上面有相應(yīng)的事件發(fā)生,系統(tǒng)將會(huì)把該字段返回拌汇。先看監(jiān)聽函數(shù)吧柒桑。看完就知道
#include <sys/epoll.h>
#include <unistd.h>
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);//阻塞等待所監(jiān)聽的事件發(fā)生
阻塞監(jiān)聽函數(shù)噪舀,類似于select()調(diào)用魁淳。參數(shù)events用來從內(nèi)核得到事件的集合,返回的結(jié)構(gòu)也是struct epoll_event与倡,其中event為相應(yīng)的事件界逛,data為注冊(cè)時(shí),設(shè)置的值(常見情況纺座,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將不確定赞别,也有說法說是永久阻塞)。該函數(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);
}
}
}
最后我們說一下犹芹,用來克服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ā)的蛔趴。
信號(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渔隶。
Unix上有定義了許多信號(hào),源自Berkeley的實(shí)現(xiàn)使用的是SIGIO信號(hào)來支持套接字和終端設(shè)備上的信號(hào)驅(qū)動(dòng)IO洁奈。在套接字IO中间唉,信號(hào)驅(qū)動(dòng)IO模型主要是在UDP套接字上使用,在TCP套接字上幾乎是沒有什么使用的(在TCP上利术,由于TCP是雙工的呈野,它的信號(hào)產(chǎn)生過于平凡,并且信號(hào)的出現(xiàn)幾乎沒有告訴我們發(fā)生了什么事情印叁。因此對(duì)于TCP套接字被冒,SIGIO信號(hào)是沒有什么使用的)。
使用信號(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);
對(duì)同步異步以及阻塞與非阻塞的理解:
阻塞同步是指一直等待結(jié)果
非阻塞同步是指立即返回錯(cuò)誤的結(jié)果