微信公眾號(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> |
|---|
fdarray 是 pollfd 的數(shù)組。pollfd 結(jié)構(gòu)體是這樣的:
| <pre>12345</pre> | <pre>struct pollfd { int fd; // 文件描述符 short events; // 我期待的事件 short revents; // 實(shí)際發(fā)生的事件:我期待的事件中發(fā)生的;或者異常情況};</pre> |
|---|
nfds 是 fdarray 的長(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)記為阻塞,決定了你
read或write它時(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):
- 單個(gè)進(jìn)程能夠監(jiān)視的文件描述符的數(shù)量存在最大限制,通常是1024,當(dāng)然可以更改數(shù)量,但由于select采用輪詢的方式掃描文件描述符,文件描述符數(shù)量越多,性能越差;(在linux內(nèi)核頭文件中,有這樣的定義:#define __FD_SETSIZE 1024)
- 內(nèi)核 / 用戶空間內(nèi)存拷貝問(wèn)題,select需要復(fù)制大量的句柄數(shù)據(jù)結(jié)構(gòu),產(chǎn)生巨大的開銷;
- select返回的是含有整個(gè)句柄的數(shù)組,應(yīng)用程序需要遍歷整個(gè)數(shù)組才能發(fā)現(xiàn)哪些句柄發(fā)生了事件;
- 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
- struct eventpoll{
..../*紅黑樹的根節(jié)點(diǎn),這顆樹中存儲(chǔ)著所有添加到epoll中的需要監(jiān)控的事件*/struct rb_root rbr;/*雙鏈表中則存放著將要通過(guò)epoll_wait返回給用戶的滿足條件的事件*/struct list_head rdlist;....- };
每一個(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
- struct epitem{
struct rb_node rbn;//紅黑樹節(jié)點(diǎn)struct list_head rdllink;//雙向鏈表節(jié)點(diǎn)struct epoll_filefd ffd; //事件句柄信息struct eventpoll *ep; //指向其所屬的eventpoll對(duì)象struct epoll_event event; //期待發(fā)生的事件類型- }
當(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?
- for( ; ; )
{nfds = epoll_wait(epfd,events,20,500);for(i=0;i<nfds;++i) < span="" >{if(events[i].data.fd==listenfd) //有新的連接{connfd = accept(listenfd,(sockaddr *)&clientaddr, &clilen); //accept這個(gè)連接ev.data.fd=connfd;ev.events=EPOLLIN|EPOLLET;epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&ev); //將新的fd添加到epoll的監(jiān)聽隊(duì)列中}else if( events[i].events&EPOLLIN ) //接收到數(shù)據(jù),讀socket{n = read(sockfd, line, MAXLINE)) < 0 //讀ev.data.ptr = md; //md為自定義類型,添加數(shù)據(jù)ev.events=EPOLLOUT|EPOLLET;epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);//修改標(biāo)識(shí)符,等待下一個(gè)循環(huán)時(shí)發(fā)送數(shù)據(jù),異步處理的精髓}else if(events[i].events&EPOLLOUT) //有數(shù)據(jù)待發(fā)送,寫socket{struct myepoll_data* md = (myepoll_data*)events[i].data.ptr; //取數(shù)據(jù)sockfd = md->fd;send( sockfd, md->ptr, strlen((char*)md->ptr), 0 ); //發(fā)送數(shù)據(jù)ev.data.fd=sockfd;ev.events=EPOLLIN|EPOLLET;epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev); //修改標(biāo)識(shí)符,等待下一個(gè)循環(huán)時(shí)接收數(shù)據(jù)}else{//其他的處理}}}
epoll的程序?qū)嵗?/h2>
[cpp] view plaincopyprint?
-
include
-
include
-
include
-
include
-
include
-
include
-
include
-
include
-
include
-
define MAXEVENTS 64
- //函數(shù):
- //功能:創(chuàng)建和綁定一個(gè)TCP socket
- //參數(shù):端口
- //返回值:創(chuàng)建的socket
- static int
- create_and_bind (char *port)
- {
- struct addrinfo hints;
- struct addrinfo *result, *rp;
- int s, sfd;
- memset (&hints, 0, sizeof (struct addrinfo));
- hints.ai_family = AF_UNSPEC; /* Return IPv4 and IPv6 choices */
- hints.ai_socktype = SOCK_STREAM; /* We want a TCP socket */
- hints.ai_flags = AI_PASSIVE; /* All interfaces */
- s = getaddrinfo (NULL, port, &hints, &result);
- if (s != 0)
{fprintf (stderr, "getaddrinfo: %s\n", gai_strerror (s));return -1;}- for (rp = result; rp != NULL; rp = rp->ai_next)
{sfd = socket (rp->ai_family, rp->ai_socktype, rp->ai_protocol);if (sfd == -1)continue;s = bind (sfd, rp->ai_addr, rp->ai_addrlen);if (s == 0){/* We managed to bind successfully! */break;}close (sfd);}- if (rp == NULL)
{fprintf (stderr, "Could not bind\n");return -1;}- freeaddrinfo (result);
- return sfd;
- }
- //函數(shù)
- //功能:設(shè)置socket為非阻塞的
- static int
- make_socket_non_blocking (int sfd)
- {
- int flags, s;
- //得到文件狀態(tài)標(biāo)志
- flags = fcntl (sfd, F_GETFL, 0);
- if (flags == -1)
{perror ("fcntl");return -1;}- //設(shè)置文件狀態(tài)標(biāo)志
- flags |= O_NONBLOCK;
- s = fcntl (sfd, F_SETFL, flags);
- if (s == -1)
{perror ("fcntl");return -1;}- return 0;
- }
- //端口由參數(shù)argv[1]指定
- int
- main (int argc, char *argv[])
- {
- int sfd, s;
- int efd;
- struct epoll_event event;
- struct epoll_event *events;
- if (argc != 2)
{fprintf (stderr, "Usage: %s [port]\n", argv[0]);exit (EXIT_FAILURE);}- sfd = create_and_bind (argv[1]);
- if (sfd == -1)
abort ();- s = make_socket_non_blocking (sfd);
- if (s == -1)
abort ();- s = listen (sfd, SOMAXCONN);
- if (s == -1)
{perror ("listen");abort ();}- //除了參數(shù)size被忽略外,此函數(shù)和epoll_create完全相同
- efd = epoll_create1 (0);
- if (efd == -1)
{perror ("epoll_create");abort ();}- event.data.fd = sfd;
- event.events = EPOLLIN | EPOLLET;//讀入,邊緣觸發(fā)方式
- s = epoll_ctl (efd, EPOLL_CTL_ADD, sfd, &event);
- if (s == -1)
{perror ("epoll_ctl");abort ();}- /* Buffer where events are returned */
- events = calloc (MAXEVENTS, sizeof event);
- /* The event loop */
- while (1)
{int n, i;n = epoll_wait (efd, events, MAXEVENTS, -1);for (i = 0; i < n; i++){if ((events[i].events & EPOLLERR) ||(events[i].events & EPOLLHUP) ||(!(events[i].events & EPOLLIN))){/* An error has occured on this fd, or the socket is notready for reading (why were we notified then?) */fprintf (stderr, "epoll error\n");close (events[i].data.fd);continue;}else if (sfd == events[i].data.fd){/* We have a notification on the listening socket, whichmeans one or more incoming connections. */while (1){struct sockaddr in_addr;socklen_t in_len;int infd;char hbuf[NI_MAXHOST], sbuf[NI_MAXSERV];in_len = sizeof in_addr;infd = accept (sfd, &in_addr, &in_len);if (infd == -1){if ((errno == EAGAIN) ||(errno == EWOULDBLOCK)){/* We have processed all incomingconnections. */break;}else{perror ("accept");break;}}//將地址轉(zhuǎn)化為主機(jī)名或者服務(wù)名s = getnameinfo (&in_addr, in_len,hbuf, sizeof hbuf,sbuf, sizeof sbuf,NI_NUMERICHOST | NI_NUMERICSERV);//flag參數(shù):以數(shù)字名返回//主機(jī)地址和服務(wù)地址if (s == 0){printf("Accepted connection on descriptor %d ""(host=%s, port=%s)\n", infd, hbuf, sbuf);}/* Make the incoming socket non-blocking and add it to thelist of fds to monitor. */s = make_socket_non_blocking (infd);if (s == -1)abort ();event.data.fd = infd;event.events = EPOLLIN | EPOLLET;s = epoll_ctl (efd, EPOLL_CTL_ADD, infd, &event);if (s == -1){perror ("epoll_ctl");abort ();}}continue;}else{/* We have data on the fd waiting to be read. Read anddisplay it. We must read whatever data is availablecompletely, as we are running in edge-triggered modeand won't get a notification again for the samedata. */int done = 0;while (1){ssize_t count;char buf[512];count = read (events[i].data.fd, buf, sizeof(buf));if (count == -1){/* If errno == EAGAIN, that means we have read alldata. So go back to the main loop. */if (errno != EAGAIN){perror ("read");done = 1;}break;}else if (count == 0){/* End of file. The remote has closed theconnection. */done = 1;break;}/* Write the buffer to standard output */s = write (1, buf, count);if (s == -1){perror ("write");abort ();}}if (done){printf ("Closed connection on descriptor %d\n",events[i].data.fd);/* Closing the descriptor will make epoll remove itfrom the set of descriptors which are monitored. */close (events[i].data.fd);}}}}- free (events);
- close (sfd);
- return EXIT_SUCCESS;
- }
微信公眾號(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程序員面試指南等干貨資源)