上兩篇文章講過了BIO與非阻塞IO以及IO多路複用,洋洋灑灑近3萬字。
這篇文章我們來聊一個很簡單,但是很多人往往分不清的一個問題,同步非同步、阻塞非阻塞到底怎麼區分?
開篇先問大家一個問題:IO多路複用是同步IO還是非同步IO?
先思考一下,再繼續往下讀。
鉅著《Unix網路程式設計》將IO模型劃分為5種,分別是
- 阻塞IO
- 非阻塞IO
- IO複用
- 訊號驅動IO
- 非同步IO
個人認為這麼分類並不是很好,因為從字面上理解阻塞IO和非阻塞IO就已經是數學意義上的全集了,怎麼又冒出了後邊3種模型,會給初學者帶來一些困擾。
接下來進入正文。
作者:蟬沐風,
公眾號:蟬沐風的碼場
1. 一個簡單的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完成之後透過訊號或回撥函式的方式將資料傳遞給使用者執行緒。如下圖所示。
因此,在理想的非同步環境下,資料準備階段和資料複製階段都是由核心完成的,不會對使用者執行緒進行阻塞,這種核心級別的改進自然需要作業系統底層的功能支援。
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
部分。
完~