原文地址(略有修改):阻塞、非阻塞、異步、同步以及select/poll和epoll
針對IO,總是涉及到阻塞、非阻塞、異步、同步以及select/poll和epoll的一些描述,那麼這些東西到底是什麼,有什麼差異?
一般來講一個IO分爲兩個階段:
- 等待數據到達
- 把數據從內核空間拷貝到用戶空間
現在假設一個進程/線程A,發出IO請求,有兩種情況:
- 立即返回
- 由於數據未準備好,需要等待,讓出CPU給別的線程,自己sleep
第一種情況就是非阻塞,A爲了知道數據是否準備好,需要不停的詢問,而在輪詢的空歇期,理論上是可以乾點別的活,例如喝喝茶、泡個妞。第二種情況就是阻塞,A除了等待就不能做任何事情。
數據終於準備好了,A現在要把數據取回去,有幾種做法:
- A自己把數據從內核空間拷貝到用戶空間。
- 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主要工作流程如下:
- 創建epoll文件描述符
- 把需要監聽的文件fd和事件加入到epoll文件描述符,也可以對已有的fd進行修改和刪除。文件fd保存在一個紅黑樹中,該fd的事件保存在一個鏈表中(每個fd一個事件鏈表),事件由內核線程負責填充,用戶線程讀取
- epoll_wait調用ep_poll,當rdlist爲空(無就緒fd)時掛起當前進程
- 文件fd狀態改變(buffer由不可讀變爲可讀或由不可寫變爲可寫),導致相應fd上的回調函數ep_poll_callback()被調用
- ep_poll_callback將相應fd對應epitem加入rdlist,導致rdlist不空,進程被喚醒,epoll_wait得以繼續執行。
- ep_events_transfer函數將rdlist中的epitem拷貝到txlist中,並將rdlist清空。
- 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模式。