动态

详情 返回 返回

異步阻塞IO是什麼鬼? - 动态 详情

這篇文章我們來聊一個很簡單,但是很多人往往分不清的一個問題,同步異步、阻塞非阻塞到底怎麼區分?

開篇先問大家一個問題:IO多路複用是同步IO還是異步IO

先思考一下,再繼續往下讀。


鉅著《Unix網絡編程》將IO模型劃分為5種,分別是

  • 阻塞IO
  • 非阻塞IO
  • IO複用
  • 信號驅動IO
  • 異步IO

個人認為這麼分類並不是很好,因為從字面上理解阻塞IO和阻塞IO就已經是數學意義上的全集了,怎麼又冒出了後邊3種模型,會給初學者帶來一些困擾。

接下來進入正文。

文章首發於公眾號:蟬沐風的碼場

1. 一個簡單的IO流程

讓我們先摒棄我們原本熟知的各種IO模型流程圖,先看一個非常簡單的IO流程,不涉及任何阻塞非阻塞、同步異步概念的圖。

IO流程

客户端發起系統調用之後,內核的操作可以被分成兩步:

  • 等待數據

    此階段網絡數據進入網卡,然後網卡將數據放到指定的內存位置,此過程CPU無感知。然後經過網卡發起硬中斷,再經過軟中斷,內核線程將數據發送到socket的內核緩衝區中。

  • 數據拷貝

    數據從socket的內核緩衝區拷貝到用户空間

2. 阻塞與非阻塞

阻塞與非阻塞在API上區別在於socket是否設置了SOCK_NONBLOCK這個參數,默認情況下是阻塞的,設置了該參數則為非阻塞。

2.1 阻塞

假設socket為阻塞模式,則IO調用如下圖所示。

阻塞示意圖

當處於運行狀態的用户線程發起recv系統調用時,如果socket內核緩衝區內沒有數據,則內核會將當前線程投入睡眠,讓出CPU的佔用。

直到網絡數據到達網卡,網卡DMA數據到內存,再經過硬中斷、軟中斷,由內核線程喚醒用户線程。

此時socket的數據已經準備就緒,用户線程由用户態進入到內核態,執行數據拷貝,將數據從內核空間拷貝到用户空間,系統調用結束。此階段,開發者通常認為用户線程處於等待(稱為阻塞也行)狀態,因為在用户態的角度上,線程確實啥也沒幹(雖然在內核態幹得累死累活)。

2.2 非阻塞

如果將socket設置為非阻塞模式,調用便換了一副光景。

非阻塞示意圖

用户線程發起系統調用,如果socket內核緩衝區中沒有數據,則系統調用立即返回,不會掛起線程。而線程會繼續輪詢,直到socket內核緩衝區內有數據為止。

如果socket內核緩衝區內有數據,則用户線程進入內核態,將數據從內核空間拷貝到用户空間,這一步和2.1小節沒有區別。

3. 同步與異步

同步異步主要看請求發起方對消息結果的獲取方式,是主動獲取還是被動通知。區別主要體現在數據拷貝階段。

3.1 同步

同步我們其實已經見識過了,2.1節和2.2節中的數據拷貝階段其實都是同步!

注:把同步的流程畫在阻塞和非阻塞的第二階段,並不是説阻塞和非阻塞的第二階段只能搭配同步手段!

同步指的是數據到達socket內核緩衝區之後,由用户線程參與到數據拷貝過程中,直到數據從內核空間拷貝到用户空間。

因此,IO多路複用,對於應用程序而言,仍然只能算是一種同步,因為應用程序仍然花費時間等待IO結果,等待期間CPU要麼用於遍歷文件描述符的狀態,要麼用於休眠等待事件發生。

select為例,用户線程發起select調用,會切換到內核空間,如果沒有數據準備就緒,則用户線程阻塞到有數據來為止,select調用結束。結束之後用户線程獲取到的只是「內核中有N個socket已經就緒」的這麼一個信息,還需要用户線程對着1024長度的描述符數組進行遍歷,才能獲取到socket中的數據,這就是同步。

舉個生活中的例子,我們給物流客服打電話詢問我們的包裹是否已到達,如果未到達,我們就先睡一會兒,等到了之後客服給我們打電話把我們喊起來,然後我們屁顛屁顛地去快遞驛站拿快遞。這就是同步阻塞。

如果我們不想睡,就一直打電話問,直到包裹到了為止,然後再屁顛屁顛地去快遞驛站拿快遞。這就是同步非阻塞。

問題就是,能不能直接讓物流的人把快遞直接送到我家,別讓我自己去拿啊!這就是異步。

3.2 理想的異步

我們理想中的完美異步應該是用户進程發起非阻塞調用,內核直接返回結果之後,用户線程可以立即處理下一個任務,只需要IO完成之後通過信號或回調函數的方式將數據傳遞給用户線程。如下圖所示。

理想的異步IO

因此,在理想的異步環境下,數據準備階段和數據拷貝階段都是由內核完成的,不會對用户線程進行阻塞,這種內核級別的改進自然需要操作系統底層的功能支持。

3.3 現實的異步

現實比理想要骨感一些。

Linux內核並沒有太惹眼的異步IO機制,這難不倒各路大神,比如Node的作者採用多線程模擬了這種異步效果。

比如讓某個主線程執行主要的非IO邏輯操作,另外再起多個專門用於IO操作的線程,讓IO線程進行阻塞IO或者非阻塞IO加輪詢的方式來完成數據獲取,通過IO線程和主線程之間通信進行數據傳遞,以此來實現異步。

多線程模擬異步

還有一種方案是Windows上的IOCP,它在某種程度上提供了理想的異步,其內部依然採用的是多線程的原理,不過是內核級別的多線程。

遺憾的是,用Windows做服務器的項目並不是特別多,期待Linux在異步的領域上取得更大的進步吧。

4. 異步阻塞?

説完了同步異步、阻塞非阻塞,一個很自然的操作就是對他們進行排列組合。

  • 同步阻塞
  • 同步非阻塞
  • 異步非阻塞
  • 異步阻塞

但是異步阻塞是什麼鬼?按照上文的解釋,該IO模型在第一階段應該是用户線程阻塞,等待數據;第二階段應該是內核線程(或專門的IO線程)處理IO操作,然後把數據通過事件或者回調的方式通知用户線程,既然如此,那麼第一步的阻塞完全沒有必要啊!非阻塞調用,然後繼續處理其他任務豈不是更好。

因此,壓根不存在異步阻塞這種模型哦

5. 千萬分清主語是誰

最後給各位提個醒,和別人討論阻塞非阻塞的時候千萬要帶上主語。

如果我問你,epoll是阻塞還是非阻塞?你怎麼回答?

應該説,epoll_wait這個函數本身是阻塞的,但是epoll會將socket設置為非阻塞。因此單純把epoll認為阻塞是太委屈它,認為其是非阻塞又抬舉它。

具體關於epoll的説明可以參見IO多路複用中的epoll部分。


完~

Add a new 评论

Some HTML is okay.