前言
這篇文章讀不懂的沒關(guān)系,可以先收藏一下。筆者準(zhǔn)備介紹完epoll和NIO等知識(shí)點(diǎn),然后寫一篇Java網(wǎng)絡(luò)IO模型的介紹,這樣可以使Java網(wǎng)絡(luò)IO的知識(shí)體系更加地完整和嚴(yán)謹(jǐn)。初學(xué)者也可以等看完IO模型介紹的博客之后,再回頭看這些博客,會(huì)更加有收獲。
如果你順利啃下這篇博客,恭喜你,nginx、redis和NIO等核心思想已經(jīng)被你掌握了,可以順勢(shì)去拓展自己的理解。否則,只是孤立的看epoll,時(shí)間一長(zhǎng)會(huì)很快忘記的。
當(dāng)然,這些核心思想,筆者也會(huì)在之后的博客慢慢做詳細(xì)講解,歡迎關(guān)注
概念初探
epoll是一種I/O事件通知機(jī)制,是linux 內(nèi)核實(shí)現(xiàn)IO多路復(fù)用的一個(gè)實(shí)現(xiàn)。
IO多路復(fù)用是指,在一個(gè)操作里同時(shí)監(jiān)聽多個(gè)輸入輸出源,在其中一個(gè)或多個(gè)輸入輸出源可用的時(shí)候返回,然后對(duì)其的進(jìn)行讀寫操作。
IO多路復(fù)用,以后的博客會(huì)有詳細(xì)講解。
I/O
輸入輸出(input/output)的對(duì)象可以是文件(file), 網(wǎng)絡(luò)(socket),進(jìn)程之間的管道(pipe)。在linux系統(tǒng)中,都用文件描述符(fd)來表示。
事件
- 可讀事件,當(dāng)文件描述符關(guān)聯(lián)的內(nèi)核讀緩沖區(qū)可讀,則觸發(fā)可讀事件。
(可讀:內(nèi)核緩沖區(qū)非空,有數(shù)據(jù)可以讀取) - 可寫事件,當(dāng)文件描述符關(guān)聯(lián)的內(nèi)核寫緩沖區(qū)可寫,則觸發(fā)可寫事件。
(可寫:內(nèi)核緩沖區(qū)不滿,有空閑空間可以寫入)
通知機(jī)制
通知機(jī)制,就是當(dāng)事件發(fā)生的時(shí)候,則主動(dòng)通知。通知機(jī)制的反面,就是輪詢機(jī)制。
epoll的通俗解釋
結(jié)合以上三條,epoll的通俗解釋是一種當(dāng)文件描述符的內(nèi)核緩沖區(qū)非空的時(shí)候,發(fā)出可讀信號(hào)進(jìn)行通知,當(dāng)寫緩沖區(qū)不滿的時(shí)候,發(fā)出可寫信號(hào)通知的機(jī)制
epoll的API詳解
epoll的核心是3個(gè)API,核心數(shù)據(jù)結(jié)構(gòu)是:1個(gè)紅黑樹和1個(gè)鏈表

1. int epoll_create(int size)
功能:
- 內(nèi)核會(huì)產(chǎn)生一個(gè)epoll 實(shí)例數(shù)據(jù)結(jié)構(gòu)并返回一個(gè)文件描述符,這個(gè)特殊的描述符就是epoll實(shí)例的句柄,后面的兩個(gè)接口都以它為中心(即epfd形參)。
size參數(shù)表示所要監(jiān)視文件描述符的最大值,不過在后來的Linux版本中已經(jīng)被棄用(同時(shí),size不要傳0,會(huì)報(bào)invalid argument錯(cuò)誤)
2. int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
功能:
- 將被監(jiān)聽的描述符添加到紅黑樹或從紅黑樹中刪除或者對(duì)監(jiān)聽事件進(jìn)行修改
typedef union epoll_data {
void *ptr; /* 指向用戶自定義數(shù)據(jù) */
int fd; /* 注冊(cè)的文件描述符 */
uint32_t u32; /* 32-bit integer */
uint64_t u64; /* 64-bit integer */
} epoll_data_t;
struct epoll_event {
uint32_t events; /* 描述epoll事件 */
epoll_data_t data; /* 見上面的結(jié)構(gòu)體 */
};
對(duì)于需要監(jiān)視的文件描述符集合,epoll_ctl對(duì)紅黑樹進(jìn)行管理,紅黑樹中每個(gè)成員由描述符值和所要監(jiān)控的文件描述符指向的文件表項(xiàng)的引用等組成。
op參數(shù)說明操作類型:
- EPOLL_CTL_ADD:向interest list添加一個(gè)需要監(jiān)視的描述符
- EPOLL_CTL_DEL:從interest list中刪除一個(gè)描述符
- EPOLL_CTL_MOD:修改interest list中一個(gè)描述符
struct epoll_event結(jié)構(gòu)描述一個(gè)文件描述符的epoll行為。在使用epoll_wait函數(shù)返回處于ready狀態(tài)的描述符列表時(shí),
- data域是唯一能給出描述符信息的字段,所以在調(diào)用epoll_ctl加入一個(gè)需要監(jiān)測(cè)的描述符時(shí),一定要在此域?qū)懭朊枋龇嚓P(guān)信息
- events域是bit mask,描述一組epoll事件,在epoll_ctl調(diào)用中解釋為:描述符所期望的epoll事件,可多選。
常用的epoll事件描述如下:
- EPOLLIN:描述符處于可讀狀態(tài)
- EPOLLOUT:描述符處于可寫狀態(tài)
- EPOLLET:將epoll event通知模式設(shè)置成edge triggered
- EPOLLONESHOT:第一次進(jìn)行通知,之后不再監(jiān)測(cè)
- EPOLLHUP:本端描述符產(chǎn)生一個(gè)掛斷事件,默認(rèn)監(jiān)測(cè)事件
- EPOLLRDHUP:對(duì)端描述符產(chǎn)生一個(gè)掛斷事件
- EPOLLPRI:由帶外數(shù)據(jù)觸發(fā)
- EPOLLERR:描述符產(chǎn)生錯(cuò)誤時(shí)觸發(fā),默認(rèn)檢測(cè)事件
3. int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
功能:
阻塞等待注冊(cè)的事件發(fā)生,返回事件的數(shù)目,并將觸發(fā)的事件寫入events數(shù)組中。
events: 用來記錄被觸發(fā)的events,其大小應(yīng)該和maxevents一致
maxevents: 返回的events的最大個(gè)數(shù)
處于ready狀態(tài)的那些文件描述符會(huì)被復(fù)制進(jìn)ready list中,epoll_wait用于向用戶進(jìn)程返回ready list。events和maxevents兩個(gè)參數(shù)描述一個(gè)由用戶分配的struct epoll event數(shù)組,調(diào)用返回時(shí),內(nèi)核將ready list復(fù)制到這個(gè)數(shù)組中,并將實(shí)際復(fù)制的個(gè)數(shù)作為返回值。注意,如果ready list比maxevents長(zhǎng),則只能復(fù)制前maxevents個(gè)成員;反之,則能夠完全復(fù)制ready list。
另外,struct epoll event結(jié)構(gòu)中的events域在這里的解釋是:在被監(jiān)測(cè)的文件描述符上實(shí)際發(fā)生的事件。
參數(shù)timeout描述在函數(shù)調(diào)用中阻塞時(shí)間上限,單位是ms:
- timeout = -1表示調(diào)用將一直阻塞,直到有文件描述符進(jìn)入ready狀態(tài)或者捕獲到信號(hào)才返回;
- timeout = 0用于非阻塞檢測(cè)是否有描述符處于ready狀態(tài),不管結(jié)果怎么樣,調(diào)用都立即返回;
- timeout > 0表示調(diào)用將最多持續(xù)timeout時(shí)間,如果期間有檢測(cè)對(duì)象變?yōu)閞eady狀態(tài)或者捕獲到信號(hào)則返回,否則直到超時(shí)。
epoll的兩種觸發(fā)方式
epoll監(jiān)控多個(gè)文件描述符的I/O事件。epoll支持邊緣觸發(fā)(edge trigger,ET)或水平觸發(fā)(level trigger,LT),通過epoll_wait等待I/O事件,如果當(dāng)前沒有可用的事件則阻塞調(diào)用線程。
select和poll只支持LT工作模式,epoll的默認(rèn)的工作模式是LT模式。
1.水平觸發(fā)的時(shí)機(jī)
- 對(duì)于讀操作,只要緩沖內(nèi)容不為空,LT模式返回讀就緒。
- 對(duì)于寫操作,只要緩沖區(qū)還不滿,LT模式會(huì)返回寫就緒。
當(dāng)被監(jiān)控的文件描述符上有可讀寫事件發(fā)生時(shí),epoll_wait()會(huì)通知處理程序去讀寫。如果這次沒有把數(shù)據(jù)一次性全部讀寫完(如讀寫緩沖區(qū)太小),那么下次調(diào)用 epoll_wait()時(shí),它還會(huì)通知你在上沒讀寫完的文件描述符上繼續(xù)讀寫,當(dāng)然如果你一直不去讀寫,它會(huì)一直通知你。如果系統(tǒng)中有大量你不需要讀寫的就緒文件描述符,而它們每次都會(huì)返回,這樣會(huì)大大降低處理程序檢索自己關(guān)心的就緒文件描述符的效率。
2.邊緣觸發(fā)的時(shí)機(jī)
- 對(duì)于讀操作
- 當(dāng)緩沖區(qū)由不可讀變?yōu)榭勺x的時(shí)候,即緩沖區(qū)由空變?yōu)椴豢盏臅r(shí)候。
- 當(dāng)有新數(shù)據(jù)到達(dá)時(shí),即緩沖區(qū)中的待讀數(shù)據(jù)變多的時(shí)候。
- 當(dāng)緩沖區(qū)有數(shù)據(jù)可讀,且應(yīng)用進(jìn)程對(duì)相應(yīng)的描述符進(jìn)行EPOLL_CTL_MOD 修改EPOLLIN事件時(shí)。
- 對(duì)于寫操作
- 當(dāng)緩沖區(qū)由不可寫變?yōu)榭蓪憰r(shí)。
- 當(dāng)有舊數(shù)據(jù)被發(fā)送走,即緩沖區(qū)中的內(nèi)容變少的時(shí)候。
- 當(dāng)緩沖區(qū)有空間可寫,且應(yīng)用進(jìn)程對(duì)相應(yīng)的描述符進(jìn)行EPOLL_CTL_MOD 修改EPOLLOUT事件時(shí)。
當(dāng)被監(jiān)控的文件描述符上有可讀寫事件發(fā)生時(shí),epoll_wait()會(huì)通知處理程序去讀寫。如果這次沒有把數(shù)據(jù)全部讀寫完(如讀寫緩沖區(qū)太小),那么下次調(diào)用epoll_wait()時(shí),它不會(huì)通知你,也就是它只會(huì)通知你一次,直到該文件描述符上出現(xiàn)第二次可讀寫事件才會(huì)通知你。這種模式比水平觸發(fā)效率高,系統(tǒng)不會(huì)充斥大量你不關(guān)心的就緒文件描述符。
在ET模式下, 緩沖區(qū)從不可讀變成可讀,會(huì)喚醒應(yīng)用進(jìn)程,緩沖區(qū)數(shù)據(jù)變少的情況,則不會(huì)再喚醒應(yīng)用進(jìn)程。
舉例1:
- 讀緩沖區(qū)剛開始是空的
- 讀緩沖區(qū)寫入2KB數(shù)據(jù)
- 水平觸發(fā)和邊緣觸發(fā)模式此時(shí)都會(huì)發(fā)出可讀信號(hào)
- 收到信號(hào)通知后,讀取了1KB的數(shù)據(jù),讀緩沖區(qū)還剩余1KB數(shù)據(jù)
- 水平觸發(fā)會(huì)再次進(jìn)行通知,而邊緣觸發(fā)不會(huì)再進(jìn)行通知
舉例2:(以脈沖的高低電平為例)
- 水平觸發(fā):0為無數(shù)據(jù),1為有數(shù)據(jù)。緩沖區(qū)有數(shù)據(jù)則一直為1,則一直觸發(fā)。
- 邊緣觸發(fā)發(fā):0為無數(shù)據(jù),1為有數(shù)據(jù),只要在0變到1的上升沿才觸發(fā)。
JDK并沒有實(shí)現(xiàn)邊緣觸發(fā),Netty重新實(shí)現(xiàn)了epoll機(jī)制,采用邊緣觸發(fā)方式;另外像Nginx也采用邊緣觸發(fā)。
JDK在Linux已經(jīng)默認(rèn)使用epoll方式,但是JDK的epoll采用的是水平觸發(fā),而Netty重新實(shí)現(xiàn)了epoll機(jī)制,采用邊緣觸發(fā)方式,netty epoll transport 暴露了更多的nio沒有的配置參數(shù),如 TCP_CORK, SO_REUSEADDR等等;另外像Nginx也采用邊緣觸發(fā)。
epoll與select、poll的對(duì)比
1. 用戶態(tài)將文件描述符傳入內(nèi)核的方式
- select:創(chuàng)建3個(gè)文件描述符集并拷貝到內(nèi)核中,分別監(jiān)聽讀、寫、異常動(dòng)作。這里受到單個(gè)進(jìn)程可以打開的fd數(shù)量限制,默認(rèn)是1024。
- poll:將傳入的struct pollfd結(jié)構(gòu)體數(shù)組拷貝到內(nèi)核中進(jìn)行監(jiān)聽。
- epoll:執(zhí)行epoll_create會(huì)在內(nèi)核的高速cache區(qū)中建立一顆紅黑樹以及就緒鏈表(該鏈表存儲(chǔ)已經(jīng)就緒的文件描述符)。接著用戶執(zhí)行的epoll_ctl函數(shù)添加文件描述符會(huì)在紅黑樹上增加相應(yīng)的結(jié)點(diǎn)。
2. 內(nèi)核態(tài)檢測(cè)文件描述符讀寫狀態(tài)的方式
- select:采用輪詢方式,遍歷所有fd,最后返回一個(gè)描述符讀寫操作是否就緒的mask掩碼,根據(jù)這個(gè)掩碼給fd_set賦值。
- poll:同樣采用輪詢方式,查詢每個(gè)fd的狀態(tài),如果就緒則在等待隊(duì)列中加入一項(xiàng)并繼續(xù)遍歷。
- epoll:采用回調(diào)機(jī)制。在執(zhí)行epoll_ctl的add操作時(shí),不僅將文件描述符放到紅黑樹上,而且也注冊(cè)了回調(diào)函數(shù),內(nèi)核在檢測(cè)到某文件描述符可讀/可寫時(shí)會(huì)調(diào)用回調(diào)函數(shù),該回調(diào)函數(shù)將文件描述符放在就緒鏈表中。
3. 找到就緒的文件描述符并傳遞給用戶態(tài)的方式
- select:將之前傳入的fd_set拷貝傳出到用戶態(tài)并返回就緒的文件描述符總數(shù)。用戶態(tài)并不知道是哪些文件描述符處于就緒態(tài),需要遍歷來判斷。
- poll:將之前傳入的fd數(shù)組拷貝傳出用戶態(tài)并返回就緒的文件描述符總數(shù)。用戶態(tài)并不知道是哪些文件描述符處于就緒態(tài),需要遍歷來判斷。
- epoll:epoll_wait只用觀察就緒鏈表中有無數(shù)據(jù)即可,最后將鏈表的數(shù)據(jù)返回給數(shù)組并返回就緒的數(shù)量。內(nèi)核將就緒的文件描述符放在傳入的數(shù)組中,所以只用遍歷依次處理即可。這里返回的文件描述符是通過mmap讓內(nèi)核和用戶空間共享同一塊內(nèi)存實(shí)現(xiàn)傳遞的,減少了不必要的拷貝。
4. 重復(fù)監(jiān)聽的處理方式
- select:將新的監(jiān)聽文件描述符集合拷貝傳入內(nèi)核中,繼續(xù)以上步驟。
- poll:將新的struct pollfd結(jié)構(gòu)體數(shù)組拷貝傳入內(nèi)核中,繼續(xù)以上步驟。
- epoll:無需重新構(gòu)建紅黑樹,直接沿用已存在的即可。
epoll更高效的原因
- select和poll的動(dòng)作基本一致,只是poll采用鏈表來進(jìn)行文件描述符的存儲(chǔ),而select采用fd標(biāo)注位來存放,所以select會(huì)受到最大連接數(shù)的限制,而poll不會(huì)。
- select、poll、epoll雖然都會(huì)返回就緒的文件描述符數(shù)量。但是select和poll并不會(huì)明確指出是哪些文件描述符就緒,而epoll會(huì)。造成的區(qū)別就是,系統(tǒng)調(diào)用返回后,調(diào)用select和poll的程序需要遍歷監(jiān)聽的整個(gè)文件描述符找到是誰處于就緒,而epoll則直接處理即可。
- select、poll都需要將有關(guān)文件描述符的數(shù)據(jù)結(jié)構(gòu)拷貝進(jìn)內(nèi)核,最后再拷貝出來。而epoll創(chuàng)建的有關(guān)文件描述符的數(shù)據(jù)結(jié)構(gòu)本身就存于內(nèi)核態(tài)中,系統(tǒng)調(diào)用返回時(shí)利用mmap()文件映射內(nèi)存加速與內(nèi)核空間的消息傳遞:即epoll使用mmap減少?gòu)?fù)制開銷。
- select、poll采用輪詢的方式來檢查文件描述符是否處于就緒態(tài),而epoll采用回調(diào)機(jī)制。造成的結(jié)果就是,隨著fd的增加,select和poll的效率會(huì)線性降低,而epoll不會(huì)受到太大影響,除非活躍的socket很多。
- epoll的邊緣觸發(fā)模式效率高,系統(tǒng)不會(huì)充斥大量不關(guān)心的就緒文件描述符
雖然epoll的性能最好,但是在連接數(shù)少并且連接都十分活躍的情況下,select和poll的性能可能比epoll好,畢竟epoll的通知機(jī)制需要很多函數(shù)回調(diào)。
