所謂 I/O,就是 Input/Output,輸入/輸出,在操作系統(tǒng)中,輸入輸出操作其實并不簡單
工作在用戶態(tài)的應(yīng)用程序想要讀取磁盤中的具體文件內(nèi)容,就需要經(jīng)過 System Call(系統(tǒng)調(diào)用)陷入內(nèi)核態(tài)
因此,在操作系統(tǒng)中,輸入輸出操作通常都會包括以下兩個階段:
- 準(zhǔn)備數(shù)據(jù):內(nèi)核緩沖區(qū)準(zhǔn)備數(shù)據(jù),等待其準(zhǔn)備好
- 數(shù)據(jù)拷貝:從內(nèi)核緩沖區(qū)向用戶緩沖區(qū)復(fù)制數(shù)據(jù)
以網(wǎng)絡(luò)通信即 Socket 上的輸入操作為例,對應(yīng)的第一階就是等待數(shù)據(jù)從網(wǎng)絡(luò)中到達(dá)網(wǎng)卡(對于網(wǎng)絡(luò) I/O 來說,很多時候數(shù)據(jù)在一開始還沒有到達(dá)。比如,還沒有收到一個完整的 TCP 包。這個時候內(nèi)核就要等待足夠的數(shù)據(jù)到來),然后從網(wǎng)卡中將數(shù)據(jù)拷貝到內(nèi)核緩沖區(qū),這樣,數(shù)據(jù)就準(zhǔn)備就完成了;第二階段就是把數(shù)據(jù)從內(nèi)核緩沖區(qū)復(fù)制到用戶緩沖區(qū)。
操作系統(tǒng)系統(tǒng)如何去管理輸入和輸出,從而獲取輸入和輸出的數(shù)據(jù)?這就是 I/O 模型。
linux 中有以下五種 I/O 模型:
- Blocking I/O:阻塞 I/O
- Non-Blocking I/O:非阻塞 I/O
- I/O Multiplexing:I/O 多路復(fù)用
- Signal Blocking I/O:信號驅(qū)動 I/O
- Asynchronous I/O:異步 I/O
Blocking I/O
在 Linux 中,默認(rèn)情況下所有的 Socket 都是 Blocking,它符合人們最常見的思考邏輯。
上面我們介紹了輸入輸出操作通常都會包括兩個階段,并不是憑空想想,而是對應(yīng)具體的 I/O 系統(tǒng)調(diào)用的,以網(wǎng)絡(luò)通信為例,Blocking I/O 就對應(yīng)阻塞的系統(tǒng)調(diào)用 recvfrom
第一階段,準(zhǔn)備數(shù)據(jù):當(dāng)用戶進(jìn)程通過系統(tǒng)調(diào)用 recvfrom 進(jìn)行數(shù)據(jù)讀取,操作系統(tǒng)就開始了 I/O 的第一個階段-準(zhǔn)備數(shù)據(jù)。這個過程需要等待,也就是說數(shù)據(jù)被拷貝到操作系統(tǒng)內(nèi)核的緩沖區(qū)中是需要一個過程的。而在用戶進(jìn)程這邊,整個進(jìn)程會被阻塞住
解釋下 阻塞 的概念:源自操作系統(tǒng)對進(jìn)程/線程狀態(tài)的描述概念,其定義為:操作系統(tǒng)把進(jìn)程/線程從“運(yùn)行(running)狀態(tài)” 掛起為 “阻塞(blocked)狀態(tài)”(又稱“等待(waiting)狀態(tài)”)。當(dāng)進(jìn)程/線程處于阻塞狀態(tài),則意味著其處于暫停運(yùn)行狀態(tài),暫時不會被 CPU 調(diào)度執(zhí)行
第二階段,數(shù)據(jù)拷貝:當(dāng)內(nèi)核一直等到數(shù)據(jù)準(zhǔn)備好了,它就會將數(shù)據(jù)從內(nèi)核空間中拷貝到用戶空間,然后系統(tǒng)調(diào)用 recvfrom 返回結(jié)果,用戶進(jìn)程才解除阻塞的狀態(tài),重新運(yùn)行起來
在上述步驟中,用戶進(jìn)程調(diào)用 recvfrom,該系統(tǒng)調(diào)用直到數(shù)據(jù)準(zhǔn)備好且被復(fù)制到用戶緩沖區(qū)中才返回。
從調(diào)用 recvfrom 開始,到它返回數(shù)據(jù)的整段時間,用戶進(jìn)程都是被阻塞住的!這就是 Blocking I/O 的特點,可以簡單記憶為 “IO 執(zhí)行的兩個階段用戶進(jìn)程都被阻塞住了”
recvfrom 成功返回后,用戶進(jìn)程才開始繼續(xù)處理。
Non-Blocking I/O
參考《Unix 網(wǎng)絡(luò)編程:第一卷》,書中是這樣描述 Non-Blocking I/O 的:
"進(jìn)程把一個套接字設(shè)置成非阻塞是在通知內(nèi)核,當(dāng)所請求的 I/O 操作非得把本進(jìn)程投入睡眠才能完成時,不要把進(jìn)程投入睡眠,而是返回一個錯誤"
意思就是,如果某個用戶進(jìn)程進(jìn)行系統(tǒng)調(diào)用 recvform 嘗試獲取數(shù)據(jù),但這時候數(shù)據(jù)還沒準(zhǔn)備好:
- 如果操作系統(tǒng)把這個進(jìn)程掛起,那就是 Blocking I/O
- 如果操作系統(tǒng)選擇立即給用戶進(jìn)程返回錯誤信息,那就是 Non-Blocking I/O
如下圖所示:
非阻塞的 recvform? 系統(tǒng)調(diào)用之后,如果數(shù)據(jù)還沒準(zhǔn)備好,應(yīng)用進(jìn)程不會被阻塞住,recvfrom? 立即返回一個 EWOULDBLOCK? 錯誤。用戶進(jìn)程在收到 recvfrom 調(diào)用的返回信息之后,可以干點別的事情,然后再發(fā)起 recvform 系統(tǒng)調(diào)用。
重復(fù)上面的過程,不斷地進(jìn)行 recvform 系統(tǒng)調(diào)用。這個過程通常被稱之為**輪詢 (polling)**。輪詢檢查內(nèi)核數(shù)據(jù),直到數(shù)據(jù)準(zhǔn)備好,再拷貝數(shù)據(jù)到用戶進(jìn)程,進(jìn)行數(shù)據(jù)處理。
需要注意的是,當(dāng) recvfrom 系統(tǒng)調(diào)用進(jìn)行拷貝數(shù)據(jù)的時候,用戶進(jìn)程同樣是被阻塞住的。
因此,Non-Blocking I/O 的特點就是用戶進(jìn)程需要不斷的主動詢問內(nèi)核數(shù)據(jù)準(zhǔn)備好了沒有,可以簡單記憶為 “IO 執(zhí)行的第一階段用戶進(jìn)程非阻塞,第二階段用戶進(jìn)程阻塞”
I/O Multiplexing
由于 Non-Blocking I/O 需要不斷主動輪詢,輪詢會消耗大量的 CPU 時間,而后臺可能有多個任務(wù)在同時進(jìn)行,人們就想到了循環(huán)查詢多個任務(wù)的完成狀態(tài),只要有任何一個任務(wù)完成,就去處理它。這就是 I/O Multiplexing。
I/O Multiplexing 引入了新的系統(tǒng)調(diào)用 select/poll/epoll(也成為多路復(fù)用器),這幾個系統(tǒng)調(diào)用也是重點,不過本文就不過多闡述了。
具體來說,I/O Multiplexing 就是將多個應(yīng)用進(jìn)程的 Socket 注冊到一個多路復(fù)用器(select/poll/epoll)上,然后使用一個進(jìn)程來監(jiān)聽該多路復(fù)用器,多路復(fù)用器會不斷的輪詢所有注冊進(jìn)來的 Socket,只要有一個 Socket 的數(shù)據(jù)準(zhǔn)備好,就會返回該 Socket。再由應(yīng)用進(jìn)程發(fā)起真正的 IO 系統(tǒng)調(diào)用(也就是 recvfrom,和 Blocking I/O 一樣),來完成數(shù)據(jù)讀取。
簡單來說,I/O Multiplexing 就是同時阻塞了多個應(yīng)用進(jìn)程,而且可以同時對多個 Socket 進(jìn)行檢測,直到有數(shù)據(jù)可讀或可寫時,才真正開始 I/O 操作。
比較上圖和 Blocking I/O,你會發(fā)現(xiàn) I/O Multiplexing 的 I/O 操作和 Blocking I/O 似乎差不多,事實上,IO 多路復(fù)用還更差一些,因為這里需要使用兩個系統(tǒng)調(diào)用 (select? 和 recvfrom?),而 Blocking IO 只需要一個系統(tǒng)調(diào)用 (recvfrom)。
但是,IO 多路復(fù)用的優(yōu)勢并不是對單個連接能處理得更快,而是只需要一個進(jìn)程就可以同時處理多個 I/O,能同時處理更多的連接。
Signal Blocking I/O
Signal Blocking I/O 就是當(dāng)用戶進(jìn)程發(fā)起 I/O 操作的時候,首先通過系統(tǒng)調(diào)用 sigaction? 向內(nèi)核注冊一個信號處理函數(shù),這個系統(tǒng)調(diào)用會立即返回不會阻塞用戶進(jìn)程;當(dāng)內(nèi)核數(shù)據(jù)準(zhǔn)備好了就會發(fā)送一個 SIGIO 信號給用戶進(jìn)程,這樣用戶進(jìn)程就知道內(nèi)核數(shù)據(jù)準(zhǔn)備好了,可以開始執(zhí)行 I/O 系統(tǒng)調(diào)用了。
和 Non-Blocking I/O 一樣,信號驅(qū)動 IO 的用戶進(jìn)程在 I/O 的第一階段準(zhǔn)備數(shù)據(jù)是非阻塞的,在第二階段數(shù)據(jù)拷貝是阻塞的
不過信號驅(qū)動 IO 基于回調(diào)機(jī)制,其實現(xiàn)和開發(fā)應(yīng)用難度大,因此在實際中并不常用。
Asynchronous I/O
異步 I/O,先來解釋下什么是異步?
POSIX 的定義如下:
- 同步 I/O 操作(synchronous I/O operation):導(dǎo)致請求進(jìn)程阻塞,直到 I/O 操作完成
- 異步 I/O 操作(asynchronous I/O operation):不導(dǎo)致請求進(jìn)程阻塞
根據(jù)這個定義,我們可以做一個分類了,那就是上述四種 I/O 都是同步 I/O!因為它們無一例外都會在第二階段阻塞住用戶進(jìn)程直到 I/O 操作完成。
這就是為什么你會看見有人把 “阻塞 I/O” 稱之為 “同步 阻塞 I/O”,把 “非阻塞 I/O” 稱之為 “同步 非阻塞 I/O” 了
而異步 IO 所謂的在整個 I/O 操作期間都不會阻塞用戶進(jìn)程,其通常的工作機(jī)制是:
用戶進(jìn)程告知內(nèi)核啟動某個 I/O 操作,并讓內(nèi)核在整個操作(包括將數(shù)據(jù)從內(nèi)核復(fù)制到用戶緩沖區(qū))完成后通知用戶進(jìn)程。
這與 Signal Blocking I/O 的本質(zhì)區(qū)別就是:
- Signal Blocking I/O 是在數(shù)據(jù)準(zhǔn)備好了之后進(jìn)行通知,告知應(yīng)用進(jìn)程可以啟動 I/O 操作進(jìn)行拷貝數(shù)據(jù)了
- Asynchronous I/O 是在整個 I/O 操作完成了之后進(jìn)行通知,告知應(yīng)用進(jìn)程 I/O 操作已經(jīng)完成了
下圖給出了一個異步調(diào)用的例子:
用戶進(jìn)程進(jìn)行異步系統(tǒng)調(diào)用 aio_read? 之后,無論內(nèi)核數(shù)據(jù)是否準(zhǔn)備好,都會直接返回給用戶進(jìn)程,然后用戶進(jìn)程可以去做別的事情。等到數(shù)據(jù)準(zhǔn)備好了,內(nèi)核直接拷貝數(shù)據(jù)給用戶進(jìn)程(不需要用戶進(jìn)程再主動發(fā)起 recvfrom 系統(tǒng)調(diào)用),拷貝完畢后內(nèi)核才會給用戶進(jìn)程發(fā)送通知,告訴用戶進(jìn)程操作已經(jīng)完成了。
所以,異步 IO 的兩個階段,用戶進(jìn)程都是非阻塞的,用戶進(jìn)程將整個 IO 操作都交由內(nèi)核完成,內(nèi)核完成后會發(fā)送通知。在此期間,用戶進(jìn)程不需要去檢查 IO 操作的狀態(tài),也不需要主動的去拷貝數(shù)據(jù)。
五種 I/O 模型比較
本文理清了五種 I/O 模型,并區(qū)分了阻塞/非阻塞、同步和異步的概念
最后上張圖對比下,加深印象
?