Linux IO模式及 select业舍、poll、epoll詳解

同步IO和異步IO升酣,阻塞IO和非阻塞IO分別是什么舷暮,到底有什么區(qū)別?不同的人在不同的上下文下給出的答案是不同的噩茄。所以先限定一下本文的上下文下面。

本文討論的背景是Linux環(huán)境下的network IO。

一 概念說(shuō)明

在進(jìn)行解釋之前绩聘,首先要說(shuō)明幾個(gè)概念:

  • 用戶空間和內(nèi)核空間

  • 進(jìn)程切換

  • 進(jìn)程的阻塞

  • 文件描述符

  • 緩存 I/O

用戶空間與內(nèi)核空間

現(xiàn)在操作系統(tǒng)都是采用虛擬存儲(chǔ)器沥割,那么對(duì)32位操作系統(tǒng)而言,它的尋址空間(虛擬存儲(chǔ)空間)為4G(2的32次方)凿菩。操作系統(tǒng)的核心是內(nèi)核机杜,獨(dú)立于普通的應(yīng)用程序,可以訪問(wèn)受保護(hù)的內(nèi)存空間衅谷,也有訪問(wèn)底層硬件設(shè)備的所有權(quán)限椒拗。為了保證用戶進(jìn)程不能直接操作內(nèi)核(kernel),保證內(nèi)核的安全会喝,操心系統(tǒng)將虛擬空間劃分為兩部分陡叠,一部分為內(nèi)核空間,一部分為用戶空間肢执。針對(duì)linux操作系統(tǒng)而言枉阵,將最高的1G字節(jié)(從虛擬地址0xC0000000到0xFFFFFFFF),供內(nèi)核使用预茄,稱為內(nèi)核空間兴溜,而將較低的3G字節(jié)(從虛擬地址0x00000000到0xBFFFFFFF),供各個(gè)進(jìn)程使用耻陕,稱為用戶空間拙徽。

進(jìn)程切換

為了控制進(jìn)程的執(zhí)行,內(nèi)核必須有能力掛起正在CPU上運(yùn)行的進(jìn)程诗宣,并恢復(fù)以前掛起的某個(gè)進(jìn)程的執(zhí)行膘怕。這種行為被稱為進(jìn)程切換。因此可以說(shuō)召庞,任何進(jìn)程都是在操作系統(tǒng)內(nèi)核的支持下運(yùn)行的岛心,是與內(nèi)核緊密相關(guān)的。

從一個(gè)進(jìn)程的運(yùn)行轉(zhuǎn)到另一個(gè)進(jìn)程上運(yùn)行篮灼,這個(gè)過(guò)程中經(jīng)過(guò)下面這些變化:

1. 保存處理機(jī)上下文忘古,包括程序計(jì)數(shù)器和其他寄存器。

2. 更新PCB信息诅诱。

3. 把進(jìn)程的PCB移入相應(yīng)的隊(duì)列髓堪,如就緒、在某事件阻塞等隊(duì)列娘荡。

4. 選擇另一個(gè)進(jìn)程執(zhí)行干旁,并更新其PCB。

5. 更新內(nèi)存管理的數(shù)據(jù)結(jié)構(gòu)它改。

6. 恢復(fù)處理機(jī)上下文疤孕。

注:總而言之就是很耗資源,具體的可以參考這篇文章:進(jìn)程切換

進(jìn)程的阻塞

正在執(zhí)行的進(jìn)程央拖,由于期待的某些事件未發(fā)生祭阀,如請(qǐng)求系統(tǒng)資源失敗、等待某種操作的完成鲜戒、新數(shù)據(jù)尚未到達(dá)或無(wú)新工作做等专控,則由系統(tǒng)自動(dòng)執(zhí)行阻塞原語(yǔ)(Block),使自己由運(yùn)行狀態(tài)變?yōu)樽枞麪顟B(tài)遏餐÷赘可見(jiàn),進(jìn)程的阻塞是進(jìn)程自身的一種主動(dòng)行為失都,也因此只有處于運(yùn)行態(tài)的進(jìn)程(獲得CPU)柏蘑,才可能將其轉(zhuǎn)為阻塞狀態(tài)幸冻。當(dāng)進(jìn)程進(jìn)入阻塞狀態(tài),是不占用CPU資源的咳焚。

文件描述符fd

文件描述符(File descriptor)是計(jì)算機(jī)科學(xué)中的一個(gè)術(shù)語(yǔ)洽损,是一個(gè)用于表述指向文件的引用的抽象化概念。

文件描述符在形式上是一個(gè)非負(fù)整數(shù)革半。實(shí)際上碑定,它是一個(gè)索引值,指向內(nèi)核為每一個(gè)進(jìn)程所維護(hù)的該進(jìn)程打開(kāi)文件的記錄表又官。當(dāng)程序打開(kāi)一個(gè)現(xiàn)有文件或者創(chuàng)建一個(gè)新文件時(shí)延刘,內(nèi)核向進(jìn)程返回一個(gè)文件描述符。在程序設(shè)計(jì)中六敬,一些涉及底層的程序編寫往往會(huì)圍繞著文件描述符展開(kāi)碘赖。但是文件描述符這一概念往往只適用于UNIX、Linux這樣的操作系統(tǒng)外构。

緩存 I/O

緩存 I/O 又被稱作標(biāo)準(zhǔn) I/O崖疤,大多數(shù)文件系統(tǒng)的默認(rèn) I/O 操作都是緩存 I/O。在 Linux 的緩存 I/O 機(jī)制中典勇,操作系統(tǒng)會(huì)將 I/O 的數(shù)據(jù)緩存在文件系統(tǒng)的頁(yè)緩存( page cache )中劫哼,也就是說(shuō),數(shù)據(jù)會(huì)先被拷貝到操作系統(tǒng)內(nèi)核的緩沖區(qū)中割笙,然后才會(huì)從操作系統(tǒng)內(nèi)核的緩沖區(qū)拷貝到應(yīng)用程序的地址空間权烧。

緩存 I/O 的缺點(diǎn):

數(shù)據(jù)在傳輸過(guò)程中需要在應(yīng)用程序地址空間和內(nèi)核進(jìn)行多次數(shù)據(jù)拷貝操作,這些數(shù)據(jù)拷貝操作所帶來(lái)的 CPU 以及內(nèi)存開(kāi)銷是非常大的伤溉。

IO模式

剛才說(shuō)了般码,對(duì)于一次IO訪問(wèn)(以read舉例),數(shù)據(jù)會(huì)先被拷貝到操作系統(tǒng)內(nèi)核的緩沖區(qū)中乱顾,然后才會(huì)從操作系統(tǒng)內(nèi)核的緩沖區(qū)拷貝到應(yīng)用程序的地址空間板祝。所以說(shuō),當(dāng)一個(gè)read操作發(fā)生時(shí)走净,它會(huì)經(jīng)歷兩個(gè)階段:

1. 等待數(shù)據(jù)準(zhǔn)備 (Waiting for the data to be ready)

2. 將數(shù)據(jù)從內(nèi)核拷貝到進(jìn)程中 (Copying the data from the kernel to the process)

正式因?yàn)檫@兩個(gè)階段券时,linux系統(tǒng)產(chǎn)生了下面五種網(wǎng)絡(luò)模式的方案。

  • 阻塞 I/O(blocking IO)

  • 非阻塞 I/O(nonblocking IO)

  • I/O 多路復(fù)用( IO multiplexing)

  • 信號(hào)驅(qū)動(dòng) I/O( signal driven IO)

  • 異步 I/O(asynchronous IO)

注:由于signal driven IO在實(shí)際中并不常用伏伯,所以我這只提及剩下的四種IO Model橘洞。

阻塞 I/O(blocking IO)

在linux中,默認(rèn)情況下所有的socket都是blocking说搅,一個(gè)典型的讀操作流程大概是這樣:

image

當(dāng)用戶進(jìn)程調(diào)用了recvfrom這個(gè)系統(tǒng)調(diào)用炸枣,kernel就開(kāi)始了IO的第一個(gè)階段:準(zhǔn)備數(shù)據(jù)(對(duì)于網(wǎng)絡(luò)IO來(lái)說(shuō),很多時(shí)候數(shù)據(jù)在一開(kāi)始還沒(méi)有到達(dá)。比如适肠,還沒(méi)有收到一個(gè)完整的UDP包霍衫。這個(gè)時(shí)候kernel就要等待足夠的數(shù)據(jù)到來(lái))。這個(gè)過(guò)程需要等待侯养,也就是說(shuō)數(shù)據(jù)被拷貝到操作系統(tǒng)內(nèi)核的緩沖區(qū)中是需要一個(gè)過(guò)程的慕淡。而在用戶進(jìn)程這邊,整個(gè)進(jìn)程會(huì)被阻塞(當(dāng)然沸毁,是進(jìn)程自己選擇的阻塞)。當(dāng)kernel一直等到數(shù)據(jù)準(zhǔn)備好了傻寂,它就會(huì)將數(shù)據(jù)從kernel中拷貝到用戶內(nèi)存息尺,然后kernel返回結(jié)果,用戶進(jìn)程才解除block的狀態(tài)疾掰,重新運(yùn)行起來(lái)搂誉。

所以,blocking IO的特點(diǎn)就是在IO執(zhí)行的兩個(gè)階段都被block了静檬。

非阻塞 I/O(nonblocking IO)

linux下炭懊,可以通過(guò)設(shè)置socket使其變?yōu)閚on-blocking。當(dāng)對(duì)一個(gè)non-blocking socket執(zhí)行讀操作時(shí)拂檩,流程是這個(gè)樣子:

image

當(dāng)用戶進(jìn)程發(fā)出read操作時(shí)侮腹,如果kernel中的數(shù)據(jù)還沒(méi)有準(zhǔn)備好,那么它并不會(huì)block用戶進(jìn)程稻励,而是立刻返回一個(gè)error父阻。從用戶進(jìn)程角度講 ,它發(fā)起一個(gè)read操作后望抽,并不需要等待加矛,而是馬上就得到了一個(gè)結(jié)果。用戶進(jìn)程判斷結(jié)果是一個(gè)error時(shí)煤篙,它就知道數(shù)據(jù)還沒(méi)有準(zhǔn)備好斟览,于是它可以再次發(fā)送read操作。一旦kernel中的數(shù)據(jù)準(zhǔn)備好了辑奈,并且又再次收到了用戶進(jìn)程的system call苛茂,那么它馬上就將數(shù)據(jù)拷貝到了用戶內(nèi)存,然后返回鸠窗。

所以味悄,nonblocking IO的特點(diǎn)是用戶進(jìn)程需要不斷的主動(dòng)詢問(wèn)kernel數(shù)據(jù)好了沒(méi)有。

I/O 多路復(fù)用( IO multiplexing)

IO multiplexing就是我們說(shuō)的select塌鸯,poll侍瑟,epoll,有些地方也稱這種IO方式為event driven IO。select/epoll的好處就在于單個(gè)process就可以同時(shí)處理多個(gè)網(wǎng)絡(luò)連接的IO涨颜。它的基本原理就是select费韭,poll,epoll這個(gè)function會(huì)不斷的輪詢所負(fù)責(zé)的所有socket庭瑰,當(dāng)某個(gè)socket有數(shù)據(jù)到達(dá)了星持,就通知用戶進(jìn)程。

image

當(dāng)用戶進(jìn)程調(diào)用了select弹灭,那么整個(gè)進(jìn)程會(huì)被block督暂,而同時(shí),kernel會(huì)“監(jiān)視”所有select負(fù)責(zé)的socket穷吮,當(dāng)任何一個(gè)socket中的數(shù)據(jù)準(zhǔn)備好了逻翁,select就會(huì)返回。這個(gè)時(shí)候用戶進(jìn)程再調(diào)用read操作捡鱼,將數(shù)據(jù)從kernel拷貝到用戶進(jìn)程八回。

所以,I/O 多路復(fù)用的特點(diǎn)是通過(guò)一種機(jī)制一個(gè)進(jìn)程能同時(shí)等待多個(gè)文件描述符驾诈,而這些文件描述符(套接字描述符)其中的任意一個(gè)進(jìn)入讀就緒狀態(tài)缠诅,select()函數(shù)就可以返回。

這個(gè)圖和blocking IO的圖其實(shí)并沒(méi)有太大的不同乍迄,事實(shí)上管引,還更差一些。因?yàn)檫@里需要使用兩個(gè)system call (select 和 recvfrom)闯两,而blocking IO只調(diào)用了一個(gè)system call (recvfrom)汉匙。但是,用select的優(yōu)勢(shì)在于它可以同時(shí)處理多個(gè)connection生蚁。

所以噩翠,如果處理的連接數(shù)不是很高的話,使用select/epoll的web server不一定比使用multi-threading + blocking IO的web server性能更好邦投,可能延遲還更大伤锚。select/epoll的優(yōu)勢(shì)并不是對(duì)于單個(gè)連接能處理得更快,而是在于能處理更多的連接志衣。)

在IO multiplexing Model中屯援,實(shí)際中,對(duì)于每一個(gè)socket念脯,一般都設(shè)置成為non-blocking狞洋,但是,如上圖所示绿店,整個(gè)用戶的process其實(shí)是一直被block的吉懊。只不過(guò)process是被select這個(gè)函數(shù)block庐橙,而不是被socket IO給block。

異步 I/O(asynchronous IO)

Linux下的asynchronous IO其實(shí)用得很少借嗽。先看一下它的流程:

image

用戶進(jìn)程發(fā)起read操作之后态鳖,立刻就可以開(kāi)始去做其它的事。而另一方面恶导,從kernel的角度浆竭,當(dāng)它受到一個(gè)asynchronous read之后,首先它會(huì)立刻返回惨寿,所以不會(huì)對(duì)用戶進(jìn)程產(chǎn)生任何block邦泄。然后,kernel會(huì)等待數(shù)據(jù)準(zhǔn)備完成裂垦,然后將數(shù)據(jù)拷貝到用戶內(nèi)存顺囊,當(dāng)這一切都完成之后,kernel會(huì)給用戶進(jìn)程發(fā)送一個(gè)signal缸废,告訴它read操作完成了。

總結(jié)

blocking和non-blocking的區(qū)別

調(diào)用blocking IO會(huì)一直block住對(duì)應(yīng)的進(jìn)程直到操作完成驶社,而non-blocking IO在kernel還準(zhǔn)備數(shù)據(jù)的情況下會(huì)立刻返回企量。

synchronous IO和asynchronous IO的區(qū)別

在說(shuō)明synchronous IO和asynchronous IO的區(qū)別之前,需要先給出兩者的定義亡电。POSIX的定義是這樣子的:

  • A synchronous I/O operation causes the requesting process to be blocked until that I/O operation completes;

  • An asynchronous I/O operation does not cause the requesting process to be blocked;

兩者的區(qū)別就在于synchronous IO做”IO operation”的時(shí)候會(huì)將process阻塞届巩。按照這個(gè)定義,之前所述的blocking IO份乒,non-blocking IO恕汇,IO multiplexing都屬于synchronous IO。

有人會(huì)說(shuō)或辖,non-blocking IO并沒(méi)有被block啊瘾英。這里有個(gè)非常“狡猾”的地方颂暇,定義中所指的”IO operation”是指真實(shí)的IO操作缺谴,就是例子中的recvfrom這個(gè)system call。non-blocking IO在執(zhí)行recvfrom這個(gè)system call的時(shí)候耳鸯,如果kernel的數(shù)據(jù)沒(méi)有準(zhǔn)備好湿蛔,這時(shí)候不會(huì)block進(jìn)程。但是县爬,當(dāng)kernel中數(shù)據(jù)準(zhǔn)備好的時(shí)候阳啥,recvfrom會(huì)將數(shù)據(jù)從kernel拷貝到用戶內(nèi)存中,這個(gè)時(shí)候進(jìn)程是被block了财喳,在這段時(shí)間內(nèi)察迟,進(jìn)程是被block的。

而asynchronous IO則不一樣,當(dāng)進(jìn)程發(fā)起IO 操作之后卷拘,就直接返回再也不理睬了喊废,直到kernel發(fā)送一個(gè)信號(hào),告訴進(jìn)程說(shuō)IO完成栗弟。在這整個(gè)過(guò)程中污筷,進(jìn)程完全沒(méi)有被block。

各個(gè)IO Model的比較如圖所示:

image

通過(guò)上面的圖片乍赫,可以發(fā)現(xiàn)non-blocking IO和asynchronous IO的區(qū)別還是很明顯的瓣蛀。在non-blocking IO中,雖然進(jìn)程大部分時(shí)間都不會(huì)被block雷厂,但是它仍然要求進(jìn)程去主動(dòng)的check惋增,并且當(dāng)數(shù)據(jù)準(zhǔn)備完成以后,也需要進(jìn)程主動(dòng)的再次調(diào)用recvfrom來(lái)將數(shù)據(jù)拷貝到用戶內(nèi)存改鲫。而asynchronous IO則完全不同诈皿。它就像是用戶進(jìn)程將整個(gè)IO操作交給了他人(kernel)完成,然后他人做完后發(fā)信號(hào)通知像棘。在此期間稽亏,用戶進(jìn)程不需要去檢查IO操作的狀態(tài),也不需要主動(dòng)的去拷貝數(shù)據(jù)缕题。

三 I/O 多路復(fù)用之select截歉、poll、epoll詳解

select烟零,poll瘪松,epoll都是IO多路復(fù)用的機(jī)制。I/O多路復(fù)用就是通過(guò)一種機(jī)制锨阿,一個(gè)進(jìn)程可以監(jiān)視多個(gè)描述符宵睦,一旦某個(gè)描述符就緒(一般是讀就緒或者寫就緒),能夠通知程序進(jìn)行相應(yīng)的讀寫操作墅诡。但select状飞,poll,epoll本質(zhì)上都是同步I/O书斜,因?yàn)樗麄兌夹枰谧x寫事件就緒后自己負(fù)責(zé)進(jìn)行讀寫诬辈,也就是說(shuō)這個(gè)讀寫過(guò)程是阻塞的,而異步I/O則無(wú)需自己負(fù)責(zé)進(jìn)行讀寫荐吉,異步I/O的實(shí)現(xiàn)會(huì)負(fù)責(zé)把數(shù)據(jù)從內(nèi)核拷貝到用戶空間焙糟。(這里啰嗦下)

select

<pre>

intselect(intn, fd_set *readfds, fd_set *writefds, fd_set *exceptfds,structtimeval *timeout);

</pre>

select 函數(shù)監(jiān)視的文件描述符分3類,分別是writefds样屠、readfds穿撮、和exceptfds缺脉。調(diào)用后select函數(shù)會(huì)阻塞,直到有描述副就緒(有數(shù)據(jù) 可讀悦穿、可寫攻礼、或者有except),或者超時(shí)(timeout指定等待時(shí)間栗柒,如果立即返回設(shè)為null即可)礁扮,函數(shù)返回。當(dāng)select函數(shù)返回后瞬沦,可以 通過(guò)遍歷fdset太伊,來(lái)找到就緒的描述符。

select目前幾乎在所有的平臺(tái)上支持逛钻,其良好跨平臺(tái)支持也是它的一個(gè)優(yōu)點(diǎn)僚焦。select的一 個(gè)缺點(diǎn)在于單個(gè)進(jìn)程能夠監(jiān)視的文件描述符的數(shù)量存在最大限制,在Linux上一般為1024曙痘,可以通過(guò)修改宏定義甚至重新編譯內(nèi)核的方式提升這一限制芳悲,但 是這樣也會(huì)造成效率的降低。

poll

<code>

int poll (struct pollfd *fds, unsigned int nfds, int timeout);

不同與select使用三個(gè)位圖來(lái)表示三個(gè)fdset的方式边坤,poll使用一個(gè) pollfd的指針實(shí)現(xiàn)名扛。

struct pollfd {

int fd; /* file descriptor */

short events; /* requested events to watch */

short revents; /* returned events witnessed */

};

</code>

pollfd結(jié)構(gòu)包含了要監(jiān)視的event和發(fā)生的event,不再使用select“參數(shù)-值”傳遞的方式惩嘉。同時(shí)罢洲,pollfd并沒(méi)有最大數(shù)量限制(但是數(shù)量過(guò)大后性能也是會(huì)下降)踢故。 和select函數(shù)一樣文黎,poll返回后,需要輪詢pollfd來(lái)獲取就緒的描述符殿较。

從上面看耸峭,select和poll都需要在返回后,通過(guò)遍歷文件描述符來(lái)獲取已經(jīng)就緒的socket淋纲。事實(shí)上劳闹,同時(shí)連接的大量客戶端在一時(shí)刻可能只有很少的處于就緒狀態(tài),因此隨著監(jiān)視的描述符數(shù)量的增長(zhǎng)洽瞬,其效率也會(huì)線性下降本涕。

epoll

epoll是在2.6內(nèi)核中提出的,是之前的select和poll的增強(qiáng)版本伙窃。相對(duì)于select和poll來(lái)說(shuō)菩颖,epoll更加靈活,沒(méi)有描述符限制为障。epoll使用一個(gè)文件描述符管理多個(gè)描述符晦闰,將用戶關(guān)系的文件描述符的事件存放到內(nèi)核的一個(gè)事件表中放祟,這樣在用戶空間和內(nèi)核空間的copy只需一次。

一 epoll操作過(guò)程

epoll操作過(guò)程需要三個(gè)接口呻右,分別如下:

<code>

int epoll_create(int size)跪妥;//創(chuàng)建一個(gè)epoll的句柄,size用來(lái)告訴內(nèi)核這個(gè)監(jiān)聽(tīng)的數(shù)目一共有多大

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)声滥;

int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

</code>

1. int epoll_create(int size);

創(chuàng)建一個(gè)epoll的句柄眉撵,size用來(lái)告訴內(nèi)核這個(gè)監(jiān)聽(tīng)的數(shù)目一共有多大,這個(gè)參數(shù)不同于select()中的第一個(gè)參數(shù)醒串,給出最大監(jiān)聽(tīng)的fd+1的值执桌,參數(shù)size并不是限制了epoll所能監(jiān)聽(tīng)的描述符最大個(gè)數(shù),只是對(duì)內(nèi)核初始分配內(nèi)部數(shù)據(jù)結(jié)構(gòu)的一個(gè)建議芜赌。

當(dāng)創(chuàng)建好epoll句柄后仰挣,它就會(huì)占用一個(gè)fd值,在linux下如果查看/proc/進(jìn)程id/fd/缠沈,是能夠看到這個(gè)fd的膘壶,所以在使用完epoll后,必須調(diào)用close()關(guān)閉洲愤,否則可能導(dǎo)致fd被耗盡颓芭。

*2. int epoll_ctl(int epfd, int op, int fd, struct epoll_event event);

函數(shù)是對(duì)指定描述符fd執(zhí)行op操作柬赐。

  • epfd:是epoll_create()的返回值亡问。

  • op:表示op操作,用三個(gè)宏來(lái)表示:添加EPOLL_CTL_ADD肛宋,刪除EPOLL_CTL_DEL州藕,修改EPOLL_CTL_MOD。分別添加酝陈、刪除和修改對(duì)fd的監(jiān)聽(tīng)事件床玻。

  • fd:是需要監(jiān)聽(tīng)的fd(文件描述符)

  • epoll_event:是告訴內(nèi)核需要監(jiān)聽(tīng)什么事,struct epoll_event結(jié)構(gòu)如下:

<code>

struct epoll_event {

__uint32_t events; /* Epoll events */

epoll_data_t data; /* User data variable */

};

//events可以是以下幾個(gè)宏的集合:

EPOLLIN :表示對(duì)應(yīng)的文件描述符可以讀(包括對(duì)端SOCKET正常關(guān)閉)沉帮;

EPOLLOUT:表示對(duì)應(yīng)的文件描述符可以寫锈死;

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ō)的喇勋。

EPOLLONESHOT:只監(jiān)聽(tīng)一次事件缨该,當(dāng)監(jiān)聽(tīng)完這次事件之后,如果還需要繼續(xù)監(jiān)聽(tīng)這個(gè)socket的話茄蚯,需要再次把這個(gè)socket加入到EPOLL隊(duì)列里

</code>

3. int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

等待epfd上的io事件压彭,最多返回maxevents個(gè)事件睦优。

參數(shù)events用來(lái)從內(nèi)核得到事件的集合,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í)菱阵。

二 工作模式

epoll對(duì)文件描述符的操作有兩種模式:LT(level trigger)ET(edge trigger)。LT模式是默認(rèn)模式缩功,LT模式與ET模式的區(qū)別如下:

LT模式:當(dāng)epoll_wait檢測(cè)到描述符事件發(fā)生并將此事件通知應(yīng)用程序晴及,應(yīng)用程序可以不立即處理該事件。下次調(diào)用epoll_wait時(shí)嫡锌,會(huì)再次響應(yīng)應(yīng)用程序并通知此事件虑稼。

ET模式:當(dāng)epoll_wait檢測(cè)到描述符事件發(fā)生并將此事件通知應(yīng)用程序,應(yīng)用程序必須立即處理該事件势木。如果不處理蛛倦,下次調(diào)用epoll_wait時(shí),不會(huì)再次響應(yīng)應(yīng)用程序并通知此事件啦桌。

1. LT模式

LT(level triggered)是缺省的工作方式溯壶,并且同時(shí)支持block和no-block socket.在這種做法中,內(nèi)核告訴你一個(gè)文件描述符是否就緒了甫男,然后你可以對(duì)這個(gè)就緒的fd進(jìn)行IO操作且改。如果你不作任何操作,內(nèi)核還是會(huì)繼續(xù)通知你的查剖。

2. ET模式

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)

ET模式在很大程度上減少了epoll事件被重復(fù)觸發(fā)的次數(shù),因此效率要比LT模式高郊供。epoll工作在ET模式的時(shí)候峡碉,必須使用非阻塞套接口,以避免由于一個(gè)文件句柄的阻塞讀/阻塞寫操作把處理多個(gè)文件描述符的任務(wù)餓死驮审。

3. 總結(jié)

假如有這樣一個(gè)例子:

1. 我們已經(jīng)把一個(gè)用來(lái)從管道中讀取數(shù)據(jù)的文件句柄(RFD)添加到epoll描述符

2. 這個(gè)時(shí)候從管道的另一端被寫入了2KB的數(shù)據(jù)

3. 調(diào)用epoll_wait(2)鲫寄,并且它會(huì)返回RFD,說(shuō)明它已經(jīng)準(zhǔn)備好讀取操作

4. 然后我們讀取了1KB的數(shù)據(jù)

5. 調(diào)用epoll_wait(2)......

LT模式:

如果是LT模式疯淫,那么在第5步調(diào)用epoll_wait(2)之后地来,仍然能受到通知。

ET模式:

如果我們?cè)诘?步將RFD添加到epoll描述符的時(shí)候使用了EPOLLET標(biāo)志熙掺,那么在第5步調(diào)用epoll_wait(2)之后將有可能會(huì)掛起未斑,因?yàn)槭S嗟臄?shù)據(jù)還存在于文件的輸入緩沖區(qū)內(nèi),而且數(shù)據(jù)發(fā)出端還在等待一個(gè)針對(duì)已經(jīng)發(fā)出數(shù)據(jù)的反饋信息币绩。只有在監(jiān)視的文件句柄上發(fā)生了某個(gè)事件的時(shí)候 ET 工作模式才會(huì)匯報(bào)事件颂碧。因此在第5步的時(shí)候,調(diào)用者可能會(huì)放棄等待仍在存在于文件輸入緩沖區(qū)內(nèi)的剩余數(shù)據(jù)类浪。

當(dāng)使用epoll的ET模型來(lái)工作時(shí)载城,當(dāng)產(chǎn)生了一個(gè)EPOLLIN事件后,

讀數(shù)據(jù)的時(shí)候需要考慮的是當(dāng)recv()返回的大小如果等于請(qǐng)求的大小费就,那么很有可能是緩沖區(qū)還有數(shù)據(jù)未讀完诉瓦,也意味著該次事件還沒(méi)有處理完,所以還需要再次讀攘ο浮:

<code>

while(rs){

buflen = recv(activeevents[i].data.fd, buf, sizeof(buf), 0);

if(buflen < 0){

// 由于是非阻塞的模式,所以當(dāng)errno為EAGAIN時(shí),表示當(dāng)前緩沖區(qū)已無(wú)數(shù)據(jù)可讀

// 在這里就當(dāng)作是該次事件已處理處.

if(errno == EAGAIN){

    break;

}

else{

    return;

}

}

else if(buflen == 0){

// 這里表示對(duì)端的socket已正常關(guān)閉.

}

if(buflen == sizeof(buf){

  rs = 1;  // 需要再次讀取

}

else{

  rs = 0;

}

}

</code>

Linux中的EAGAIN含義

Linux環(huán)境下開(kāi)發(fā)經(jīng)常會(huì)碰到很多錯(cuò)誤(設(shè)置errno)睬澡,其中EAGAIN是其中比較常見(jiàn)的一個(gè)錯(cuò)誤(比如用在非阻塞操作中)。

從字面上來(lái)看眠蚂,是提示再試一次煞聪。這個(gè)錯(cuò)誤經(jīng)常出現(xiàn)在當(dāng)應(yīng)用程序進(jìn)行一些非阻塞(non-blocking)操作(對(duì)文件或socket)的時(shí)候。

例如逝慧,以 O_NONBLOCK的標(biāo)志打開(kāi)文件/socket/FIFO昔脯,如果你連續(xù)做read操作而沒(méi)有數(shù)據(jù)可讀。此時(shí)程序不會(huì)阻塞起來(lái)等待數(shù)據(jù)準(zhǔn)備就緒返回笛臣,read函數(shù)會(huì)返回一個(gè)錯(cuò)誤EAGAIN云稚,提示你的應(yīng)用程序現(xiàn)在沒(méi)有數(shù)據(jù)可讀請(qǐng)稍后再試。

又例如沈堡,當(dāng)一個(gè)系統(tǒng)調(diào)用(比如fork)因?yàn)闆](méi)有足夠的資源(比如虛擬內(nèi)存)而執(zhí)行失敗静陈,返回EAGAIN提示其再調(diào)用一次(也許下次就能成功)。

三 代碼演示

下面是一段不完整的代碼且格式不對(duì),意在表述上面的過(guò)程鲸拥,去掉了一些模板代碼拐格。

<code>

define IPADDRESS "127.0.0.1"

define PORT 8787

define MAXSIZE 1024

define LISTENQ 5

define FDSIZE 1000

define EPOLLEVENTS 100

listenfd = socket_bind(IPADDRESS,PORT);

struct epoll_event events[EPOLLEVENTS];

//創(chuàng)建一個(gè)描述符

epollfd = epoll_create(FDSIZE);

//添加監(jiān)聽(tīng)描述符事件

add_event(epollfd,listenfd,EPOLLIN);

//循環(huán)等待

for ( ; ; ){

//該函數(shù)返回已經(jīng)準(zhǔn)備好的描述符事件數(shù)目

ret = epoll_wait(epollfd,events,EPOLLEVENTS,-1);

//處理接收到的連接

handle_events(epollfd,events,ret,listenfd,buf);

}

//事件處理函數(shù)

static void handle_events(int epollfd,struct epoll_event *events,int num,int listenfd,char *buf)

{

int i;

int fd;

//進(jìn)行遍歷;這里只要遍歷已經(jīng)準(zhǔn)備好的io事件。num并不是當(dāng)初epoll_create時(shí)的FDSIZE刑赶。

for (i = 0;i < num;i++)

{

    fd = events[i].data.fd;

    //根據(jù)描述符的類型和事件類型進(jìn)行處理

    if ((fd == listenfd) &&(events[i].events & EPOLLIN))

        handle_accpet(epollfd,listenfd);

    else if (events[i].events & EPOLLIN)

        do_read(epollfd,fd,buf);

    else if (events[i].events & EPOLLOUT)

        do_write(epollfd,fd,buf);

}

}

//添加事件

static void add_event(int epollfd,int fd,int state){

struct epoll_event ev;

ev.events = state;

ev.data.fd = fd;

epoll_ctl(epollfd,EPOLL_CTL_ADD,fd,&ev);

}

//處理接收到的連接

static void handle_accpet(int epollfd,int listenfd){

int clifd;   

struct sockaddr_in cliaddr;   

socklen_t  cliaddrlen;   

clifd = accept(listenfd,(struct sockaddr*)&cliaddr,&cliaddrlen);   

if (clifd == -1)       

perror("accpet error:");   

else {       

    printf("accept a new client: %s:%d\n",inet_ntoa(cliaddr.sin_addr),cliaddr.sin_port);                      //添加一個(gè)客戶描述符和事件       

    add_event(epollfd,clifd,EPOLLIN);   

}

}

//讀處理

static void do_read(int epollfd,int fd,char *buf){

int nread;

nread = read(fd,buf,MAXSIZE);

if (nread == -1)    {       

    perror("read error:");       

    close(fd); //記住close fd       

    delete_event(epollfd,fd,EPOLLIN); //刪除監(jiān)聽(tīng)

}

else if (nread == 0)    {       

    fprintf(stderr,"client close.\n");

    close(fd); //記住close fd     

    delete_event(epollfd,fd,EPOLLIN); //刪除監(jiān)聽(tīng)

}   

else {       

    printf("read message is : %s",buf);       

    //修改描述符對(duì)應(yīng)的事件禁荒,由讀改為寫       

    modify_event(epollfd,fd,EPOLLOUT);   

}

}

//寫處理

static void do_write(int epollfd,int fd,char *buf) {

int nwrite;   

nwrite = write(fd,buf,strlen(buf));   

if (nwrite == -1){       

    perror("write error:");       

    close(fd);  //記住close fd     

    delete_event(epollfd,fd,EPOLLOUT);  //刪除監(jiān)聽(tīng)   

}else{

    modify_event(epollfd,fd,EPOLLIN);

}   

memset(buf,0,MAXSIZE);

}

//刪除事件

static void delete_event(int epollfd,int fd,int state) {

struct epoll_event ev;

ev.events = state;

ev.data.fd = fd;

epoll_ctl(epollfd,EPOLL_CTL_DEL,fd,&ev);

}

//修改事件

static void modify_event(int epollfd,int fd,int state){

struct epoll_event ev;

ev.events = state;

ev.data.fd = fd;

epoll_ctl(epollfd,EPOLL_CTL_MOD,fd,&ev);

}

</code>

//注:另外一端我就省了

四 epoll總結(jié)

在 select/poll中,進(jìn)程只有在調(diào)用一定的方法后角撞,內(nèi)核才對(duì)所有監(jiān)視的文件描述符進(jìn)行掃描呛伴,而epoll事先通過(guò)epoll_ctl()來(lái)注冊(cè)一 個(gè)文件描述符,一旦基于某個(gè)文件描述符就緒時(shí)谒所,內(nèi)核會(huì)采用類似callback的回調(diào)機(jī)制热康,迅速激活這個(gè)文件描述符,當(dāng)進(jìn)程調(diào)用epoll_wait() 時(shí)便得到通知劣领。(此處去掉了遍歷文件描述符姐军,而是通過(guò)監(jiān)聽(tīng)回調(diào)的的機(jī)制。這正是epoll的魅力所在尖淘。)

epoll的優(yōu)點(diǎn)主要是一下幾個(gè)方面:

1. 監(jiān)視的描述符數(shù)量不受限制奕锌,它所支持的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)系很大惊暴。select的最大缺點(diǎn)就是進(jìn)程打開(kāi)的fd是有數(shù)量限制的。這對(duì) 于連接數(shù)量比較大的服務(wù)器來(lái)說(shuō)根本不能滿足趁桃。雖然也可以選擇多進(jìn)程的解決方案( Apache就是這樣實(shí)現(xiàn)的)辽话,不過(guò)雖然linux上面創(chuàng)建進(jìn)程的代價(jià)比較小,但仍舊是不可忽視的卫病,加上進(jìn)程間數(shù)據(jù)同步遠(yuǎn)比不上線程間同步的高效油啤,所以也不是一種完美的方案。

IO的效率不會(huì)隨著監(jiān)視fd的數(shù)量的增長(zhǎng)而下降蟀苛。epoll不同于select和poll輪詢的方式益咬,而是通過(guò)每個(gè)fd定義的回調(diào)函數(shù)來(lái)實(shí)現(xiàn)的。只有就緒的fd才會(huì)執(zhí)行回調(diào)函數(shù)帜平。

如果沒(méi)有大量的idle -connection或者dead-connection幽告,epoll的效率并不會(huì)比select/poll高很多,但是當(dāng)遇到大量的idle- connection罕模,就會(huì)發(fā)現(xiàn)epoll的效率大大高于select/poll评腺。

參考

用戶空間與內(nèi)核空間帘瞭,進(jìn)程上下文與中斷上下文[總結(jié)]

進(jìn)程切換

維基百科-文件描述符

Linux 中直接 I/O 機(jī)制的介紹

IO - 同步淑掌,異步,阻塞蝶念,非阻塞 (亡羊補(bǔ)牢篇)

Linux中select poll和epoll的區(qū)別

IO多路復(fù)用之select總結(jié)

IO多路復(fù)用之poll總結(jié)

IO多路復(fù)用之epoll總結(jié)

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末抛腕,一起剝皮案震驚了整個(gè)濱河市芋绸,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌担敌,老刑警劉巖摔敛,帶你破解...
    沈念sama閱讀 206,126評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異全封,居然都是意外死亡马昙,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門刹悴,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)行楞,“玉大人,你說(shuō)我怎么就攤上這事土匀∽臃浚” “怎么了?”我有些...
    開(kāi)封第一講書人閱讀 152,445評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵就轧,是天一觀的道長(zhǎng)证杭。 經(jīng)常有香客問(wèn)我,道長(zhǎng)妒御,這世上最難降的妖魔是什么解愤? 我笑而不...
    開(kāi)封第一講書人閱讀 55,185評(píng)論 1 278
  • 正文 為了忘掉前任,我火速辦了婚禮乎莉,結(jié)果婚禮上拔创,老公的妹妹穿的比我還像新娘。我一直安慰自己届吁,他們只是感情好十电,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,178評(píng)論 5 371
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著肥橙,像睡著了一般魄宏。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上存筏,一...
    開(kāi)封第一講書人閱讀 48,970評(píng)論 1 284
  • 那天宠互,我揣著相機(jī)與錄音,去河邊找鬼椭坚。 笑死予跌,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的善茎。 我是一名探鬼主播券册,決...
    沈念sama閱讀 38,276評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了烁焙?” 一聲冷哼從身側(cè)響起航邢,我...
    開(kāi)封第一講書人閱讀 36,927評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎骄蝇,沒(méi)想到半個(gè)月后膳殷,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,400評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡九火,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,883評(píng)論 2 323
  • 正文 我和宋清朗相戀三年赚窃,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片岔激。...
    茶點(diǎn)故事閱讀 37,997評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡考榨,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出鹦倚,到底是詐尸還是另有隱情河质,我是刑警寧澤,帶...
    沈念sama閱讀 33,646評(píng)論 4 322
  • 正文 年R本政府宣布震叙,位于F島的核電站掀鹅,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏媒楼。R本人自食惡果不足惜乐尊,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,213評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望划址。 院中可真熱鬧扔嵌,春花似錦、人聲如沸夺颤。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 30,204評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)世澜。三九已至独旷,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間寥裂,已是汗流浹背嵌洼。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 31,423評(píng)論 1 260
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留封恰,地道東北人麻养。 一個(gè)月前我還...
    沈念sama閱讀 45,423評(píng)論 2 352
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像诺舔,于是被迫代替她去往敵國(guó)和親鳖昌。 傳聞我的和親對(duì)象是個(gè)殘疾皇子备畦,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,722評(píng)論 2 345

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