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

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