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