Java網(wǎng)絡(luò)編程和NIO詳解6:Linux epoll實(shí)現(xiàn)原理詳解

微信公眾號(hào)【黃小斜】作者是螞蟻金服 JAVA 工程師,目前在螞蟻財(cái)富負(fù)責(zé)后端開發(fā)工作,專注于 JAVA 后端技術(shù)棧,同時(shí)也懂點(diǎn)投資理財(cái),堅(jiān)持學(xué)習(xí)和寫作,用大廠程序員的視角解讀技術(shù)與互聯(lián)網(wǎng),我的世界里不只有 coding!關(guān)注公眾號(hào)后回復(fù)”架構(gòu)師“即可領(lǐng)取 Java基礎(chǔ)、進(jìn)階、項(xiàng)目和架構(gòu)師等免費(fèi)學(xué)習(xí)資料,更有數(shù)據(jù)庫(kù)、分布式、微服務(wù)等熱門技術(shù)學(xué)習(xí)視頻,內(nèi)容豐富,兼顧原理和實(shí)踐,另外也將贈(zèng)送作者原創(chuàng)的Java學(xué)習(xí)指南、Java程序員面試指南等干貨資源

Linux epoll實(shí)現(xiàn)原理詳解

在linux 沒(méi)有實(shí)現(xiàn)epoll事件驅(qū)動(dòng)機(jī)制之前,我們一般選擇用select或者poll等IO多路復(fù)用的方法來(lái)實(shí)現(xiàn)并發(fā)服務(wù)程序。在大數(shù)據(jù)、高并發(fā)、集群等一些名詞唱得火熱之年代,select和poll的用武之地越來(lái)越有限,風(fēng)頭已經(jīng)被epoll占盡。

本文便來(lái)介紹epoll的實(shí)現(xiàn)機(jī)制,并附帶講解一下select和poll。通過(guò)對(duì)比其不同的實(shí)現(xiàn)機(jī)制,真正理解為何epoll能實(shí)現(xiàn)高并發(fā)。

這部分轉(zhuǎn)自https://jeff.wtf/2017/02/IO-multiplexing/

為什么要 I/O 多路復(fù)用

當(dāng)需要從一個(gè)叫 r_fd 的描述符不停地讀取數(shù)據(jù),并把讀到的數(shù)據(jù)寫入一個(gè)叫 w_fd 的描述符時(shí),我們可以用循環(huán)使用阻塞 I/O :

<pre>123</pre> <pre>while((n = read(r_fd, buf, BUF_SIZE)) > 0) if(write(w_fd, buf, n) != n) err_sys("write error");</pre>

但是,如果要從兩個(gè)地方讀取數(shù)據(jù)呢?這時(shí),不能再使用會(huì)把程序阻塞住的 read 函數(shù)。因?yàn)榭赡茉谧枞氐却?r_fd1 的數(shù)據(jù)時(shí),來(lái)不及處理 r_fd2,已經(jīng)到達(dá)的 r_fd2 的數(shù)據(jù)可能會(huì)丟失掉。

這個(gè)情況下需要使用非阻塞 I/O。

只要做個(gè)標(biāo)記,把文件描述符標(biāo)記為非阻塞的,以后再對(duì)它使用 read 函數(shù):如果它還沒(méi)有數(shù)據(jù)可讀,函數(shù)會(huì)立即返回并把 errorno 這個(gè)變量的值設(shè)置為 35,于是我們知道它沒(méi)有數(shù)據(jù)可讀,然后可以立馬去對(duì)其他描述符使用 read;如果它有數(shù)據(jù)可讀,我們就讀取它數(shù)據(jù)。對(duì)所有要讀的描述符都調(diào)用了一遍 read 之后,我們可以等一個(gè)較長(zhǎng)的時(shí)間(比如幾秒),然后再?gòu)牡谝粋€(gè)文件描述符開始調(diào)用 read 。這種循環(huán)就叫做輪詢(polling)。

這樣,不會(huì)像使用阻塞 I/O 時(shí)那樣因?yàn)橐粋€(gè)描述符 read 長(zhǎng)時(shí)間處于等待數(shù)據(jù)而使程序阻塞。

輪詢的缺點(diǎn)是浪費(fèi)太多 CPU 時(shí)間。大多數(shù)時(shí)候我們沒(méi)有數(shù)據(jù)可讀,但是還是用了 read 這個(gè)系統(tǒng)調(diào)用,使用系統(tǒng)調(diào)用時(shí)會(huì)從用戶態(tài)切換到內(nèi)核態(tài)。而大多數(shù)情況下我們調(diào)用 read,然后陷入內(nèi)核態(tài),內(nèi)核發(fā)現(xiàn)這個(gè)描述符沒(méi)有準(zhǔn)備好,然后切換回用戶態(tài)并且只得到 EAGAIN (errorno 被設(shè)置為 35),做的是無(wú)用功。描述符非常多的時(shí)候,每次的切換過(guò)程就是巨大的浪費(fèi)。

所以,需要 I/O 多路復(fù)用。I/O 多路復(fù)用通過(guò)使用一個(gè)系統(tǒng)函數(shù),同時(shí)等待多個(gè)描述符的可讀、可寫狀態(tài)。

為了達(dá)到這個(gè)目的,我們需要做的是:建立一個(gè)描述符列表,以及我們分別關(guān)心它們的什么事件(可讀還是可寫還是發(fā)生例外情況);調(diào)用一個(gè)系統(tǒng)函數(shù),直到這個(gè)描述符列表里有至少一個(gè)描述符關(guān)聯(lián)的事件發(fā)生時(shí),這個(gè)函數(shù)才會(huì)返回。

select, poll, epoll 就是這樣的系統(tǒng)函數(shù)。

select,poll,epoll 源碼分析

select

我們可以在所有 POSIX 兼容的系統(tǒng)里使用 select 函數(shù)來(lái)進(jìn)行 I/O 多路復(fù)用。我們需要通過(guò) select 函數(shù)的參數(shù)傳遞給內(nèi)核的信息有:

  • 我們關(guān)心哪些描述符
  • 我們關(guān)心它們的什么事件
  • 我們希望等待多長(zhǎng)時(shí)間

select 的返回時(shí),內(nèi)核會(huì)告訴我們:

  • 可讀的描述符的個(gè)數(shù)
  • 哪些描述符發(fā)生了哪些事件
<pre>123456</pre> <pre>#include <sys/select.h>int select(int maxfdp1, fd_set* readfds, fd_set* writefds, fd_set* exceptfds, struct timeval* timeout);// 返回值: 已就緒的描述符的個(gè)數(shù)。超時(shí)時(shí)為 0 ,錯(cuò)誤時(shí)為 -1</pre>

maxfdp1 意思是 “max file descriptor plus 1” ,就是把你要監(jiān)視的所有文件描述符里最大的那個(gè)加上 1 。(它實(shí)際上決定了內(nèi)核要遍歷文件描述符的次數(shù),比如你監(jiān)視了文件描述符 5 和 20 并把 maxfdp1 設(shè)置為 21 ,內(nèi)核每次都會(huì)從描述符 0 依次檢查到 20。)

中間的三個(gè)參數(shù)是你想監(jiān)視的文件描述符的集合。可以把 fd_set 類型視為 1024 位的二進(jìn)制數(shù),這意味著 select 只能監(jiān)視小于 1024 的文件描述符(1024 是由 Linux 的 sys/select.h 里 FD_SETSIZE 宏設(shè)置的值)。在 select 返回后我們通過(guò) FD_ISSET 來(lái)判斷代表該位的描述符是否是已準(zhǔn)備好的狀態(tài)。

最后一個(gè)參數(shù)是等待超時(shí)的時(shí)長(zhǎng):到達(dá)這個(gè)時(shí)長(zhǎng)但是沒(méi)有任一描述符可用時(shí),函數(shù)會(huì)返回 0 。

用一個(gè)代碼片段來(lái)展示 select 的用法:

<pre>12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455</pre> <pre>// 這個(gè)例子要監(jiān)控文件描述符 3, 4 的可讀狀態(tài),以及 4, 5 的可寫狀態(tài)// 初始化兩個(gè) fd_set 以及 timevalfd_set read_set, write_set;FD_ZERO(read_set);FD_ZERO(write_set);timeval t;t.tv_sec = 5; // 超時(shí)為 5 秒t.tv_usec = 0; // 加 0 微秒// 設(shè)置好兩個(gè) fd_setint fd1 = 3;int fd2 = 4;int fd3 = 5;int maxfdp1 = 5 + 1;FD_SET(fd1, &read_set);FD_SET(fd2, &read_set);FD_SET(fd2, &write_set);FD_SET(fd3, &write_set);// 準(zhǔn)備備用的 fd_setfd_set r_temp = read_set;fd_set w_temp = write_set;while(true){ // 每次都要重新設(shè)置放入 select 的 fd_set read_set = r_temp; write_set = w_temp; // 使用 select int n = select(maxfdp1, &read_set, &write_set, NULL, &t); // 上面的 select 函數(shù)會(huì)一直阻塞,直到 // 3, 4 可讀以及 4, 5 可寫這四件事中至少一項(xiàng)發(fā)生 // 或者等待時(shí)間到達(dá) 5 秒,返回 0 for(int i=0; i<maxfdp1 && n>0; i++){ if(FD_ISSET(i, &read_set)){ n--; if(i==fd1) prinf("描述符 3 可讀"); if(i==fd2) prinf("描述符 4 可讀"); } if(FD_ISSET(i, &write_set)){ n--; if(i==fd2) prinf("描述符 3 可寫"); if(i==fd3) prinf("描述符 4 可寫"); } } // 上面的 printf 語(yǔ)句換成對(duì)應(yīng)的 read 或者 write 函數(shù)就 // 可以立即讀取或者寫入相應(yīng)的描述符而不用等待}</pre>

可以看到,select 的缺點(diǎn)有:

  • 默認(rèn)能監(jiān)視的文件描述符不能大于 1024,也代表監(jiān)視的總數(shù)不超過(guò)1024。即使你因?yàn)樾枰O(jiān)視的描述符大于 1024 而改動(dòng)內(nèi)核的 FD_SETSIZE 值,但由于 select 是每次都會(huì)線性掃描整個(gè)fd_set,集合越大速度越慢,所以性能會(huì)比較差。
  • select 函數(shù)返回時(shí)只能看見已準(zhǔn)備好的描述符數(shù)量,至于是哪個(gè)描述符準(zhǔn)備好了需要循環(huán)用 FD_ISSET 來(lái)檢查,當(dāng)未準(zhǔn)備好的描述符很多而準(zhǔn)備好的很少時(shí),效率比較低。
  • select 函數(shù)每次執(zhí)行的時(shí)候,都把參數(shù)里傳入的三個(gè) fd_set 從用戶空間復(fù)制到內(nèi)核空間。而每次 fd_set 里要監(jiān)視的描述符變化不大時(shí),全部重新復(fù)制一遍并不劃算。同樣在每次都是未準(zhǔn)備好的描述符很多而準(zhǔn)備好的很少時(shí),調(diào)用 select 會(huì)很頻繁,用戶/內(nèi)核間的的數(shù)據(jù)復(fù)制就成了一個(gè)大的開銷。

還有一個(gè)問(wèn)題是在代碼的寫法上給我一些困擾的,就是每次調(diào)用 select 前必須重新設(shè)置三個(gè) fd_set。 fd_set 類型只是 1024 位的二進(jìn)制數(shù)(實(shí)際上結(jié)構(gòu)體里是幾個(gè) long 變量的數(shù)組;比如 64 位機(jī)器上 long 是 64 bit,那么 fd_set 里就是 16 個(gè) long 變量的數(shù)組),由一位的 1 和 0 代表一個(gè)文件描述符的狀態(tài),但是其實(shí)調(diào)用 select 前后位的 1/0 狀態(tài)意義是不一樣的。

先講一下幾個(gè)對(duì) fd_set 操作的函數(shù)的作用:FD_ZERO 把 fd_set 所有位設(shè)置為 0 ;FD_SET 把一個(gè)位設(shè)置為 1 ;FD_ISSET 判斷一個(gè)位是否為 1 。

調(diào)用 select 前:我們用 FD_ZERO 把 fd_set 先全部初始化,然后用 FD_SET 把我們關(guān)心的代表描述符的位設(shè)置為 1 。我們這時(shí)可以用 FD_ISSET 判斷這個(gè)位是否被我們?cè)O(shè)置,這時(shí)的含義是我們想要監(jiān)視的描述符是否被設(shè)置為被監(jiān)視的狀態(tài)。

調(diào)用 select 時(shí):內(nèi)核判斷 fd_set 里的位并把各個(gè) fd_set 里所有值為 1 的位記錄下來(lái),然后把 fd_set 全部設(shè)置成 0 ;一個(gè)描述符上有對(duì)應(yīng)的事件發(fā)生時(shí),把對(duì)應(yīng) fd_set 里代表這個(gè)描述符的位設(shè)置為 1 。

在 select 返回之后:我們同樣用 FD_ISSET 判斷各個(gè)我們關(guān)心的位是 0 還是 1 ,這時(shí)的含義是,這個(gè)位是否是發(fā)生了我們關(guān)心的事件。

所以,在下一次調(diào)用 select 前,我們不得不把已經(jīng)被內(nèi)核改掉的 fd_set 全部重新設(shè)置一下。

select 在監(jiān)視大量描述符尤其是更多的描述符未準(zhǔn)備好的情況時(shí)性能很差。《Unix 高級(jí)編程》里寫,用 select 的程序通常只使用 3 到 10 個(gè)描述符。

poll

poll 和 select 是相似的,只是給的接口不同。

<pre>1234</pre> <pre>#include <poll.h>int poll(struct pollfd fdarray[], nfds_t nfds, int timeout);// 返回值: 已就緒的描述符的個(gè)數(shù)。超時(shí)時(shí)為 0 ,錯(cuò)誤時(shí)為 -1</pre>

fdarraypollfd 的數(shù)組。pollfd 結(jié)構(gòu)體是這樣的:

<pre>12345</pre> <pre>struct pollfd { int fd; // 文件描述符 short events; // 我期待的事件 short revents; // 實(shí)際發(fā)生的事件:我期待的事件中發(fā)生的;或者異常情況};</pre>

nfdsfdarray 的長(zhǎng)度,也就是 pollfd 的個(gè)數(shù)。

timeout 代表等待超時(shí)的毫秒數(shù)。

相比 select ,poll 有這些優(yōu)點(diǎn):由于 poll 在 pollfd 里用 int fd 來(lái)表示文件描述符而不像 select 里用的 fd_set 來(lái)分別表示描述符,所以沒(méi)有必須小于 1024 的限制,也沒(méi)有數(shù)量限制;由于 poll 用 events 表示期待的事件,通過(guò)修改 revents 來(lái)表示發(fā)生的事件,所以不需要像 select 在每次調(diào)用前重新設(shè)置描述符和期待的事件。

除此之外,poll 和 select 幾乎相同。在 poll 返回后,需要遍歷 fdarray 來(lái)檢查各個(gè) pollfd 里的 revents 是否發(fā)生了期待的事件;每次調(diào)用 poll 時(shí),把 fdarray 復(fù)制到內(nèi)核空間。在描述符太多而每次準(zhǔn)備好的較少時(shí),poll 有同樣的性能問(wèn)題。

epoll

epoll 是在 Linux 2.5.44 中首度登場(chǎng)的。不像 select 和 poll ,它提供了三個(gè)系統(tǒng)函數(shù)而不是一個(gè)。

epoll_create 用來(lái)創(chuàng)建一個(gè) epoll 描述符:

<pre>1234</pre> <pre>#include <sys/epoll.h>int epoll_create(int size);// 返回值:epoll 描述符</pre>

size 用來(lái)告訴內(nèi)核你想監(jiān)視的文件描述符的數(shù)目,但是它并不是限制了能監(jiān)視的描述符的最大個(gè)數(shù),而是給內(nèi)核最初分配的空間一個(gè)建議。然后系統(tǒng)會(huì)在內(nèi)核中分配一個(gè)空間來(lái)存放事件表,并返回一個(gè) epoll 描述符,用來(lái)操作這個(gè)事件表。

epoll_ctl 用來(lái)增/刪/改內(nèi)核中的事件表:

<pre>123</pre> <pre>int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);// 返回值:成功時(shí)返回 0 ,失敗時(shí)返回 -1</pre>

epfd 是 epoll 描述符。

op 是操作類型(增加/刪除/修改)。

fd 是希望監(jiān)視的文件描述符。

event 是一個(gè) epoll_event 結(jié)構(gòu)體的指針。epoll_event 的定義是這樣的:

<pre>1234567891011</pre> <pre>typedef union epoll_data { void *ptr; int fd; uint32_t u32; uint64_t u64;} epoll_data_t;struct epoll_event { uint32_t events; // 我期待的事件 epoll_data_t data; // 用戶數(shù)據(jù)變量};</pre>

這個(gè)結(jié)構(gòu)體里,除了期待的事件外,還有一個(gè) data ,是一個(gè) union,它是用來(lái)讓我們?cè)诘玫较旅娴谌齻€(gè)函數(shù)的返回值以后方便的定位文件描述符的。

epoll_wait 用來(lái)等待事件

<pre>1234</pre> <pre>int epoll_wait(int epfd, struct epoll_event *result_events, int maxevents, int timeout);// 返回值:已就緒的描述符個(gè)數(shù)。超時(shí)時(shí)為 0 ,錯(cuò)誤時(shí)為 -1</pre>

epfd 是 epoll 描述符。

result_events 是 epoll_event 結(jié)構(gòu)體的指針,它將指向的是所有已經(jīng)準(zhǔn)備好的事件描述符相關(guān)聯(lián)的 epoll_event(在上個(gè)步驟里調(diào)用 epoll_ctl 時(shí)關(guān)聯(lián)起來(lái)的)。下面的例子可以讓你知道這個(gè)參數(shù)的意義。

maxevents 是返回的最大事件個(gè)數(shù),也就是你能通過(guò) result_events 指針遍歷到的最大的次數(shù)。

timeout 是等待超時(shí)的毫秒數(shù)。

用一個(gè)代碼片段來(lái)展示 epoll 的用法:

<pre>123456789101112131415161718192021222324252627282930313233343536373839404142434445</pre> <pre>// 這個(gè)例子要監(jiān)控文件描述符 3, 4 的可讀狀態(tài),以及 4, 5 的可寫狀態(tài)/* 通過(guò) epoll_create 創(chuàng)建 epoll 描述符 /int epfd = epoll_create(4);int fd1 = 3;int fd2 = 4;int fd3 = 5;/ 通過(guò) epoll_ctl 注冊(cè)好四個(gè)事件 /struct epoll_event ev1;ev1.events = EPOLLIN; // 期待它的可讀事件發(fā)生ev1.data = fd1; // 我們通常就把 data 設(shè)置為 fd ,方便以后查看epoll_ctl(epfd, EPOLL_CTL_ADD, fd1, &ev1); // 添加到事件表struct epoll_event ev2;ev2.events = EPOLLIN;ev2.data = fd2;epoll_ctl(epfd, EPOLL_CTL_ADD, fd2, &ev2);struct epoll_event ev3;ev3.events = EPOLLOUT; // 期待它的可寫事件發(fā)生ev3.data = fd2;epoll_ctl(epfd, EPOLL_CTL_ADD, fd2, &ev3);struct epoll_event ev4;ev4.events = EPOLLOUT;ev4.data = fd3;epoll_ctl(epfd, EPOLL_CTL_ADD, fd3, &ev4);/ 通過(guò) epoll_wait 等待事件 */# DEFINE MAXEVENTS 4struct epoll_event result_events[MAXEVENTS];while(true){ int n = epoll_wait(epfd, &result_events, MAXEVENTS, 5000); for(int i=0; i<n; n--){ // result_events[i] 一定是 ev1 到 ev4 中的一個(gè) if(result_events[i].events&EPOLLIN) printf("描述符 %d 可讀", result_events[i].fd); else if(result_events[i].events&EPOLLOUT) printf("描述符 %d 可寫", result_events[i].fd) }}</pre>

所以 epoll 解決了 poll 和 select 的問(wèn)題:

  • 只在 epoll_ctl 的時(shí)候把數(shù)據(jù)復(fù)制到內(nèi)核空間,這保證了每個(gè)描述符和事件一定只會(huì)被復(fù)制到內(nèi)核空間一次;每次調(diào)用 epoll_wait 都不會(huì)復(fù)制新數(shù)據(jù)到內(nèi)核空間。相比之下,select 每次調(diào)用都會(huì)把三個(gè) fd_set 復(fù)制一遍;poll 每次調(diào)用都會(huì)把 fdarray 復(fù)制一遍。

  • epoll_wait 返回 n ,那么只需要做 n 次循環(huán),可以保證遍歷的每一次都是有意義的。相比之下,select 需要做至少 n 次至多 maxfdp1 次循環(huán);poll 需要遍歷完 fdarray 即做 nfds 次循環(huán)。

  • 在內(nèi)部實(shí)現(xiàn)上,epoll 使用了回調(diào)的方法。調(diào)用 epoll_ctl 時(shí),就是注冊(cè)了一個(gè)事件:在集合中放入文件描述符以及事件數(shù)據(jù),并且加上一個(gè)回調(diào)函數(shù)。一旦文件描述符上的對(duì)應(yīng)事件發(fā)生,就會(huì)調(diào)用回調(diào)函數(shù),這個(gè)函數(shù)會(huì)把這個(gè)文件描述符加入到就緒隊(duì)列上。當(dāng)你調(diào)用 epoll_wait 時(shí),它只是在查看就緒隊(duì)列上是否有內(nèi)容,有的話就返回給你的程序。select() poll()``epoll_wait() 三個(gè)函數(shù)在操作系統(tǒng)看來(lái),都是睡眠一會(huì)兒然后判斷一會(huì)兒的循環(huán),但是 select 和 poll 在醒著的時(shí)候要遍歷整個(gè)文件描述符集合,而 epoll_wait 只是看看就緒隊(duì)列是否為空而已。這是 epoll 高性能的理由,使得其 I/O 的效率不會(huì)像使用輪詢的 select/poll 隨著描述符增加而大大降低。

注 1 :select/poll/epoll_wait 三個(gè)函數(shù)的等待超時(shí)時(shí)間都有一樣的特性:等待時(shí)間設(shè)置為 0 時(shí)函數(shù)不阻塞而是立即返回,不論是否有文件描述符已準(zhǔn)備好;poll/epoll_wait 中的 timeout 為 -1,select 中的 timeout 為 NULL 時(shí),則無(wú)限等待,直到有描述符已準(zhǔn)備好才會(huì)返回。

注 2 :有的新手會(huì)把文件描述符是否標(biāo)記為阻塞 I/O 等同于 I/O 多路復(fù)用函數(shù)是否阻塞。其實(shí)文件描述符是否標(biāo)記為阻塞,決定了你 readwrite 它時(shí)如果它未準(zhǔn)備好是阻塞等待,還是立即返回 EAGAIN ;而 I/O 多路復(fù)用函數(shù)除非你把 timeout 設(shè)置為 0 ,否則它總是會(huì)阻塞住你的程序。

注 3 :上面的例子只是入門,可能是不準(zhǔn)確或不全面的:一是數(shù)據(jù)要立即處理防止丟失;二是 EPOLLIN/EPOLLOUT 不完全等同于可讀可寫事件,具體要去搜索 poll/epoll 的事件具體有哪些;三是大多數(shù)實(shí)際例子里,比如一個(gè) tcp server ,都會(huì)在運(yùn)行中不斷增加/刪除的文件描述符而不是記住固定的 3 4 5 幾個(gè)描述符(用這種例子更能看出 epoll 的優(yōu)勢(shì));四是 epoll 的優(yōu)勢(shì)更多的體現(xiàn)在處理大量閑連接的情況,如果場(chǎng)景是處理少量短連接,用 select 反而更好,而且用 select 的代碼能運(yùn)行在所有平臺(tái)上。

Epoll數(shù)據(jù)結(jié)構(gòu):

select()和poll() IO多路復(fù)用模型

select的缺點(diǎn):

  1. 單個(gè)進(jìn)程能夠監(jiān)視的文件描述符的數(shù)量存在最大限制,通常是1024,當(dāng)然可以更改數(shù)量,但由于select采用輪詢的方式掃描文件描述符,文件描述符數(shù)量越多,性能越差;(在linux內(nèi)核頭文件中,有這樣的定義:#define __FD_SETSIZE 1024)
  2. 內(nèi)核 / 用戶空間內(nèi)存拷貝問(wèn)題,select需要復(fù)制大量的句柄數(shù)據(jù)結(jié)構(gòu),產(chǎn)生巨大的開銷;
  3. select返回的是含有整個(gè)句柄的數(shù)組,應(yīng)用程序需要遍歷整個(gè)數(shù)組才能發(fā)現(xiàn)哪些句柄發(fā)生了事件;
  4. select的觸發(fā)方式是水平觸發(fā),應(yīng)用程序如果沒(méi)有完成對(duì)一個(gè)已經(jīng)就緒的文件描述符進(jìn)行IO操作,那么之后每次select調(diào)用還是會(huì)將這些文件描述符通知進(jìn)程。

相比select模型,poll使用鏈表保存文件描述符,因此沒(méi)有了監(jiān)視文件數(shù)量的限制,但其他三個(gè)缺點(diǎn)依然存在。

拿select模型為例,假設(shè)我們的服務(wù)器需要支持100萬(wàn)的并發(fā)連接,則在__FD_SETSIZE 為1024的情況下,則我們至少需要開辟1k個(gè)進(jìn)程才能實(shí)現(xiàn)100萬(wàn)的并發(fā)連接。除了進(jìn)程間上下文切換的時(shí)間消耗外,從內(nèi)核/用戶空間大量的無(wú)腦內(nèi)存拷貝、數(shù)組輪詢等,是系統(tǒng)難以承受的。因此,基于select模型的服務(wù)器程序,要達(dá)到10萬(wàn)級(jí)別的并發(fā)訪問(wèn),是一個(gè)很難完成的任務(wù)。

因此,該epoll上場(chǎng)了。

epoll IO多路復(fù)用模型實(shí)現(xiàn)機(jī)制

由于epoll的實(shí)現(xiàn)機(jī)制與select/poll機(jī)制完全不同,上面所說(shuō)的 select的缺點(diǎn)在epoll上不復(fù)存在。

設(shè)想一下如下場(chǎng)景:有100萬(wàn)個(gè)客戶端同時(shí)與一個(gè)服務(wù)器進(jìn)程保持著TCP連接。而每一時(shí)刻,通常只有幾百上千個(gè)TCP連接是活躍的(事實(shí)上大部分場(chǎng)景都是這種情況)。如何實(shí)現(xiàn)這樣的高并發(fā)?

在select/poll時(shí)代,服務(wù)器進(jìn)程每次都把這100萬(wàn)個(gè)連接告訴操作系統(tǒng)(從用戶態(tài)復(fù)制句柄數(shù)據(jù)結(jié)構(gòu)到內(nèi)核態(tài)),讓操作系統(tǒng)內(nèi)核去查詢這些套接字上是否有事件發(fā)生,輪詢完后,再將句柄數(shù)據(jù)復(fù)制到用戶態(tài),讓服務(wù)器應(yīng)用程序輪詢處理已發(fā)生的網(wǎng)絡(luò)事件,這一過(guò)程資源消耗較大,因此,select/poll一般只能處理幾千的并發(fā)連接。

epoll的設(shè)計(jì)和實(shí)現(xiàn)與select完全不同。epoll通過(guò)在Linux內(nèi)核中申請(qǐng)一個(gè)簡(jiǎn)易的文件系統(tǒng)(文件系統(tǒng)一般用什么數(shù)據(jù)結(jié)構(gòu)實(shí)現(xiàn)?B+樹)。把原先的select/poll調(diào)用分成了3個(gè)部分:

1)調(diào)用epoll_create()建立一個(gè)epoll對(duì)象(在epoll文件系統(tǒng)中為這個(gè)句柄對(duì)象分配資源)

2)調(diào)用epoll_ctl向epoll對(duì)象中添加這100萬(wàn)個(gè)連接的套接字

3)調(diào)用epoll_wait收集發(fā)生的事件的連接

如此一來(lái),要實(shí)現(xiàn)上面說(shuō)是的場(chǎng)景,只需要在進(jìn)程啟動(dòng)時(shí)建立一個(gè)epoll對(duì)象,然后在需要的時(shí)候向這個(gè)epoll對(duì)象中添加或者刪除連接。同時(shí),epoll_wait的效率也非常高,因?yàn)檎{(diào)用epoll_wait時(shí),并沒(méi)有一股腦的向操作系統(tǒng)復(fù)制這100萬(wàn)個(gè)連接的句柄數(shù)據(jù),內(nèi)核也不需要去遍歷全部的連接。

下面來(lái)看看Linux內(nèi)核具體的epoll機(jī)制實(shí)現(xiàn)思路。

當(dāng)某一進(jìn)程調(diào)用epoll_create方法時(shí),Linux內(nèi)核會(huì)創(chuàng)建一個(gè)eventpoll結(jié)構(gòu)體,這個(gè)結(jié)構(gòu)體中有兩個(gè)成員與epoll的使用方式密切相關(guān)。eventpoll結(jié)構(gòu)體如下所示:

[cpp] view plain copy

  1. struct eventpoll{
  2.  ....  
    
  3.  /*紅黑樹的根節(jié)點(diǎn),這顆樹中存儲(chǔ)著所有添加到epoll中的需要監(jiān)控的事件*/  
    
  4.  struct rb_root  rbr;  
    
  5.  /*雙鏈表中則存放著將要通過(guò)epoll_wait返回給用戶的滿足條件的事件*/  
    
  6.  struct list_head rdlist;  
    
  7.  ....  
    
  8. };

每一個(gè)epoll對(duì)象都有一個(gè)獨(dú)立的eventpoll結(jié)構(gòu)體,用于存放通過(guò)epoll_ctl方法向epoll對(duì)象中添加進(jìn)來(lái)的事件。這些事件都會(huì)掛載在紅黑樹中,如此,重復(fù)添加的事件就可以通過(guò)紅黑樹而高效的識(shí)別出來(lái)(紅黑樹的插入時(shí)間效率是lgn,其中n為樹的高度)。

而所有添加到epoll中的事件都會(huì)與設(shè)備(網(wǎng)卡)驅(qū)動(dòng)程序建立回調(diào)關(guān)系,也就是說(shuō),當(dāng)相應(yīng)的事件發(fā)生時(shí)會(huì)調(diào)用這個(gè)回調(diào)方法。這個(gè)回調(diào)方法在內(nèi)核中叫ep_poll_callback,它會(huì)將發(fā)生的事件添加到rdlist雙鏈表中。

在epoll中,對(duì)于每一個(gè)事件,都會(huì)建立一個(gè)epitem結(jié)構(gòu)體,如下所示:

[cpp] view plain copy

  1. struct epitem{
  2.  struct rb_node  rbn;//紅黑樹節(jié)點(diǎn)  
    
  3.  struct list_head    rdllink;//雙向鏈表節(jié)點(diǎn)  
    
  4.  struct epoll_filefd  ffd;  //事件句柄信息  
    
  5.  struct eventpoll *ep;    //指向其所屬的eventpoll對(duì)象  
    
  6.  struct epoll_event event; //期待發(fā)生的事件類型  
    
  7. }

當(dāng)調(diào)用epoll_wait檢查是否有事件發(fā)生時(shí),只需要檢查eventpoll對(duì)象中的rdlist雙鏈表中是否有epitem元素即可。如果rdlist不為空,則把發(fā)生的事件復(fù)制到用戶態(tài),同時(shí)將事件數(shù)量返回給用戶。

epoll數(shù)據(jù)結(jié)構(gòu)示意圖

從上面的講解可知:通過(guò)紅黑樹和雙鏈表數(shù)據(jù)結(jié)構(gòu),并結(jié)合回調(diào)機(jī)制,造就了epoll的高效。

OK,講解完了Epoll的機(jī)理,我們便能很容易掌握epoll的用法了。一句話描述就是:三步曲。

第一步:epoll_create()系統(tǒng)調(diào)用。此調(diào)用返回一個(gè)句柄,之后所有的使用都依靠這個(gè)句柄來(lái)標(biāo)識(shí)。

第二步:epoll_ctl()系統(tǒng)調(diào)用。通過(guò)此調(diào)用向epoll對(duì)象中添加、刪除、修改感興趣的事件,返回0標(biāo)識(shí)成功,返回-1表示失敗。

第三部:epoll_wait()系統(tǒng)調(diào)用。通過(guò)此調(diào)用收集收集在epoll監(jiān)控中已經(jīng)發(fā)生的事件。

epoll實(shí)例

最后,附上一個(gè)epoll編程實(shí)例。

幾乎所有的epoll程序都使用下面的框架:

[cpp] view plaincopyprint?

  1. for( ; ; )
  2. {  
    
  3.     nfds = epoll_wait(epfd,events,20,500);  
    
  4.     for(i=0;i<nfds;++i)  < span="" >
    
  5.     {  
    
  6.         if(events[i].data.fd==listenfd) //有新的連接  
    
  7.         {  
    
  8.             connfd = accept(listenfd,(sockaddr *)&clientaddr, &clilen); //accept這個(gè)連接  
    
  9.             ev.data.fd=connfd;  
    
  10.             ev.events=EPOLLIN|EPOLLET;  
    
  11.             epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&ev); //將新的fd添加到epoll的監(jiān)聽隊(duì)列中  
    
  12.         }  
    
  13.         else if( events[i].events&EPOLLIN ) //接收到數(shù)據(jù),讀socket  
    
  14.         {  
    
  15.             n = read(sockfd, line, MAXLINE)) < 0    //讀  
    
  16.             ev.data.ptr = md;     //md為自定義類型,添加數(shù)據(jù)  
    
  17.             ev.events=EPOLLOUT|EPOLLET;  
    
  18.             epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);//修改標(biāo)識(shí)符,等待下一個(gè)循環(huán)時(shí)發(fā)送數(shù)據(jù),異步處理的精髓  
    
  19.         }  
    
  20.         else if(events[i].events&EPOLLOUT) //有數(shù)據(jù)待發(fā)送,寫socket  
    
  21.         {  
    
  22.             struct myepoll_data* md = (myepoll_data*)events[i].data.ptr;    //取數(shù)據(jù)  
    
  23.             sockfd = md->fd;  
    
  24.             send( sockfd, md->ptr, strlen((char*)md->ptr), 0 );        //發(fā)送數(shù)據(jù)  
    
  25.             ev.data.fd=sockfd;  
    
  26.             ev.events=EPOLLIN|EPOLLET;  
    
  27.             epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev); //修改標(biāo)識(shí)符,等待下一個(gè)循環(huán)時(shí)接收數(shù)據(jù)  
    
  28.         }  
    
  29.         else  
    
  30.         {  
    
  31.             //其他的處理  
    
  32.         }  
    
  33.     }  
    
  34. }  
    

epoll的程序?qū)嵗?/h2>

[cpp] view plaincopyprint?

  1. include

  2. include

  3. include

  4. include

  5. include

  6. include

  7. include

  8. include

  9. include

  10. define MAXEVENTS 64

  11. //函數(shù):
  12. //功能:創(chuàng)建和綁定一個(gè)TCP socket
  13. //參數(shù):端口
  14. //返回值:創(chuàng)建的socket
  15. static int
  16. create_and_bind (char *port)
  17. {
  18. struct addrinfo hints;
  19. struct addrinfo *result, *rp;
  20. int s, sfd;
  21. memset (&hints, 0, sizeof (struct addrinfo));
  22. hints.ai_family = AF_UNSPEC; /* Return IPv4 and IPv6 choices */
  23. hints.ai_socktype = SOCK_STREAM; /* We want a TCP socket */
  24. hints.ai_flags = AI_PASSIVE; /* All interfaces */
  25. s = getaddrinfo (NULL, port, &hints, &result);
  26. if (s != 0)
  27.  {  
    
  28.    fprintf (stderr, "getaddrinfo: %s\n", gai_strerror (s));  
    
  29.    return -1;  
    
  30.  }  
    
  31. for (rp = result; rp != NULL; rp = rp->ai_next)
  32.  {  
    
  33.    sfd = socket (rp->ai_family, rp->ai_socktype, rp->ai_protocol);  
    
  34.    if (sfd == -1)  
    
  35.      continue;  
    
  36.    s = bind (sfd, rp->ai_addr, rp->ai_addrlen);  
    
  37.    if (s == 0)  
    
  38.      {  
    
  39.        /* We managed to bind successfully! */  
    
  40.        break;  
    
  41.      }  
    
  42.    close (sfd);  
    
  43.  }  
    
  44. if (rp == NULL)
  45.  {  
    
  46.    fprintf (stderr, "Could not bind\n");  
    
  47.    return -1;  
    
  48.  }  
    
  49. freeaddrinfo (result);
  50. return sfd;
  51. }
  52. //函數(shù)
  53. //功能:設(shè)置socket為非阻塞的
  54. static int
  55. make_socket_non_blocking (int sfd)
  56. {
  57. int flags, s;
  58. //得到文件狀態(tài)標(biāo)志
  59. flags = fcntl (sfd, F_GETFL, 0);
  60. if (flags == -1)
  61.  {  
    
  62.    perror ("fcntl");  
    
  63.    return -1;  
    
  64.  }  
    
  65. //設(shè)置文件狀態(tài)標(biāo)志
  66. flags |= O_NONBLOCK;
  67. s = fcntl (sfd, F_SETFL, flags);
  68. if (s == -1)
  69.  {  
    
  70.    perror ("fcntl");  
    
  71.    return -1;  
    
  72.  }  
    
  73. return 0;
  74. }
  75. //端口由參數(shù)argv[1]指定
  76. int
  77. main (int argc, char *argv[])
  78. {
  79. int sfd, s;
  80. int efd;
  81. struct epoll_event event;
  82. struct epoll_event *events;
  83. if (argc != 2)
  84.  {  
    
  85.    fprintf (stderr, "Usage: %s [port]\n", argv[0]);  
    
  86.    exit (EXIT_FAILURE);  
    
  87.  }  
    
  88. sfd = create_and_bind (argv[1]);
  89. if (sfd == -1)
  90.  abort ();  
    
  91. s = make_socket_non_blocking (sfd);
  92. if (s == -1)
  93.  abort ();  
    
  94. s = listen (sfd, SOMAXCONN);
  95. if (s == -1)
  96.  {  
    
  97.    perror ("listen");  
    
  98.    abort ();  
    
  99.  }  
    
  100. //除了參數(shù)size被忽略外,此函數(shù)和epoll_create完全相同
  101. efd = epoll_create1 (0);
  102. if (efd == -1)
  103.  {  
    
  104.    perror ("epoll_create");  
    
  105.    abort ();  
    
  106.  }  
    
  107. event.data.fd = sfd;
  108. event.events = EPOLLIN | EPOLLET;//讀入,邊緣觸發(fā)方式
  109. s = epoll_ctl (efd, EPOLL_CTL_ADD, sfd, &event);
  110. if (s == -1)
  111.  {  
    
  112.    perror ("epoll_ctl");  
    
  113.    abort ();  
    
  114.  }  
    
  115. /* Buffer where events are returned */
  116. events = calloc (MAXEVENTS, sizeof event);
  117. /* The event loop */
  118. while (1)
  119.  {  
    
  120.    int n, i;  
    
  121.    n = epoll_wait (efd, events, MAXEVENTS, -1);  
    
  122.    for (i = 0; i < n; i++)  
    
  123.      {  
    
  124.        if ((events[i].events & EPOLLERR) ||  
    
  125.            (events[i].events & EPOLLHUP) ||  
    
  126.            (!(events[i].events & EPOLLIN)))  
    
  127.          {  
    
  128.            /* An error has occured on this fd, or the socket is not 
    
  129.               ready for reading (why were we notified then?) */  
    
  130.            fprintf (stderr, "epoll error\n");  
    
  131.            close (events[i].data.fd);  
    
  132.            continue;  
    
  133.          }  
    
  134.        else if (sfd == events[i].data.fd)  
    
  135.          {  
    
  136.            /* We have a notification on the listening socket, which 
    
  137.               means one or more incoming connections. */  
    
  138.            while (1)  
    
  139.              {  
    
  140.                struct sockaddr in_addr;  
    
  141.                socklen_t in_len;  
    
  142.                int infd;  
    
  143.                char hbuf[NI_MAXHOST], sbuf[NI_MAXSERV];  
    
  144.                in_len = sizeof in_addr;  
    
  145.                infd = accept (sfd, &in_addr, &in_len);  
    
  146.                if (infd == -1)  
    
  147.                  {  
    
  148.                    if ((errno == EAGAIN) ||  
    
  149.                        (errno == EWOULDBLOCK))  
    
  150.                      {  
    
  151.                        /* We have processed all incoming 
    
  152.                           connections. */  
    
  153.                        break;  
    
  154.                      }  
    
  155.                    else  
    
  156.                      {  
    
  157.                        perror ("accept");  
    
  158.                        break;  
    
  159.                      }  
    
  160.                  }  
    
  161.                                //將地址轉(zhuǎn)化為主機(jī)名或者服務(wù)名  
    
  162.                s = getnameinfo (&in_addr, in_len,  
    
  163.                                 hbuf, sizeof hbuf,  
    
  164.                                 sbuf, sizeof sbuf,  
    
  165.                                 NI_NUMERICHOST | NI_NUMERICSERV);//flag參數(shù):以數(shù)字名返回  
    
  166.                                //主機(jī)地址和服務(wù)地址  
    
  167.                if (s == 0)  
    
  168.                  {  
    
  169.                    printf("Accepted connection on descriptor %d "  
    
  170.                           "(host=%s, port=%s)\n", infd, hbuf, sbuf);  
    
  171.                  }  
    
  172.                /* Make the incoming socket non-blocking and add it to the 
    
  173.                   list of fds to monitor. */  
    
  174.                s = make_socket_non_blocking (infd);  
    
  175.                if (s == -1)  
    
  176.                  abort ();  
    
  177.                event.data.fd = infd;  
    
  178.                event.events = EPOLLIN | EPOLLET;  
    
  179.                s = epoll_ctl (efd, EPOLL_CTL_ADD, infd, &event);  
    
  180.                if (s == -1)  
    
  181.                  {  
    
  182.                    perror ("epoll_ctl");  
    
  183.                    abort ();  
    
  184.                  }  
    
  185.              }  
    
  186.            continue;  
    
  187.          }  
    
  188.        else  
    
  189.          {  
    
  190.            /* We have data on the fd waiting to be read. Read and 
    
  191.               display it. We must read whatever data is available 
    
  192.               completely, as we are running in edge-triggered mode 
    
  193.               and won't get a notification again for the same 
    
  194.               data. */  
    
  195.            int done = 0;  
    
  196.            while (1)  
    
  197.              {  
    
  198.                ssize_t count;  
    
  199.                char buf[512];  
    
  200.                count = read (events[i].data.fd, buf, sizeof(buf));  
    
  201.                if (count == -1)  
    
  202.                  {  
    
  203.                    /* If errno == EAGAIN, that means we have read all 
    
  204.                       data. So go back to the main loop. */  
    
  205.                    if (errno != EAGAIN)  
    
  206.                      {  
    
  207.                        perror ("read");  
    
  208.                        done = 1;  
    
  209.                      }  
    
  210.                    break;  
    
  211.                  }  
    
  212.                else if (count == 0)  
    
  213.                  {  
    
  214.                    /* End of file. The remote has closed the 
    
  215.                       connection. */  
    
  216.                    done = 1;  
    
  217.                    break;  
    
  218.                  }  
    
  219.                /* Write the buffer to standard output */  
    
  220.                s = write (1, buf, count);  
    
  221.                if (s == -1)  
    
  222.                  {  
    
  223.                    perror ("write");  
    
  224.                    abort ();  
    
  225.                  }  
    
  226.              }  
    
  227.            if (done)  
    
  228.              {  
    
  229.                printf ("Closed connection on descriptor %d\n",  
    
  230.                        events[i].data.fd);  
    
  231.                /* Closing the descriptor will make epoll remove it 
    
  232.                   from the set of descriptors which are monitored. */  
    
  233.                close (events[i].data.fd);  
    
  234.              }  
    
  235.          }  
    
  236.      }  
    
  237.  }  
    
  238. free (events);
  239. close (sfd);
  240. return EXIT_SUCCESS;
  241. }

微信公眾號(hào)【Java技術(shù)江湖】一位阿里 Java 工程師的技術(shù)小站。(關(guān)注公眾號(hào)后回復(fù)”Java“即可領(lǐng)取 Java基礎(chǔ)、進(jìn)階、項(xiàng)目和架構(gòu)師等免費(fèi)學(xué)習(xí)資料,更有數(shù)據(jù)庫(kù)、分布式、微服務(wù)等熱門技術(shù)學(xué)習(xí)視頻,內(nèi)容豐富,兼顧原理和實(shí)踐,另外也將贈(zèng)送作者原創(chuàng)的Java學(xué)習(xí)指南、Java程序員面試指南等干貨資源)

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 看到網(wǎng)上有不少討論epoll,但大多不夠詳細(xì)準(zhǔn)確,以前面試有被問(wèn)到這個(gè)問(wèn)題。不去更深入的了解,只能停留在知其然...
    電臺(tái)_Fang閱讀 12,148評(píng)論 0 8
  • epoll概述 epoll是linux中IO多路復(fù)用的一種機(jī)制,I/O多路復(fù)用就是通過(guò)一種機(jī)制,一個(gè)進(jìn)程可以監(jiān)視多...
    發(fā)仔很忙閱讀 11,088評(píng)論 4 35
  • 一、epoll簡(jiǎn)介 epoll是當(dāng)前在Linux下開發(fā)大規(guī)模并發(fā)網(wǎng)絡(luò)程序的熱門選擇,epoll在Linux2.6內(nèi)...
    金星show閱讀 3,736評(píng)論 0 3
  • 本文摘抄自linux基礎(chǔ)編程 IO概念 Linux的內(nèi)核將所有外部設(shè)備都可以看做一個(gè)文件來(lái)操作。那么我們對(duì)與外部設(shè)...
    VD2012閱讀 1,067評(píng)論 0 2
  • 同步IO和異步IO,阻塞IO和非阻塞IO分別是什么,到底有什么區(qū)別?不同的人在不同的上下文下給出的答案是不同的。所...
    lxqfirst閱讀 2,220評(píng)論 0 47

友情鏈接更多精彩內(nèi)容