一篇文章讀懂阻塞,非阻塞,同步,非同步

weixin_33670713發表於2018-05-14

我們在進行程式設計開發的時候,經常會涉及到同步,非同步,阻塞,非阻塞,IO多路複用等概念,這幾個概念有區別,但是有時候也容易混淆,如果不總結一下的話很容易受到困擾,下面就記錄一下這幾個概念的理解。

Unix網路程式設計中的五種IO模型

  • Blocking IO - 阻塞IO
  • NoneBlocking IO - 非阻塞IO
  • IO multiplexing - IO多路複用
  • signal driven IO - 訊號驅動IO
  • asynchronous IO - 非同步IO

由於signal driven IO在實際使用中並不常用,所以這裡只討論剩下的四種IO模型。

在討論之前先說明一下IO發生時涉及到的物件和步驟,對於一個network IO,它會涉及到兩個系統物件:

  • application 呼叫這個IO的程式
  • kernel 系統核心

那他們經歷的兩個互動過程是:

  • 階段1 wait for data 等待資料準備
  • 階段2 copy data from kernel to user 將資料從核心拷貝到使用者程式中

之所以會有同步、非同步、阻塞和非阻塞這幾種說法就是根據程式在這兩個階段的處理方式不同而產生的。瞭解了這些背景之後,我們就分別針對四種IO模型進行講解

Blocking IO - 阻塞IO

在linux中,預設情況下所有的socket都是blocking,一個典型的讀操作流程大概如下圖:


11224747-02876ed5afe356ff.gif
blocking I O.gif

當使用者程式呼叫了recvfrom這個系統呼叫,kernel就開始了IO的第一個階段:準備資料。對於network IO來說,很多時候資料在一開始還沒有到達(比如,還沒有收到一個完整的UDP包),這個時候kernel就要等待足夠的資料到來。而在使用者程式這邊,整個程式會被阻塞。當kernel一直等到資料準備好了,它就會將資料從kernel中拷貝到使用者記憶體,然後kernel返回結果,使用者程式才解除block的狀態,重新執行起來。

所以,blocking IO的特點就是在IO執行的兩個階段都被block了。

NoneBlockingIO - 非阻塞IO

linux下,可以通過設定socket使其變為non-blocking。當對一個non-blocking socket執行讀操作時,流程是這個樣子:


11224747-e4c17ed342162afc.gif
NonBlocking IO.gif

從圖中可以看出,當使用者程式發出recvfrom這個系統呼叫後,如果kernel中的資料還沒有準備好,那麼它並不會block使用者程式,而是立刻返回一個結果(no datagram ready)。從使用者程式角度講 ,它發起一個操作後,並沒有等待,而是馬上就得到了一個結果。使用者程式得知資料還沒有準備好後,它可以每隔一段時間再次傳送recvfrom操作。一旦kernel中的資料準備好了,並且又再次收到了使用者程式的system call,那麼它馬上就將資料拷貝到了使用者記憶體,然後返回。

所以,使用者程式其實是需要不斷的主動詢問kernel資料好了沒有。

IO multiplexing - IO多路複用

I/O多路複用(multiplexing)是網路程式設計中最常用的模型,像我們最常用的select、epoll都屬於這種模型。以select為例:


11224747-dcc024f7fa2e8460.gif
multiplexing IO.gif

看起來它與blocking I/O很相似,兩個階段都阻塞。但它與blocking I/O的一個重要區別就是它可以等待多個資料包就緒(datagram ready),即可以處理多個連線。這裡的select相當於一個“代理”,呼叫select以後程式會被select阻塞,這時候在核心空間內select會監聽指定的多個datagram (如socket連線),如果其中任意一個資料就緒了就返回。此時程式再進行資料讀取操作,將資料拷貝至當前程式內。由於select可以監聽多個socket,我們可以用它來處理多個連線。

在select模型中每個socket一般都設定成non-blocking,雖然等待資料階段仍然是阻塞狀態,但是它是被select呼叫阻塞的,而不是直接被I/O阻塞的。select底層通過輪詢機制來判斷每個socket讀寫是否就緒。

當然select也有一些缺點,比如底層輪詢機制會增加開銷、支援的檔案描述符數量過少等。為此,Linux引入了epoll作為select的改進版本。

asynchronous IO - 非同步IO

非同步I/O在網路程式設計中幾乎用不到,在File I/O中可能會用到:


11224747-05e3a70e98d2331e.gif
asynchronous IO.gif

這裡面的讀取操作的語義與上面的幾種模型都不同。這裡的讀取操作(aio_read)會通知核心進行讀取操作並將資料拷貝至程式中,完事後通知程式整個操作全部完成(繫結一個回撥函式處理資料)。讀取操作會立刻返回,程式可以進行其它的操作,所有的讀取、拷貝工作都由核心去做,做完以後通知程式,程式呼叫繫結的回撥函式來處理資料。

總結

我們來總結一下阻塞、非阻塞,同步和非同步這兩組概念。

先來說阻塞和非阻塞:

  • 阻塞呼叫會一直等待遠端資料就緒再返回,即上面的階段1會阻塞呼叫者,直到讀取結束。
  • 而非阻塞無論在什麼情況下都會立即返回,雖然非阻塞大部分時間不會被block,但是它仍要求程式不斷地去主動詢問kernel是否準備好資料,也需要程式主動地再次呼叫recvfrom來將資料拷貝到使用者記憶體。

再說一說同步和非同步:

  • 同步方法會一直阻塞程式,直到I/O操作結束,注意這裡相當於上面的階段1,階段2都會阻塞呼叫者。其中 Blocking IO - 阻塞IO,Nonblocking IO - 非阻塞IO,IO multiplexing - IO多路複用,signal driven IO - 訊號驅動IO 這四種IO都可以歸類為同步IO。
  • 而非同步方法不會阻塞呼叫者程式,即使是從核心空間的緩衝區將資料拷貝到程式中這一操作也不會阻塞程式,拷貝完畢後核心會通知程式資料拷貝結束。

下面的這張圖很好地總結了之前講的這五種I/O模型(來自Unix Network Programming)


11224747-aaabc156a42177d4.png
io-modelposix-io-model-comparison.png

最後,在舉個簡單的例子幫助理解,比如我們怎樣解決午飯問題:

A君喜歡下館子吃飯,服務員點完餐後,A君一直坐在座位上等待廚師炒菜,什麼事情也沒有幹,過了一會服務員端上飯菜後,A君就開吃了 -- 【阻塞I/O】

B君也喜歡下館子,服務員點完餐後,B君看這個服務員姿色不錯,便一直和服務員聊人生理想,並時不時的打聽自己的飯做好了沒有,過了一會飯也做好了,B君也撩到了美女服務員的微訊號 -- 【非阻塞I/O 】順便撩了個妹子☺

C君同樣喜歡下館子吃飯,但是C君不喜歡一個人下館子吃,要呼朋喚友一起下館子,但是這幫人到了飯店之後,每個人只點自己的,服務員一起給他們下單後,就交給後廚去做了,每做好一個人的,服務員就負責給他們端上來。做他們的服務員真滴好累? -- 【IO多路複用】

D君比較宅,不喜歡下館子,那怎麼辦呢?美團外賣啊(此處應有廣告費:-D)手機下單後,自己啥也不用操心,只要等快遞小哥上門就行了,這段時間可以擼好幾把王者農藥的了,嘿嘿 -- 【非同步IO】

相關文章