在學(xué)習(xí)NIO之前,我們非常有必要了解一下操作系統(tǒng)中的各種IO模型,否則是不會(huì)理解NIO的實(shí)現(xiàn)的.
這篇文章是我翻譯I/O Multiplexing: The select and poll Functions這篇文章中的前半部分關(guān)于IO模型的部分.這篇文章中,還對(duì)select()等系統(tǒng)調(diào)用有更加深入的介紹,各位不妨讀一下.
正文
在Unix下,我們有五種不同的IO模型,分別是:
- 阻塞IO(Blocking IO)
- 非阻塞IO(Nonblocking IO)
- I/O復(fù)用(I/O Multiplexing)
- 信號(hào)驅(qū)動(dòng)IO(signal driven I/O)
- 異步IO(Asynchronous IO)
對(duì)于一個(gè)讀操作來說,一般會(huì)經(jīng)過下面兩個(gè)過程:
- 等待數(shù)據(jù)就緒.比如說,對(duì)于一個(gè)網(wǎng)絡(luò)連接來說,就是等待數(shù)據(jù)通過連接到達(dá)主機(jī).當(dāng)數(shù)據(jù)到達(dá)主機(jī)時(shí),把數(shù)據(jù)拷貝到內(nèi)核中的緩沖區(qū).
- 將數(shù)據(jù)從內(nèi)核拷貝到進(jìn)程.即把數(shù)據(jù)從內(nèi)核的緩沖區(qū)拷貝到應(yīng)用程序的緩沖區(qū).
阻塞IO
最常用的IO模型就是阻塞IO.默認(rèn)情況下,全部的socket都是阻塞的.其處理過程如下圖所示:

在這個(gè)例子中,我們會(huì)通過UDP而不是TCP來舉例,因?yàn)閷?duì)于UDP來說,等待數(shù)據(jù)就緒這一步更加直觀:要不就是收到了一個(gè)數(shù)據(jù)報(bào),要不就是沒收到一個(gè)數(shù)據(jù)報(bào).但是對(duì)于TCP來說,還有很多額外的變量.
上圖中的recvfrom是一個(gè)系統(tǒng)調(diào)用.當(dāng)我們執(zhí)行一次系統(tǒng)調(diào)用的時(shí)候,有一次從用戶態(tài)到內(nèi)核態(tài)的切換.
從上圖中我們可以看到,進(jìn)程調(diào)用recvfrom之后,這個(gè)系統(tǒng)調(diào)用并不會(huì)立即返回,它會(huì)等到數(shù)據(jù)報(bào)到達(dá)并且被拷貝到應(yīng)用程序的緩沖區(qū)中,或者出現(xiàn)了一個(gè)錯(cuò)誤,才會(huì)返回.我們稱這個(gè)過程是阻塞的,應(yīng)用程序只有在數(shù)據(jù)報(bào)被放入緩沖區(qū)之后,才能繼續(xù)進(jìn)行.
非阻塞IO
非阻塞IO和阻塞IO相對(duì),它會(huì)告訴內(nèi)核,"當(dāng)我要你完成的IO操作不能完成時(shí),不要讓進(jìn)程阻塞,你給我返回一個(gè)錯(cuò)誤就行了".過程如下圖所示:

- 在上面的三個(gè)recvfrom操作中,由于數(shù)據(jù)并沒有就緒,所以內(nèi)核返回了一個(gè)EWOULDBLOCK錯(cuò)誤.
- 在第四個(gè)recvfrom中,數(shù)據(jù)已經(jīng)就緒了,并且已經(jīng)被拷貝到我們的應(yīng)用程序的緩沖區(qū)了,內(nèi)核返回一個(gè)OK,然后我們的應(yīng)用程序處理這些數(shù)據(jù).
我們可以看到,在這種模型中,我們需要使用輪詢的方式來確定數(shù)據(jù)到底是否就緒.盡管這會(huì)浪費(fèi)CPU時(shí)間,但是仍然是比較常見的模型,一般是在系統(tǒng)函數(shù)中用到.
I/O多路復(fù)用
在I/O多路復(fù)用中,我們會(huì)調(diào)用select()或者poll(),并且阻塞在這兩個(gè)系統(tǒng)調(diào)用上.而不是阻塞在recvfrom這個(gè)實(shí)際的IO操作的系統(tǒng)調(diào)用上.下面是I/O多路復(fù)用模型的過程圖:

從上圖中,我們可以看到,我們會(huì)阻塞在select()這個(gè)系統(tǒng)調(diào)用上,并等待數(shù)據(jù)到達(dá).當(dāng)select()告訴我們數(shù)據(jù)到達(dá)時(shí),再通過recvfrom系統(tǒng)調(diào)用將數(shù)據(jù)拷貝到應(yīng)用程序的緩沖區(qū).
看到這里,如果各位不了解select(),可能就會(huì)有一個(gè)疑問.你這不是脫了褲子放屁嗎?這不是還是跟阻塞IO模型一樣,還是阻塞嗎?只不過現(xiàn)在不是阻塞在recvfrom上,而是阻塞在select上而已.而且,現(xiàn)在還多了一次系統(tǒng)調(diào)用,那效率不是更低嗎?
多了一次系統(tǒng)調(diào)用,確實(shí)是I/O多路復(fù)用模型的缺點(diǎn).但是存在即合理,它也有優(yōu)點(diǎn).
它的優(yōu)點(diǎn)在于,select可以同時(shí)監(jiān)聽多個(gè)文件描述符,以及感興趣的事件.所以,我們可以在一個(gè)線程中完成之前需要好多個(gè)線程才能完成的事情.
比如,我們想要同時(shí)從一個(gè)接受來自Socket的數(shù)據(jù),以及從文件中讀數(shù)據(jù).在阻塞IO模型中,我們會(huì)這么做:
1.創(chuàng)建一個(gè)線程A,在其中創(chuàng)建一個(gè)Socket Server,并通過它的accept()方法,等待客戶端的連接并處理數(shù)據(jù)
2.創(chuàng)建一個(gè)線程B,在其中打開文件并且讀數(shù)據(jù).
這就需要兩個(gè)線程,對(duì)吧?
而且我們又知道,線程之間的切換是有開銷的,也是需要涉及到用戶態(tài)到內(nèi)核態(tài)的轉(zhuǎn)換.
而我們?cè)贗/O多路復(fù)用模型中,可以這樣做:
1.通過注冊(cè)函數(shù)告訴系統(tǒng),應(yīng)用程序?qū)τ赟ocket的讀事件以及文件的讀事件感興趣
2.通過輪詢調(diào)用select()方法,查看哪些我們感興趣的事件已經(jīng)發(fā)生了
3.在同一個(gè)線程中,依次進(jìn)行對(duì)應(yīng)的操作
我們可以看到,在這里我們只需要用一個(gè)線程就可以做到在阻塞IO中我們需要兩個(gè)線程才能做到的事情.這就是I/O復(fù)用中的復(fù)用的含義.
信號(hào)驅(qū)動(dòng)IO
信號(hào)驅(qū)動(dòng)IO使用信號(hào)量機(jī)制,它告訴內(nèi)核,當(dāng)文件描述符準(zhǔn)備就緒時(shí),通過SIGIO信號(hào)通知我們.過程如下:

- 我們首先通過sigaction系統(tǒng)調(diào)用安裝一個(gè)事件處理器.這個(gè)操作會(huì)立即返回.所以我們的應(yīng)用程序會(huì)繼續(xù)運(yùn)行,而不會(huì)阻塞.
- 當(dāng)數(shù)據(jù)準(zhǔn)備就緒時(shí),內(nèi)核會(huì)給我們的應(yīng)用程序發(fā)出一個(gè)SIGIO信號(hào),我們可以繼續(xù)進(jìn)行下面的處理:
- 在信號(hào)處理器中,通過recvfrom系統(tǒng)調(diào)用將數(shù)據(jù)從內(nèi)核緩沖區(qū)讀取到應(yīng)用程序緩沖區(qū)中
- 告訴應(yīng)用程序從緩沖區(qū)讀取數(shù)據(jù)并且處理
這種模型的優(yōu)點(diǎn)是,在等待數(shù)據(jù)就緒時(shí),應(yīng)用程序并不會(huì)被阻塞.應(yīng)用程序可以繼續(xù)運(yùn)行,只需要在數(shù)據(jù)就緒時(shí),讓時(shí)間處理器通知它即可.
異步IO
異步IO模型跟事件驅(qū)動(dòng)IO模型類似,也是告訴內(nèi)核,在一定情況下通知我們.但是它跟事件驅(qū)動(dòng)IO模型不同的是,在事件驅(qū)動(dòng)IO模型中,內(nèi)核會(huì)在數(shù)據(jù)就緒,即數(shù)據(jù)被拷貝到內(nèi)核緩沖區(qū)時(shí),通知我們.而在異步IO中,內(nèi)核會(huì)在整個(gè)操作都被完成,即數(shù)據(jù)從內(nèi)核緩沖區(qū)拷貝到應(yīng)用程序緩沖區(qū)時(shí),通知我們.如下圖所示:

- 我們調(diào)用aio_read這個(gè)系統(tǒng)調(diào)用,并且給內(nèi)核傳遞下面的數(shù)據(jù):
- 文件描述符,緩沖區(qū)指針,緩沖區(qū)大小
- 文件偏移量
- 當(dāng)整個(gè)操作完成時(shí),如何通知我們
這個(gè)系統(tǒng)調(diào)用會(huì)立即返回,在整個(gè)操作完成之前,不會(huì)被阻塞
五種IO模型的比較

同步IO和異步IO
POSIX中,定義了下面的兩個(gè)術(shù)語:
- 同步IO:在整個(gè)IO操作完成之前,會(huì)導(dǎo)致應(yīng)用程序阻塞的IO操作叫做同步IO
- 異步IO:在整個(gè)IO操作完成之前,不會(huì)導(dǎo)致應(yīng)用程序阻塞的IO操作叫做異步IO
我們可以看到,上面我們定義的五個(gè)IO模型,前四個(gè)(阻塞IO模型,非阻塞IO模型,IO復(fù)用模型,信號(hào)驅(qū)動(dòng)模型)都屬于同步IO,而只有異步IO模型屬于異步IO.
注意
原文中還有更多關(guān)于select等IO復(fù)用的詳細(xì)介紹,所以強(qiáng)烈建議各位讀一下原文.