1. 程式人生 > >Select和Epoll底層實現的區別

Select和Epoll底層實現的區別

JAVA的NIO技術從1.5開始,一直到現在的JDK8,這套JDK自帶的API幾乎填充了了整個java端伺服器的程式碼實現,人們都是大談特談這些介面,但是很少有人深究作業系統實現的底層細節,這篇文章帶你簡單瀏覽一下這些底層的細節。

JDK 1.5 中NIO出來後,搞出了幾個類,Selector,Channel,Buffer,關心的事件如read/write等這些內容,實質這些類是java部分的再次封裝,早在很早之前,基於作業系統的select介面就已經存在.我們可以在linux的命令列的環境中 man select一下,看看select介面的系統呼叫:
在這裡插入圖片描述

基於POSIX-2001的介面,需要引入4個.h檔案,select系統呼叫的引數一共有5個:

引數1:你所監視的系統描述符fd最大的,然後+1,====》比較奇怪

引數2:fd讀的狀態有通知了,回填到這個讀的集合中

引數3:fd寫集合傳入,也回填到這個引數

引數4:異常集合傳入,也回填到這個引數

引數5:超時設定,如果沒有這個引數,2,3,4引數啥都沒發生,select就一直阻塞,

通過這些引數,我們就可以瞭解,select的系統呼叫級別的程式碼幾乎和java的NIO的類庫很像(或者說java類庫中的就是按照select來實現)

select這個系統呼叫是很古老的,一般的Unix的衍生作業系統都支援,移植行也是非常的好,並且基於事件進行組織;

但是,通過前面你檢視引數就可以總結出來,其缺陷也是多多:

  1. 引數1非常的奇怪,最大的fd還要+1,經常有人會沒有加1,而導致系 統呼叫失敗,對於此,只能怨Unix設計者的古老了,沒有招;
  2. 2,3,4引數,每一次是我設定的需要監視的引數,需要傳入到這個select中,但是在恰恰這個傳入的引數,如果有事件發生的話,也是回填到這個引數。=》這相當於什麼?相當於你好不容易傳入的東西,都被沖掉了,因此,你每一次select完事之後,這些fd你還得重新設定一遍,=》可以總結介面非常的難用,而java的NIO介面在這裡也延續了這一習慣。
  3. void FD_CLR(int fd, fd_set *set); ==》對於fd描述符的操作集合的方法很不好用,這個極容易引起混淆,注意這個系統呼叫不是清空,而是刪除,名字容易糊塗.
  4. 監視的fd,僅僅就是讀,寫,異常,監視事件範圍太單一,不利於查詢;

看到這些缺陷,可以發現,JAVA的NIO和這個非常的類似,說的沒錯,最早期的NIO的底層實現就是select,至少是JDK1.5,和JDK1.6中都是。

在JDK1.6的後期的版本中,需要開啟-D引數:

-Djava.nio.channels.spi.SelectorProvider=sun.nio.ch.EPollSelectorProvider

這個引數,就指示JAVA 的NIO框架預設就採用的epoll作為底層支撐。
到了JDK1.7的時候,預設就是epoll,select已經完全退出歷史舞臺了。

為什麼這裡提到了epoll?epoll有啥優點呢?
其實epoll也就是一個系統呼叫,你在linux中man epoll一下,它仍會告訴你epoll是什麼東西:
在這裡插入圖片描述

可以看到,根本就不是man 2,因為epoll 函式就是一個方言

第一步:
在這裡插入圖片描述
通過epoll_create進行建立epoll 例項。

第二步:
在這裡插入圖片描述
通過epoll_ctl對fd進行註冊感興趣的事件。

第三步:
在這裡插入圖片描述
通過epoll_wait來等待,來檢視監視結果

上面的三個步驟貌似還挺麻煩,但你要仔細分析一下,你就知道為什麼epoll好的原因了;

其中一個重要的系統呼叫就是通過epoll_ctl函式註冊感興趣的時間,而這個epoll_ctl函式:
在這裡插入圖片描述

引數1:剛才epoll_create的系統呼叫的返回的內容,也就是epoll的例項

引數2:op操作

    EPOLL_CTL_ADD

          Register the target file descriptor fd on  the  epoll  instance  referred  to  by  the  file descriptor epfd and associate the event event with the internal file linked to fd.

   EPOLL_CTL_MOD

          Change the event event associated with the target file descriptor fd.

   EPOLL_CTL_DEL

          Remove  (deregister)  the  target  file descriptor fd from the epoll instance referred to by epfd.  The event is ignored and can be NULL (but see BUGS below).

引數3:針對的物件是檔案描述符

引數4:針對引數3的fd的哪個事件

========》分析到這裡,我們可以發現,在epoll中貌似操作的fd事件集合是開放一個系統呼叫供客戶端進行呼叫的,而不是類似select中我們自己可以攢1個fd集合,但是在epoll這裡不行,我們只能以呼叫系統呼叫函式的方式,操縱這個fd。

而這種架構,就如下圖所示,這也就表明了,為啥epoll優異的原因:

1.關於fd事件陣列的複製
在這裡插入圖片描述
上圖是對比了三個系統呼叫,你可以理解poll和select差不多,實線上是使用者態,實線下是核心態。

可以看到select和poll的fd_set集合,是在使用者態進行定義,然後你通過系統呼叫,將這個引數傳入到核心態中,這是一次複製,這個資料結構就在核心態也被複制一份(虛線部分);

而select和poll的系統呼叫結束,發現有一些fd有事件來了,再將這個資料結構,從核心態傳回使用者態,然後使用者再進行遍歷;

裡外裡,這就是兩次fd陣列的複製,我們要是有10000個fd關注,可以看到,每一次select和poll都來回折騰一遍,消耗太大!

===》epoll改進在於通過epoll_create系統呼叫,直接在核心態建立fd陣列,沒有複製

epoll系統呼叫結束,發現有一些fd事件來了,將核心態傳入使用者態,這有一次複製;

總結,一次系統呼叫查詢到有事件的fd,select,poll兩次來回在使用者態和核心態複製,而epoll只有1次,這個就是第一個優點。

2.fd事件陣列的遍歷
在這裡插入圖片描述
select和poll在核心中也對應fd_set陣列,可以看到這是從使用者態拷貝到核心中的,而epoll的fd_set陣列是核心中的資料結構,這是我們已知的二者的大不同;正因為如此,select和poll的fd_set陣列,就是普通的資料,沒有任何的附加功能,因此IO多路複用,硬體事件發生後(也稱就緒狀態),會直接賦值到這個fd_set陣列中;而select和poll在每一次阻塞-喚醒,這一過程中,至少有1到n次的select的輪詢工作;===》select和poll需要遍歷,epoll同樣也得遍歷,但是epoll的機制在於遍歷的內容少的嚇人,epoll中核心所謂的fd_set集合,並不是遍歷的物件,他其中每一個fd都對應回撥函式,當就緒事件發生後,將這個真正有事件的fd連同事件,一塊放到一個epollfd就緒佇列中。

可以看到,每一次epoll遍歷僅僅是這個fd就緒佇列,這個佇列中的fd全部都是就緒的,甚至可以這麼說,epoll就壓根沒有遍歷,只需判斷一下fd就緒佇列是否為空,不為空就返回,因此效率驚人,同比100w個fd做監視,對於那種網絡卡類的稀疏網路事件的情況(也就是大部分時間,甚至99%以上的時間都沒事幹,沒流量),select和poll一般至少要遍歷100w次或者200w次,甚至設定超時時間的話,在等待超時時間這段cpu就爆滿了;

==》但是epoll僅僅遍歷1次?2次?最壞的等待超時也僅僅是n次,數量級差太多了,這也就是epoll的優勢,總結一下,也就是epoll單獨搞了一個fd就緒佇列的模式,減少了遍歷!

3.從核心角度來看,基於fd事件陣列在核心態都需要與硬體驅動進行繫結,繫結是很耗時的,epoll的fd事件陣列,就在核心中,繫結1次就OK

而select這些使用者態的陣列,執行到核心態中,每一次都需要重新繫結一次:
在這裡插入圖片描述

4.fd事件的限制
在這裡插入圖片描述

總結了上面的4條,其實epoll效能優異可以歸結於一句話,就是epoll的事件fd放在了核心中,不在使用者態折騰了,直接更底層的進行操作,省去了不少的事情,這個是實質的原因!

java中的NIO在JDK後續的版本中,在linux的環境下,基本都是epoll了,當然類似於epoll的機制,Solaris中有eventq,FreeBSD中的kqueue也相當的猛,

這些都是IO多路複用技術,它們的本質並不是AIO,所謂的AIO至少到目前位置,沒有什麼好的系統呼叫實現,雖然有AIO的介面,但基於硬體平臺的不同,效果差強人意。而IO多路複用技術,是通過一個按照時鐘週期輪詢的裝置,基於事件去你註冊的事件集合,有事件的話直接返回,沒有事件的話如果沒有超時時間的話,就阻塞,從這一點來看,貌似是非同步的過程,但是這個過程和純AIO還不一樣,純的AIO介面根本不需要什麼Selector,epoll例項這些裝置,還有上述的各種fd集合的掃描,繫結,遍歷,和使用者態到核心態的賦值和遷移,直接就是事件驅動,一個註冊事件對應一個核心級別的繫結,上述的fd集合這些費勁的東西根本都不需要。不過隨著時代的發展,基於硬體的AIO介面現在很多專案已經也在用,效率也是驚人的高的。

java的NIO這塊目前底層技術還是IO多路複用為主,linux中epoll是主要解決方案。