一文讓你讀懂懂阻塞、非阻塞、同步、異步IO

介紹

在談及網絡IO的時候總避不開阻塞、非阻塞、同步、異步、IO多路複用、select、poll、epoll等這幾個詞語。在面試的時候也會被常常問到這幾個的區別。本文就來說一下這幾個詞語的含義、區別以及使用方式。react

Unix網絡編程一書中做者給出了五種IO模型:nginx

一、BlockingIO - 阻塞IO面試

二、NoneBlockingIO - 非阻塞IO編程

三、IO multiplexing - IO多路複用數組

四、signal driven IO - 信號驅動IO服務器

五、asynchronous IO - 異步IO網絡

這五種IO模型中前四個都是同步的IO,只有最後一個是異步IO。信號驅動IO使用的比較少,重點介紹其餘幾種IO以及在Java中的應用。app

 

阻塞、非阻塞、同步、異步以及IO多路複用框架

在進行網絡IO的時候會涉及到用戶態和內核態,而且在用戶態和內核態之間會發生數據交換,從這個角度來講咱們能夠把IO抽象成兩個階段:一、用戶態等待內核態數據準備好,二、將數據從內核態拷貝到用戶態。之因此會有同步、異步、阻塞和非阻塞這幾種說法就是根據程序在這兩個階段的處理方式不一樣而產生的。異步

同步阻塞

一文讓你讀懂懂阻塞、非阻塞、同步、異步IO

當在用戶態調用read操做的時候,若是這時候kernel尚未準備好數據,那麼用戶態會一直阻塞等待,直到有數據返回。當kernel準備好數據以後,用戶態繼續等待kernel把數據從內核態拷貝到用戶態以後纔可使用。這裏會發生兩種等待:一個是用戶態等待kernel有數據能夠讀,另一個是當有數據可讀時用戶態等待kernel把數據拷貝到用戶態。

在Java中同步阻塞的實現對應的是傳統的文件IO操做以及Socket的accept的過程。在Socket調用accept的時候,程序會一直等待知道有描述符就緒,而且把就緒的數據拷貝到用戶態,而後程序中就能夠拿到對應的數據。

同步非阻塞

一文讓你讀懂懂阻塞、非阻塞、同步、異步IO

對比第一張同步阻塞IO的圖就會發現,在同步非阻塞模型下第一個階段是不等待的,不管有沒有數據準備好,都是當即返回。第二個階段仍然是須要等待的,用戶態須要等待內核態把數據拷貝過來才能使用。對於同步非阻塞模式的處理,須要每隔一段時間就去詢問一下內核數據是否是能夠讀了,若是內核說能夠,那麼就開始第二階段等待。

IO多路複用

IO多路複用也是同步的。

一文讓你讀懂懂阻塞、非阻塞、同步、異步IO

IO多路複用的方式看起來跟同步阻塞是同樣的,兩個階段都是阻塞的,可是IO多路複用能夠實現以較小的代價同時監聽多個IO。一般狀況下是經過一個線程來同時監聽多個描述符,只要任何一個知足就緒條件,那麼內核態就返回。IO多路複用使得傳統的每請求每線程的處理方式獲得解耦,一個線程能夠同時處理多個IO請求,而後交到後面的線程池裏處理,這也是netty等框架的處理方式,所謂的reactor模式。IO多路複用的實現依賴於操做系統的select、poll和epoll,後面會詳細介紹這幾個系統調用。

IO多路複用在Java中的實現方式是在Socket編程中使用非阻塞模式,而後配置感興趣的事件,經過調用select函數來實現。select函數就是對應的第一個階段。若是給select配置了超時參數,在指定時間內沒有感興趣事件發生的話,select調用也會返回,這也是爲何要作非阻塞模式下運行。

異步IO

一文讓你讀懂懂阻塞、非阻塞、同步、異步IO

異步模式下,前面提到的兩個階段都不會等待。使用異步模式,用戶態調用read方法的時候,至關於告訴內核數據發送給我以後告訴我一聲我先去幹別的事情了。在這兩個階段都不會等待,只須要在內核態通知數據準備好以後使用便可。一般狀況下使用異步模式都會使用callback,當數據可用以後執行callback函數。

 

IO多路複用

如今用Java開發的網絡服務器一般採用IO多路複用的方式來加快網絡IO操做,例如Netty、Tomcat等。IO多路複用的基礎是select、poll和epoll。這三個函數是從操做系統的角度上支持的IO多路複用的操做,下面就分別來看一下這三個函數。

select

函數簽名以下:

int select(int maxfdp1, fd_set *readset,fd_set *writeset,fd_set *exceptset,const struct timeval *timeout)

maxfdp1爲指定的待監聽的描述符的個數,由於描述符是從0開始的,因此須要加1

readset爲要監聽的讀描述符

writeset爲要監聽的寫描述符

exceptset爲要監聽的異常描述符

timeout監聽沒有準備好的描述符的話,多久能夠返回,支持按照秒或者毫秒來配置時間

select操做的邏輯是首先將要監聽的讀、寫以及異常描述符拷貝到內核空間,而後遍歷全部的描述符,若是有感興趣的事件發生,那麼就返回。

select在使用的過程當中有三個問題:

一、被監控的fds(描述符)集合限制爲1024,1024過小了

二、須要將描述符集合從用戶空間拷貝到內核空間

三、當有描述符可操做的時候都須要遍歷一下整個描述符集合才能知道哪一個是可操做的,效率很低。

poll

函數簽名以下:

int poll(struct pollfd[] fds, unsigned int nfds, int timeout);

poll操做與select操做相似,仍舊避免不了描述符從用戶空間拷貝到內核空間,可是poll再也不有1024個描述符的限制。對於事件的觸發通知仍是使用遍歷全部描述符的方式,所以在大量鏈接的狀況下也存在遍歷低效的問題。poll函數在傳遞參數的時候統一的將要監聽的描述符和事件封裝在了pollfd結構體數組中。

epoll

epoll有三個方法:epoll_create、epoll_ctl和epoll_wait。epoll_create是建立一個epoll句柄;epoll_ctl是註冊要監聽的事件類型;epoll_wait則是等待事件的產生。 經過這三個方法epoll解決了select的三個問題。

一、1024數量限制的問題

經過epoll_create方法來建立一個epoll句柄,這個句柄監聽的描述符的數量再也不有限制。

二、文件描述符頻繁從用戶空間拷貝到內核空間的問題

經過觀察select的操做會發現描述符從用戶空間到內核空間拷貝發生在調用select方法的時候,只要沒有註冊新的事件或者取消註冊事件,每次拷貝的描述符都是同樣的。所以epoll引入了epoll_ctl調用,該方法用於註冊新事件和取消註冊事件。而在epoll_wait的時候並不會拷貝描述符,描述符始終存在於內核空間,當須要修改的時候只要調用epoll_ctl修改一下內核的描述符便可。如此一來便省去了描述符來回拷貝的開銷。

三、文件描述符可操做的時候遍歷整個描述符集合的問題

在調用epoll_ctl註冊感興趣的事件的時候,實際上會爲設置的事件添加一個回調函數,當對應的感興趣的事件發生的時候,回調函數就會觸發,而後將本身加到一個鏈表中。epoll_wait函數的做用就是去查看這個鏈表中有沒有已經準備就緒的事件,若是有的話就通知應用程序處理,如此操做epoll_wait只須要遍歷就緒的事件描述符便可。

 

epoll在Java中的使用

目前針對Java服務器的非阻塞編程基本都是基於epoll的。在進行非阻塞編程的時候有兩個步驟:一、註冊感興趣的事情;二、調用select方法,查找感興趣的事件。

註冊感興趣的事件

咱們在編寫Socket的非阻塞代碼的時候須要在Selector上註冊感興趣的事情,一般寫法是serverSocketChannel.register(selector, SelectionKey.XXX)。來看一下這行代碼背後的執行邏輯是什麼樣的。

一文讓你讀懂懂阻塞、非阻塞、同步、異步IO

註冊的時候實際執行的是EPollSelectorImp。該方法主要有如下三步:

一、implRegister方法。在fdToKey的Map中插入channel對應的文件描述法和SelectionKey的映射,當作註冊Channel、關閉Channel、取消註冊等操做是都是操做此Map。

二、往pollWrapper[Epoll實例]中放入channel實例。

三、往keys[HashSet]中放入SelectionKey

select方法

經過Java的Selector.select方法來獲取準備好的鍵的時候實際執行的代碼以下:

一文讓你讀懂懂阻塞、非阻塞、同步、異步IO

首先調用EPollArrayWrapper的poll方法,該方法作兩件事:一、調用epollCtl方法向epoll中註冊感興趣的事件;二、調用epollWait方法返回已就緒的文件描述符集合

而後調用updateSelectedKeys方法調用把epoll中就緒的文件描述符加到ready隊列中等待上層應用處理, updateSelectedKeys經過fdToKey查找文件描述符對應的SelectionKey,並在SelectionKey對應的channel中添加對應的事件到ready隊列。

 

水平觸發LT與邊緣觸發ET

epoll支持兩種觸發模式,分別是水平觸發和邊緣觸發。

LT是缺省的工做方式,而且同時支持block和no-block socket。在這種作法中,內核告訴你一個文件描述符是否就緒了,而後你能夠對這個就緒的fd進行IO操做。若是你不做任何操做,內核仍是會繼續通知你的。

ET是高速工做方式,只支持no-block socket。在這種模式下,當描述符從未就緒變爲就緒時,內核會通知你一次,而且除非你作了某些操做致使那個文件描述符再也不爲就緒狀態了,不然不會再次發送通知。

能夠看到,原本內核在被DMA中斷,捕獲到IO設備來數據後,只須要查找這個數據屬於哪一個文件描述符,進而通知線程裏等待的函數便可,可是,LT要求內核在通知階段還要繼續再掃描一次剛纔所創建的內核fd和io對應的那個數組,由於應用程序可能沒有真正去讀上次通知有數據後的那些fd,這種溝通方式效率是很低下的,只是方便編程而已;

JDK並無實現邊緣觸發,關於邊緣觸發和水平觸發的差別簡單列舉以下,邊緣觸發的性能更高,但編程難度也更高,netty就從新實現了Epoll機制,採用邊緣觸發方式;另外像nginx等也採用的是邊緣觸發。

相關文章
相關標籤/搜索