Netty 學習筆記(1)通信原理


前言

      本文主要從 select 和 epoll 系統調用入手,來打開 Netty 的大門,從認識 Netty 的基礎原理 —— I/O 多路復用模型開始。如下是微信公號系列文章,持續更新

開篇:Netty源碼學習系列

Netty 學習筆記(1)Netty 通信原理

Netty服務端與客戶端初始化流程(1)

Netty服務端與客戶端初始化流程(2)

Netty服務端與客戶端初始化流程(3)

Netty服務端與客戶端初始化流程(4)

Netty的線程調度模型分析(1)

Netty的線程調度模型分析(2)

Netty的線程調度模型分析(3)

Netty的線程調度模型分析(4)

Netty的線程調度模型分析(5)

Netty的線程調度模型分析-番外篇(1)

Netty的線程調度模型分析-番外篇(2)

Netty的線程調度模型分析-番外篇(3)

Netty的線程調度模型分析-番外篇(4)

​。。。。


 

Netty 的通信原理

  Netty 底層的通信機制是基於I/O多路復用模型構建的,簡單一句話概括就是多路網絡連接可以復用一個I/O線程,在 Java 層面也就是封裝了其 NIO API,但是 JDK 底層基於 Linux 的 epoll 機制實現(其實是三個函數)。注意在老舊的 Linux 上,可能還是 select,沒考證過,但是時下主流版本,肯定早就是 epoll 機制了,不妨就認為 JDK NIO 底層是基於 epoll 模型。

  想象這樣一個場景:老師站在講台上提問,下面100個學生把答案寫在紙上,誰寫完誰舉手示意,讓老師來檢查,完成的好就可以放學回家。如果學生張三舉手,李四也舉手,就表示他們已經完成了,老師就立即依次去檢查張三和李四的答案,檢查完畢,老師就可以返回講台休息或者溜達等等,接着王五,趙四兒又舉手,然后老師馬上去檢查他們的答案。。。以此往復。

  如上這種生活現象就是 I/O 多路復用模型,Linux下的 select、poll,和epoll 就是實現的這種機制,這樣就避免了大量的無用操作,比如,老師不需要依次的等待一個學生寫完了,然后檢查一個學生,檢查完畢,再去等待下一個學生。。。(對應多客戶端單線程模型),也不需要請100個老師,每個老師對應1個學生(一客戶端一線程的 BIO 模型),而是讓所有學生先自己悶頭寫答案,寫完才主動舉手示意,老師在去檢查答案,處理完畢,老師就可以走了,繼續等待其它學生舉手,全程一個老師就能處理(epoll 函數),這就是所謂的非阻塞模式。另外,老師也不需要順序的詢問每個學生的問題完成情況(select 函數)只需要看誰舉手。。。這樣老師不煩躁,學生也能專心答題。

  類比到通信,整個I/O過程只在調用 select、poll、epoll 這些調用的時候才會阻塞,收發客戶消息是不會阻塞的,整個進程或者線程就被充分利用起來,從而使得系統在單線程(進程)的情況下,可以同時處理多個客戶端請求,這就是I/O 多路復用模型。與傳統的多線程(單線程)模型相比,I/O多路復用的最大優勢就是系統開銷小,系統不需要創建新的額外線程,也不需要維護這些線程的運行、切換、同步問題,降低了系統的開發和維護的工作量,節省了時間和系統資源。

  主要的應用場景,服務器需要同時處理多個處於監聽狀態或多個連接狀態的套接字,服務器需要同時處理多種網絡協議的套接字。

  支持I/O多路復用的系統調用主要有select、pselect、poll、epoll。而當前推薦使用的是epoll,優勢如下:

  1. 支持一個進程打開的socket fd(file description)不受限制
  2. I/O效率不會隨着fd數目的增加而線性下將
  3. 使用mmap加速內核與用戶空間的消息傳遞。
  4. epoll擁有更加簡單的API。

  而常見的一種 I/O 多路復用模型有所謂的 reactor 模式,Netty 就實現了多線程的 reactor 模型(reactor 模型有三種,單線程,多線程和主從),即當有感興趣的事件(event)發生,就通知對應的事件處理器(ChannelHandler)去處理這個事件,如果沒有就不處理。故用一個線程(NioEventLoop)做輪詢就可以了。如果要獲得更高性能,可以使用少量的線程,一個負責接收請求(boss NioEventLoopGroup),其他的負責處理請求(worker NioEventLoopGroup),對於多 CPU 時效率會更高(Netty 的線程池會默認啟動 2 倍的 CPU 核數個線程)。

  后續筆記會詳細分析。

Socket 的抽象層次

  Socket是一種"打開—讀/寫—關閉"模式的實現,服務器和客戶端各自維護一個"文件",在建立連接打開后,可以向自己文件寫入內容供對方讀取或者讀取對方內容,通訊結束時關閉文件。

  不同層次的抽象,對 Socket 的解釋是不一樣的,在計算機網絡中,解釋 Socket 是 ip 地址+端口號,都對,主要看是哪一層次的抽象。

  在網絡編程層次,這些Socket函數是操作系統內核實現的,用戶代碼無法觸及,只能使用,這些內核代碼把TCP/IP協議棧和網卡封裝,暴露出來對用戶友好的API,就成了所謂的 Socket 函數,用戶代碼可以用這些 Socket 函數操縱本地的TCP/IP協議棧和網卡,和服務器通信。

  回到網絡層次,OSI 的上三層等價於 TCP/IP 協議族的應用層(典型的 Telnet、FTP 等應用), OSI 下兩層等價於 TCP/IP 協議族中隨系統提供的設備驅動程序和硬件。在一個網絡程序中, 對應OSI 模型,上三層處理應用本身的細節,卻對應用底層的通信細節了解很少;下四層可以處理所有的底層網絡的通信細節。OSI 的上三層可以對應所謂的用戶進程,下四層通常對應操作系統內核的一部分,因此,把第4層和第5層之間的接口抽象為 Socket API 是自然而然的一個過程,即所謂的 Socket 所處的位置就是 TCP/IP 協議族應用層和傳輸層的交界處。

從編程角度看TCP協議狀態轉移過程 

  在 Linux 的網絡編程這個層次中,客戶機和服務器各有一個Socket文件,當兩台主機通信時,客戶機里的客戶端應用進程 A 發送消息,通過 TCP協議數據包頭的 SYN 標志位置1,進行主動打開,經 A 主機的 TCP/IP 協議棧發送到 LAN,然后經 WAN 中的路由器傳給服務端應用進程 B 的目的主機所在的 LAN,之后經目的主機的 LAN 將報文傳給目的主機,最后經目的主機的 TCP/IP 協議棧處理,服務器被動打開,將消息遞交給目的應用程序 B。

  具體分析如下,在連接建立階段,客戶端調用 connect() 函數發起主動連接——觸發客戶端的 TCP 協議棧發送 SYN 報文,此時客戶端處於 SYN-SENT 態,如下。而在此之前,服務端的 Socket 需要已經處於監聽態(LISTEN),在 Linux 上就是調用 listen() 函數即可實現監聽 Socket。

  服務端的 TCP 協議棧收到該 SYN 報文后,發送給處於 LISTEN 狀態的服務端 Socket,服務端應用進程通過調用 accept() 函數觸發其 TCP 協議棧發送 SYN+ACK 報文返回給客戶端,此時服務端從 LISTEN 態轉移到 SYN-RCVD 態。

  客戶端收到服務端的 SYN+ACK 報文后,發送確認的 ACK 報文,此時客戶端從 SYN-SENT 態進入 ESTABLISHED 態,當服務端收到客戶端的 ACK 報文后,同樣會從 SYN-RCVD 態也進入 ESTABLISHED 態,此時服務端的 accept() 函數返回。

  經過如上三個報文交互,TCP 連接建立,然后就可以進行數據傳輸。

  在數據傳輸階段,客戶端的 Socket 可以調用 send() 函數發送數據,然后服務端的 Socket 接到客戶端 Socket 傳來的請求,調用 read() 函數讀取,調用 write() 函數寫入響應。

  在連接斷開階段,以客戶端主動關閉為例子。

  客戶端的 TCP 協議棧主動發送一個 FIN 報文,主動關閉到服務端方向的連接,此時客戶端狀態從 ESTABLISHED  態轉移到 FIN-WAIT-1 態。通過調用 close() 函數即可實現。

  服務端 TCP 協議棧收到 FIN 報文,就發回客戶端一個 ACK 報文確認關閉,此時,服務端狀態從 ESTABLISHED 態轉移到 CLOSE-WAIT 態(因為是被動關閉),和 SYN 一樣,一個 FIN 也占用一個序號,同時服務端還向客戶端傳送一個文件結束符。當客戶端接受到服務端確認關閉的報文后,客戶端狀態從 FIN-WAIT-1 態轉移到 FIN-WAIT-2 態。

  接着這個服務端程序就關閉它的連接,這會導致服務端的 TCP 協議棧也會發送一個 FIN 報文給客戶端,這里也能清楚看到,ACK 不消耗序號。此時,服務端狀態從  CLOSE-WAIT  轉移到 LAST-ACK 態。

  客戶端收到服務端的 FIN 報文,也必須發回一個ACK 確認報文。此時,客戶端狀態從 FIN-WAIT-2 態轉移到 TIME-WAIT 態。

  至此,TCP 連接關閉。

Linux 網絡編程中的系統調用函數

  對於運行在 Java 虛擬機上的 Java 語言來說,其自身的 Socket 函數,就是對操作系統的這些系統調用函數的封裝而已。看看這些系統調用函數,有助於理解非阻塞通信原理,先認識一些輔助的 Socket 系統調用函數。

  socket 函數:對應於普通文件的打開操作,要知道 Linux 中,一切都是文件,包括 Socket 本身也是一個文件,分別存在於客戶端和服務端機器上。前面也提到了 fd,即普通文件的打開操作會返回一個文件描述符——file description,即 fd,socket() 函數就是用來創建 Socket 描述符(socket descriptor,即 sd) 的,它唯一標識一個 Socket。這個 sd 跟 fd 一樣,后續的操作都會用到它,把它作為參數,通過它來進行一些 Socket 的讀寫操作。

  bind 函數:給一個 sd 綁定一個協議和地址+端口號。

  listen 函數:socket() 函數創建的 Socket 默認是一個主動類型的,listen 函數將 Socket 變為被動類型的,用於等待客戶的連接請求。

  connect 函數:客戶端通過調用 connect 函數來建立與 TCP 服務器的連接。

  accept 函數:TCP 服務端依次調用 socket()、bind()、listen() 之后,就會監聽指定的 Socket 地址了,TCP 客戶端依次調用 socket()、connect() 之后就向 TCP 服務端發送一個連接請求。服務端監聽到這個請求后,調用 accept() 函數接收請求,如果 accept()  函數成功返回,則標識服務端與客戶端已經正確建立連接,此時服務端可以通過 accept 函數返回的 Socket 來完成與客戶端的通信,之后的操作就和普通的 I/O 操作(read 函數和 write 函數)沒什么區別。

Linux select 函數

  Linux 提供了 select/poll 函數,這些系統調用的進程通過將一個或多個 fd(文件描述符,Linux 的一切都是文件) 傳遞給 select 或 poll 系統調用,阻塞在這兩個系統調用中的某一個之上,而不是阻塞在真正的 I/O 系統調用上,這樣 select/poll 可以幫我們偵測多個 fd 是否處於就緒狀態。

  具體的說,聯系老師和學生考試的例子,select/poll 順序掃描 fd 是否就緒,但是 select 支持的 fd 數量有限,因此它的使用受到了一些制約。Linux 還提供一個 epoll 系統調用,兩個東西本質是一樣的,只不過 epoll 高級一些,能力更強一些,是基於事件驅動方式代替順序掃描,因此性能更高——當有 fd 就緒時,立即回調函數rollback。該函數允許進程指示內核等待多個事件中的任何一個發生,並只在有一個或多個事件發生或經歷一段指定的時間后才喚醒它。

  也就是說,我們調用 select/epoll 告知內核對哪些描述符(讀、寫或異常條件〉感興趣以及等待多長時間。我們感興趣的描述符不局限於套接字,任何描述符都可以使用select來測試。

  乍一看上面的解釋,可能會懵逼,當然,懂得就略過。下面就詳細分析下,畢竟人家都黑我們 Javaer 不懂。。。

  眾所周知,read、write、recv, 和 recvfrom 等函數都是阻塞的函數,所謂阻塞,簡單說,就是當函數不能成功執行完畢的時候,程序就會一直停在這里,無法繼續執行以后的代碼。

  嚴格的說,Linux 對一個 fd 指定的文件或設備, 有兩種工作方式: 阻塞與非阻塞方式。阻塞方式是指當試圖對該 fd 進行讀寫時,如果當時沒有數據可讀,或者暫時不可寫,程序就進入等待狀態,直到可讀或者可寫為止。非阻塞方式是指如果 fd 沒有數據可讀,或者不可寫,讀/寫的函數馬上返回,不會等待結果。使用 selcet/epoll 函數就可以實現非阻塞編程。

  先看 selcet 函數,它本質是一個輪循函數,即當循環詢問fd時,可設置超時時間,超時時間到了就跳過代碼繼續往下執行。

 1 fd_set readfd;
 2 struct timeval timeout;
 3  
 4 FD_ZERO(&readfd); // 初始化 readfd
 5 FD_SET(gps_fd, &readfd); // 把 gps_fd 加入 readfd
 6 timeout.tv_sec = 3; // 設置 3 秒超時
 7 timeout.tv_usec = 0;
 8  
 9 j = select(gps_fd+1, &readfd, NULL, NULL, &timeout); // 用 select 對 gps_fd 進行輪循
10 if(j>0){
11     if( FD_ISSET(gps_fd, &readfd) ){ // 如果 gps_fd 可讀
12         i = read(gps_fd, buf, SIZE);
13         buf[i] = '\0';
14     }
15 }
C 系列的代碼 ztm 的很繁瑣,實現個簡單的聊天 demo,都要很多代碼和繁瑣的考慮。。。到了如今,Java 強大的生態系統愈發完善,其 Netty 已經可以和 C++ 實現的異步非阻塞服務器抗衡,愈發想不通,為什么還有人要用 C++ 語言來實現類似項目(純屬個人吐槽。。。),這里只是依靠之前的知識基礎,其實我也忘得差不多了。。。寫了一個小小的方法,直觀感受下,我們重點還是在 Java 這塊。
View Code

  主要看 select 函數的參數,幫助我們理解它的工作原理。先看參數類型,fd_set 是一個集合(struct),其中存放的是 fd,有的書也叫文件句柄。timeval 也是一個 struct,代表時間值

int select(int maxfdp, fd_set *readfds, fd_set *writefds, fd_set *errorfds, struct timeval *timeout); 

  第一個參數 int maxfdp:指fd_set集合中所有 fd 的范圍,即所有文件描述符的最大值加1,不能錯。

  第二個參數 fd_set  *readfds:集合中包括 fd,select 會監視這些 fd 是否可讀,看名字 readfds 也能看出來,如果 readfds 中有一個文件可讀,select 就會返回一個大於 0 的值,表示有文件可讀,如果沒有可讀的,則根據 timeout 參數判斷是否超時,若超時,select 返回 0,若發生錯誤直接返回負值。也可以傳入 NULL 值,表示不關心任何文件的讀變化。

  第三個參數 fd_set  *writefds:集合中包括 fd,select 監視這些 fd 是否可寫,如果有一個 fd 可寫,select 就返回一個大於 0 的值,否則根據 timeout 判斷是否超時,后續和 readfds 一樣。

  第四個參數 fd_set  *errorfds:同上,select 可以監視 fd 的錯誤異常。 

  第五個參數 struct timeval  *timeout:是 select 的超時時間,它可使 select 處於三種狀態;

    1、傳入 NULL,select 變為阻塞函數,一定等到被監視的 fd 集合中,某個 fd 發生變化為止;

    2、設為 0,select 變成非阻塞函數,不管 fd 是否有變化,都立刻返回,fd 無變化返回  0,有變化返回一個正值;

    3、大於 0,select 的阻塞超時時間,時間內有事件到來就返回,否則在超時后就一定返回,返回值同上。 

  前面說了,selcet 函數本質是一個輪循函數,即 select 內部會循環詢問參數集合里的 fd,原理其實也很簡單,每次輪詢發現有 fd 發生變化,就會返回,否則一直輪詢直到超時,如果沒有超時就直接返回,不阻塞。輪詢的目的就是發現 fd 可讀或者可寫,然后可以讓單個進程去處理 I/O 事件,避免 fork 多個客戶進程。 

Linux epoll 機制

  熟悉 Java NIO 編程的都知道,JDK 里也有 select() 方法,一般也叫它I/O多路復用器(網上有人翻譯為選擇器,個人感覺並不能突出其實現思想,我采納了《Netty 權威指南》作者的翻譯),它實際上底層並不是基於 Linux 的 select 系統函數實現,不要被名字誤導,它是基於 epoll 系統函數而實現。下面就學習下這個系統函數,幫助我們理解 Java NIO 編程思想。

  epoll 是 Linux 下 I/O多路復用器——select/poll 的增強版,最早出現在 Linux 內核 2.5.44中,其實現與使用方式與 select/poll 有一些差異,epoll 是通過了一組函數來完成有關任務,而不是類似 select 函數那樣,只依靠一個函數。

select 函數的缺陷

  簡單的看下 select 的執行流程;首先要設置 maxfdp,將 fd 加入 select 監控集,使用一個 array 保存放到 select 監控集中的 fd,一是用於在 select 返回后,array 作為源數據和 fdset 進行 fd_isset 判斷。二是在 select 返回后會把以前加入的但並無事件發生的 fd 清空,則每次開始 select 都要從 array 取得 fd 逐一加入。select 的模型必須在 select 前循環 array(加fd,取 maxfd),返回后循環 array。下面的 demo 只一次調用,很簡單。

 1 int main() {  
 2         char buf[10] = "";  
 3         fd_set rdfds; // 監視可讀事件的 fd 集合
 4         struct timeval tv; // 超時時間
 5         int ret;  
 6         FD_ZERO(&rdfds); // 初始化 readfds
 7         FD_SET(0, &rdfds); // fd==0 表示鍵盤輸入
 8         tv.tv_sec = 3;  
 9         tv.tv_usec = 500;  
10         ret = select(1, &rdfds, NULL, NULL, &tv); // 第一個參數是 maxfdp,值是監控的 fd 號 + 1,本例就是 0 + 1 
11         if(ret < 0)  
12               printf("selcet error \r\n");  
13         else if(ret == 0)  
14               printf("timeout \r\n");  
15         else  
16               printf("ret = %d \r\n", ret);  
17
18         if(FD_ISSET(0, &rdfds)){ // 說明監控的 fd 可讀,stdin 輸入已經發生  
19               printf(" reading 。。。");  
20               read(0, buf, 9); // 從鍵盤讀取輸入  
21         }  
22         write(1, buf, strlen(buf)); // 在終端回顯  
23         printf(" %d \r\n", strlen(buf));  
24         return 0;  
25 }  

  顯然,可以發現 select 會做很多無用功。

  1、即使只有一個 fd 就緒,select 也要遍歷整個 fd 集合,這顯然是無意義的操作。

  2、如果事件需要循環處理,那么每次 select 后,都要清空以前加入的但並無事件發生的 fd 數組(本例子就一個),在每次重新開始 select 時,都要再次從 array 取得 fd 逐一加入 fd_set 集合,每次這樣的操作都需要做一次從進程的用戶空間到內核空間的內存拷貝,使得 select 的效率較低。

  3、select 能夠處理的最大 fd 數目是有限制的,而且限制很低,一般為 1024,如果客戶端過多,會大大降低服務器響應效率。

epoll 高效的原因

  select 函數將當前進程輪流加入每個 fd 對應設備的等待隊列去詢問該 fd 有無可讀/寫事件,無非是想,在哪一個設備就緒時能夠通知當前進程退出調用,Linux 的開發者想到,找個“代理”的回調函數代替當前進程,去加入 fd 對應設備的等待隊列,讓這個代理的回調函數去等待設備就緒,當有設備就緒就將自己喚醒,然后該回調函數就把這個設備的 fd 放到一個就緒隊列,同時通知可能在等待的輪詢進程來這個就緒隊列里取已經就緒的 fd。當前輪詢的進程不需要遍歷整個被偵聽的 fd 集合。

  簡單說:

  • epoll 將用戶關心的 fd 放到了 Linux 內核里的一個事件表中,而不是像 select/poll 函數那樣,每次調用都需要復制 fd 到內核。內核將持久維護加入的 fd,減少了內核和用戶空間復制數據的性能開銷。
  • 當一個 fd 的事件發生(比如說讀事件),epoll 機制無須遍歷整個被偵聽的 fd 集,只要遍歷那些被內核 I/O 事件異步喚醒而加入就緒隊列的 fd 集合,減少了無用功。
  • epoll 機制支持的最大 fd 上限遠遠大於 1024,在 1GB 內存的機器上是 10 萬左右,具體數目可以 cat/proc/sys/fs/file-max查看。

epoll 機制的兩種工作方式:ET 和 LT

  epoll 由三個系統調用組成,分別是 epoll_create,epoll_ctl 和 epoll_wait。epoll_create 用於創建和初始化一些內部使用的數據結構,epoll_ctl 用於添加,刪除或修改指定的 fd 及其期待的事件,epoll_wait 就是用於等待任何先前指定的fd事件就緒。

  服務端使用 epoll 步驟如下:

  • 調用 epoll_create 在 Linux 內核中創建一個事件表;
  • 將 fd(監聽套接字 listener)添加到所創建的事件表;
  • 在主循環中,調用 epoll_wait 等待返回就緒的 fd 集合;
int main() {
    struct epoll_event ev, events[20];
    struct sockaddr_in clientaddr, serveraddr;
    int epfd;
    int sd;
    int maxi;
    int nfds;
    int i;
    int sock_fd, conn_fd;
    char buf[10];

    epfd = epoll_create(2560); // 生成 epoll 句柄,size 告訴內核監聽的 fd 數目最大值
    // 當創建 epoll 句柄后,它就會占用一個 fd 值,所以在使用完 epoll,必須調用 close() 釋放資源,否則可能導致 fd 被耗盡。
    sd = socket(AF_INET, SOCK_STREAM, 0); // 創建 Socket
    ev.data.fd = sd; // 設置與要處理事件相關的 fd,這里就是 Socket 的 sd
    ev.events = EPOLLIN; // 設置感興趣的 fd 事件類型, EPOLLIN 表示 fd 可讀(包括對端 Socket 正常關閉)事件
    epoll_ctl(epfd, EPOLL_CTL_ADD, sd, &ev);// EPOLL_CTL_ADD:注冊新的 fd 到 epfd 中
    memset(&serveraddr, 0, sizeof(serveraddr));
    serveraddr.sin_family = AF_INET;
    serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
    serveraddr.sin_port = htons(SERV_PORT);
    bind(sd, (struct sockaddr*)&serveraddr, sizeof(serveraddr)); // 綁定 Socket
    socklen_t clilen;
    listen(sd, 20); // 轉為監聽的 Socket
    int n;

    while(1) {
        nfds = epoll_wait(epfd, events, 20, 500); //等待 fd 感興趣的事件發生,函數返回需要處理的事件數目
        for(i = 0; i < nfds; i++) {
            if(events[i].data.fd == sd) { // 新連接到了
                clilen = sizeof(struct sockaddr_in);
                conn_fd = accept(sd, (struct sockaddr*)&clientaddr, &clilen);
                printf("accept a new client : %s\n", inet_ntoa(clientaddr.sin_addr));
                ev.data.fd = conn_fd;
                ev.events = EPOLLIN; // 設置 fd 的監聽事件為可寫
                epoll_ctl(epfd, EPOLL_CTL_ADD, conn_fd, &ev); // 注冊新的 Socket 到 epfd
            } else if(events[i].events & EPOLLIN) { // 可讀事件被觸發
                if((sock_fd = events[i].data.fd) < 0)
                    continue;
                if((n = recv(sock_fd, buf, 10, 0)) < 0) {
                    if(errno == ECONNRESET) {
                        close(sock_fd);
                        events[i].data.fd = -1;
                    } else {
                        printf("readline error \n");
                    }
                } else if(n == 0) {
                    close(sock_fd);
                    printf("關閉 \n");
                    events[i].data.fd = -1;
                }
                printf("%d -- > %s\n",sock_fd, buf);
                ev.data.fd = sock_fd;
                ev.events = EPOLLOUT;
                epoll_ctl(epfd, EPOLL_CTL_MOD, sock_fd, &ev); // 修改監聽事件為可讀
            } else if(events[i].events & EPOLLOUT) { // 可寫事件被觸發
                sock_fd = events[i].data.fd;
                printf("OUT\n");
                scanf("%s",buf);
                send(sock_fd, buf, 10, 0);
                ev.data.fd = sock_fd;
                ev.events = EPOLLIN;
                epoll_ctl(epfd, EPOLL_CTL_MOD,sock_fd, &ev);
            }
        }
    }
}
View Code

  把 fd 加入到 epoll 的監聽隊列中, 當 fd 可讀/寫事件,這個條件發生時,在經驗看來,epoll_wait() 當然會立即返回(也叫被觸發),事實上確實是這樣的,而且返回值是需要處理的 fd 事件的數目。這里要討論的是 epoll_wait() 函數返回的條件到底都有什么。

  如果 epoll_wait() 只在讀/寫事件發生時返回,就像前面舉的經驗例子,該觸發叫做邊緣觸發——ET(edge-triggered),也就是說如果事件處理函數只讀取了該 fd 的緩沖區的部分內容就返回了,接下來再次調用 epoll_wait(),雖然此時該就緒的 fd 對應的緩沖區中還有數據,但 epoll_wait() 函數也不會返回。

  相反,無論當前的 fd 中是否有讀/寫事件反生了,只要 fd 對應的緩沖區中有數據可讀/寫,epoll_wait() 就立即返回,這叫做水平觸發——LT(level-triggered)。

  一句話總結,在 ET 模式下,只有 fd 狀態發生改變,fd 才會被再次選出。ET 模式的特殊性,使 ET 模式下的一次輪詢必須處理完本次輪詢出的 fd 緩沖區里的的所有數據,否則該 fd 將不會在下次輪詢中被選出。

  select/poll 使用的觸發方式是 LT,相對來說比較低效,而 ET 是 epoll 的高速工作方式。

epoll 缺點

  epoll 每次只遍歷活躍的 fd (如果是 LT,也會遍歷先前活躍的 fd),在活躍 fd 較少的情況下就會很有優勢,如果大部分 fd 都是活躍的,epoll 的效率可能還不如 select/poll。

Java NIO 的觸發模式

  如果寫過 Java NIO 代碼,那么就能推測到 JDK NIO 的 epoll 模型是 LT,在 Netty 的實際開發中,也能體會到 NioServerSocketChannel 是 LT,當然如果使用 Netty 自己實現的 epoll Channel,就是 ET。

  Netty 的 NioEventLoop 模型中,每次輪詢都會進行負載均衡,限制了每次從 fd 中讀取數據的最大值,造成一次讀事件處理並不會 100% 讀完 fd 緩沖區中的所有數據。在基於 LT 的 NioServerSocketChannel 中,Netty 不需要做特殊處理,在處理完一個 I/O 事件后直接從 SelectionKey 中移除該事件即可,如果有未讀完的數據,下次輪詢仍會獲得該事件。而在EpollServerSocketChannel,如果一次事件處理不把數據讀完,需要手動地觸發一次事件,否則下次輪詢將不會讀取先前活躍的 fd 遺留的數據。 

歡迎關注

dashuai的博客是終身學習踐行者,大廠程序員,且專注於工作經驗、學習筆記的分享和日常吐槽,包括但不限於互聯網行業,附帶分享一些PDF電子書,資料,幫忙內推,歡迎拍磚!

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM