淺談IO的多路複用技術之一(select和epoll實質)

原文地址:http://www.10tiao.com/html/308/201601/401697863/1.html

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是主要解決方案。

明天我們會聊聊,NIO的一些bug,比較常見的空轉的那個bug,看看一些服務器是咋解決這個問題的。

後天我們會繼續這一個話題,看看java的AIO的接口,聊聊最新的進展。

相關文章鏈接:https://my.oschina.net/xianggao/blog/663655

發佈了269 篇原創文章 · 獲贊 329 · 訪問量 204萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章