Linux epoll 與多路複用

原文地址(略有修改):阻塞、非阻塞、異步、同步以及select/poll和epoll
針對IO,總是涉及到阻塞、非阻塞、異步、同步以及select/poll和epoll的一些描述,那麼這些東西到底是什麼,有什麼差異?

一般來講一個IO分爲兩個階段:

  1. 等待數據到達
  2. 把數據從內核空間拷貝到用戶空間

現在假設一個進程/線程A,發出IO請求,有兩種情況:

  1. 立即返回
  2. 由於數據未準備好,需要等待,讓出CPU給別的線程,自己sleep

第一種情況就是非阻塞,A爲了知道數據是否準備好,需要不停的詢問,而在輪詢的空歇期,理論上是可以乾點別的活,例如喝喝茶、泡個妞。第二種情況就是阻塞,A除了等待就不能做任何事情。

數據終於準備好了,A現在要把數據取回去,有幾種做法:

  1. A自己把數據從內核空間拷貝到用戶空間。
  2. A創建一個新線程(或者直接使用內核線程),這個新線程把數據從內核空間拷貝到用戶空間。

第一種情況,所有的事情都是同一個線程做,叫做同步,有同步阻塞(BIO)、同步非阻塞(NIO)。第二種情況,叫做異步,只有異步非阻塞(AIO)

同步阻塞

同一個線程在IO時一直阻塞,直到讀取數據成功,把數據從核心空間拷貝到用戶空間

同步非阻塞

同一個線程發起IO後,立即獲得返回,後面定期輪詢數據讀取情況,發現數據讀取成功,把數據從核心空間拷貝到用戶空間

異步非阻塞

一個線程發起IO後,立即返回,由另外的線程發現數據讀取成功,把數據從核心空間拷貝到用戶空間。

多路複用

select是幾乎所有unix、linux都支持的一種多路IO方式,通過select函數發出IO請求後,線程阻塞,一直到數據準備完畢,然後才能把數據從核心空間拷貝到用戶空間,所以select從內核上來說是同步阻塞方式。

poll對select的使用方法進行了一些改進,突破了最大文件數的限制,同時使用更加方便一些。

通過poll函數發出IO請求後,線程阻塞,直到數據準備完畢,poll函數在pollfd中通過revents字段返回事件,然後線程把數據從核心空間拷貝到用戶空間,所以poll同樣是同步阻塞方式,性能同select相比沒有改進。

epoll是linux爲了解決select/poll的性能問題而新搞出來的機制,基本的思路是:由專門的內核線程來不停地掃描fd列表,有結果後,把結果放到fd相關的鏈表中,用戶線程只需要定期從該fd對應的鏈表中讀取事件就可以了。同時,爲了節省把數據從核心空間拷貝到用戶空間的消耗,採用了mmap的方式,允許程序在用戶空間直接訪問數據所在的內核空間,不需要把數據copy一份。

epoll主要工作流程如下:

  1. 創建epoll文件描述符
  2. 把需要監聽的文件fd和事件加入到epoll文件描述符,也可以對已有的fd進行修改和刪除。文件fd保存在一個紅黑樹中,該fd的事件保存在一個鏈表中(每個fd一個事件鏈表),事件由內核線程負責填充,用戶線程讀取
  3. epoll_wait調用ep_poll,當rdlist爲空(無就緒fd)時掛起當前進程
  4. 文件fd狀態改變(buffer由不可讀變爲可讀或由不可寫變爲可寫),導致相應fd上的回調函數ep_poll_callback()被調用
  5. ep_poll_callback將相應fd對應epitem加入rdlist,導致rdlist不空,進程被喚醒,epoll_wait得以繼續執行。
  6. ep_events_transfer函數將rdlist中的epitem拷貝到txlist中,並將rdlist清空。
  7. ep_send_events函數掃描txlist中的每個epitem,調用其關聯fd對用的poll方法。此時對poll的調用僅僅是取得fd上較新的events(防止之前events被更新),之後將取得的events和相應的fd發送到用戶空間。事件發生後,讀取事件對應的epoll_data,該結構中包含了文件fd和數據地址,由於採用了mmap,程序可以直接讀取數據。

有人把epoll這種方式叫做同步非阻塞(NIO),因爲用戶線程需要不停地輪詢,自己讀取數據,看上去好像只有一個線程在做事情。

也有人把這種方式叫做異步非阻塞(AIO),因爲畢竟是內核線程負責掃描fd列表,並填充事件鏈表的。

個人認爲真正理想的異步非阻塞,應該是內核線程填充事件鏈表後,主動通知用戶線程,或者調用應用程序事先註冊的回調函數來處理數據,如果還需要用戶線程不停的輪詢來獲取事件信息,就不是太完美了,所以也有不少人認爲epoll是僞AIO。

LT模式與ET模式

以前select/poll中,每次遍歷fd列表,發現fd可寫、可讀或異常後,就把bit置1(select)或返回對應事件(poll)。

epoll同樣支持這種方式,每次fd可寫、可讀或異常後,就寫入事件到事件鏈表中。此外,epoll還支持只在事件發生變化時才寫入事件鏈表,例如如果事件一直是可讀,則只在第一次寫入鏈表。

這兩種方式分別叫做水平觸發(Level Triggered)和邊沿觸發(Edge Triggered),簡稱LT和ET。LT是缺省的工作方式,同時支持block和no-block socket,ET只支持no-block socket。異步事件驅動框架Netty底層採用的就是epoll + ET模式,而 JDK NIO中採用的是epoll + LT模式。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章