出處
阻塞 IO, 非阻塞 IO, 同步 IO, 異步 IO 這些術語相信有不少朋友都也不同程度的困惑吧? 我原來也是, 什麼同步非阻塞 IO, 異步非阻塞 IO 的, 搞的頭都大了. 後來仔細讀了一遍《UNIX 網絡編程卷一 套接字聯網 API(第三版)》的 6.2 章節, 終於把這些名詞搞懂了.
下面我以《UNIX 網絡編程卷一 套接字聯網 API(第三版)》的 6.2 章節的內容為准, 整理了一下各種網絡 IO 模型具體定義以及一些容易混淆的地方.
簡介
阻塞式 I/O 模型
最流行的 IO 操作是阻塞式 IO(Blocking IO). 以 UDP 數據報套接字為例, 下圖是其阻塞 IO 的調用過程:
在上圖中, 進程調用 recvfrom, 其系統調用直到數據報返回並且被復制到應用進程的緩沖區中 或者發送錯誤時才返回. 因此進程在調用 recvfrom 開始到它返回的整段時間內都是被阻塞的.
非阻塞式 IO(Non-Blocking IO)
進程把一個套接字設置為非阻塞是在通知內核: 當調用線程所請求的 IO 操作需要調用線程休眠來等待操作完成時, 此時不要將調用線程休眠, 而是返回一個錯誤.
如上圖所示, 前三次調用 recvfrom 時, 沒有數據可返回, 因此內核轉而立即返回一個 EWOULDBLOCK 錯誤. 第四次調用 recvfrom 時, 已經有數據了, 此時, recvfrom 會阻塞住, 等待內核將數據賦值到應用進程的緩沖區中, 然後再返回.(注意, 當有數據時, recvfrom 是阻塞的, 它會等待內核將數據復制到應用進程的緩沖區後, 才返回).
當一個應用進程像這樣對一個非阻塞描述符循環調用 recvfrom 時, 我們稱之為輪詢(polling). 應用進程持續輪詢內核, 以查看某個操作是否完成, 這麼做會消耗大量的 CPU 時間, 不過這種模型偶爾也會遇到, 通常是專門提供某一種功能的系統中才有.
IO 復用模型
有了 IO 復用(IO multiplexing), 我們就可以調用 select 或 poll, 阻塞在這兩個系統調用中的某一個之上, 而不是阻塞在真正的 IO 系統調用上. 例如:
如上圖所示, 當調用了 select 後, select 會阻塞住, 等待數據報套接字變為可讀. 當 select 返回套接字可讀這一條件時, 我們就可以調用 recvfrom 把所讀取的數據報復制到應用進程緩沖區.
對比阻塞式 IO, IO 復用模型優勢並不明顯, 並且從使用方式來說, IO 復用模型還需要多調用一次 select, 因此從易用性上來說, 比阻塞式 IO 還略有不足. 不過 select 的殺手锏在於它可以監聽多個文件描述符, 大大減小了阻塞線程的個數.
信號驅動 IO 模型
信號驅動模型如上圖所示. 當文件描述符就緒時, 我們可以讓內核以信號的方式通知我們.我們首先需要開啟套接字的信號驅動式 IO 功能, 並通過 sigaction 系統調用安裝一個信號處理函數. sigaction 系統調用是異步的, 它會立即返回. 當有數據時, 內核會給此進程發送一個 SIGIO 信號, 進而我們的信號處理函數就會被執行, 我們就可以在這個函數中調用 recvfrom 讀取數據.
異步 IO 模型
異步 IO (asynchronous IO) 由 POSIX 規范定義, 在 POSIX 中定義了若干個異步 IO 的操作函數. 這個函數的工作原理是: 告知內核啟動某個動作, 並讓內核在整個操作(包括將數據從內核復制到應用進程緩沖區)完成後通知我們的應用進程.
異步 IO 模型和信號驅動的 IO 模型的主要區別在於: 信號驅動 IO 是由內核通知我們何時可以啟動一個 IO 操作, 而異步 IO 模型是由內核通知我們 IO 操作何時完成.
異步 IO 模型的操作過程如圖所示:
當我們調用 aio_read 函數時(POSIX 異步 IO 函數以 aio_或 lio_ 開頭), 給內核傳遞描述符, 緩沖區指針, 緩沖區大小(和 read 相同的三個參數) 和文件偏移(以 lseek 類似), 並告訴內核當整個操作完成時如何通知應用進程. 該系統調用立即返回, 而且在等待 IO 完成期間, 應用進程不被阻塞.
各種 IO 模型的比較
如圖所示, 上述五中 IO 模型中, 前四種模型(阻塞 IO, 非阻塞 IO, IO 復用, 信號驅動 IO)的主要區別在於第一階段, 因為他們的第二階段是一樣的: 在數據從內核復制到調用者的緩沖區期間, 進程阻塞於 recvfrom 調用. 而第五種, 即異步 IO 模型中, 兩個階段都不需要應用進程處理, 內核為我們處理好了數據的等待和數據的復制過程.
關於同步 IO 和異步 IO
根據 POSIX 定義:
根據上述定義, 我們的前四種模型: 阻塞 IO 模型, 非阻塞 IO 模型, IO 復用模型和信號驅動 IO 模型都是同步 IO 模型, 因為其中真正的 IO 操作(recvfrom 調用) 會阻塞進程(因為當有數據時, recvfrom 會阻塞等待內核將數據從內核空間復制到應用進程空間, 當賦值完成後, recvfrom 才返回.) 只有異步 IO 模型與 POSIX 定義的異步 IO 相匹配.
總結
在處理網絡 IO 操作時, 阻塞和非阻塞 IO 都是同步 IO.只有調用了特殊的 API 才是異步 IO.
因此網上常說的 "同步阻塞 IO", "同步非阻塞 IO" 其實就是阻塞 IO 模型和非阻塞 IO 模型, 因為阻塞 IO 和非阻塞 IO 模型都是同步的, 加了 "同步" 二字其實是多余了.網絡上常說的 "異步非阻塞 IO" 其實就是異步 IO 模型.