Unix下五種I/O模型
- 阻塞 I/O
- 非阻塞 I/O
- I/O 多路復(fù)用(select和poll)
- 信號驅(qū)動 I/O(SIGIO)
- 異步 I/O(Posix.1的aio_系列函數(shù))
阻塞I/O
如上文所述恰力,阻塞I/O下請求無法立即完成則保持阻塞。阻塞I/O分為如下兩個階段。
階段1:等待數(shù)據(jù)就緒。網(wǎng)絡(luò) I/O 的情況就是等待遠端數(shù)據(jù)陸續(xù)抵達;磁盤I/O的情況就是等待磁盤數(shù)據(jù)從磁盤上讀取到內(nèi)核態(tài)內(nèi)存中瓤漏。
階段2:數(shù)據(jù)拷貝。出于系統(tǒng)安全,用戶態(tài)的程序沒有權(quán)限直接讀取內(nèi)核態(tài)內(nèi)存擒抛,因此內(nèi)核負責(zé)把內(nèi)核態(tài)內(nèi)存中的數(shù)據(jù)拷貝一份到用戶態(tài)內(nèi)存中。
非阻塞I/O
非阻塞I/O請求包含如下三個階段
socket設(shè)置為 NONBLOCK(非阻塞)就是告訴內(nèi)核补疑,當所請求的I/O操作無法完成時歧沪,不要將線程睡眠,而是返回一個錯誤碼(EWOULDBLOCK) 莲组,這樣請求就不會阻塞诊胞。
I/O操作函數(shù)將不斷的測試數(shù)據(jù)是否已經(jīng)準備好,如果沒有準備好锹杈,繼續(xù)測試撵孤,直到數(shù)據(jù)準備好為止迈着。整個I/O 請求的過程中,雖然用戶線程每次發(fā)起I/O請求后可以立即返回邪码,但是為了等到數(shù)據(jù)裕菠,仍需要不斷地輪詢、重復(fù)請求霞扬,消耗了大量的 CPU 的資源糕韧。
數(shù)據(jù)準備好了,從內(nèi)核拷貝到用戶空間喻圃。
一般很少直接使用這種模型萤彩,而是在其他I/O模型中使用非阻塞I/O 這一特性。這種方式對單個I/O 請求意義不大斧拍,但給I/O多路復(fù)用提供了條件雀扶。
I/O多路復(fù)用(異步阻塞I/O)
I/O多路復(fù)用會用到select或者poll函數(shù),這兩個函數(shù)也會使線程阻塞肆汹,但是和阻塞I/O所不同的是愚墓,這兩個函數(shù)可以同時阻塞多個I/O操作。而且可以同時對多個讀操作昂勉,多個寫操作的I/O函數(shù)進行檢測浪册,直到有數(shù)據(jù)可讀或可寫時,才真正調(diào)用I/O操作函數(shù)岗照。
從流程上來看村象,使用select函數(shù)進行I/O請求和同步阻塞模型沒有太大的區(qū)別,甚至還多了添加監(jiān)視Channel攒至,以及調(diào)用select函數(shù)的額外操作厚者,增加了額外工作。但是库菲,使用 select以后最大的優(yōu)勢是用戶可以在一個線程內(nèi)同時處理多個Channel的I/O請求。用戶可以注冊多個Channel志膀,然后不斷地調(diào)用select讀取被激活的Channel熙宇,即可達到在同一個線程內(nèi)同時處理多個I/O請求的目的。而在同步阻塞模型中梧却,必須通過多線程的方式才能達到這個目的奇颠。
調(diào)用select/poll該方法由一個用戶態(tài)線程負責(zé)輪詢多個Channel,直到某個階段1的數(shù)據(jù)就緒放航,再通知實際的用戶線程執(zhí)行階段2的拷貝烈拒。 通過一個專職的用戶態(tài)線程執(zhí)行非阻塞I/O輪詢,模擬實現(xiàn)了階段一的異步化。
信號驅(qū)動I-O(SIGIO)
首先我們允許socket進行信號驅(qū)動I/O荆几,并安裝一個信號處理函數(shù)吓妆,線程繼續(xù)運行并不阻塞。當數(shù)據(jù)準備好時吨铸,線程會收到一個SIGIO 信號行拢,可以在信號處理函數(shù)中調(diào)用I/O操作函數(shù)處理數(shù)據(jù)。
異步I/O
調(diào)用aio_read 函數(shù)诞吱,告訴內(nèi)核描述字舟奠,緩沖區(qū)指針,緩沖區(qū)大小房维,文件偏移以及通知的方式沼瘫,然后立即返回。當內(nèi)核將數(shù)據(jù)拷貝到緩沖區(qū)后咙俩,再通知應(yīng)用程序耿戚。所以異步I/O模式下,階段1和階段2全部由內(nèi)核完成阿趁,完成不需要用戶線程的參與膜蛔。
幾種I/O模型對比
除異步I/O外,其它四種模型的階段2基本相同脖阵,都是從內(nèi)核態(tài)拷貝數(shù)據(jù)到用戶態(tài)皂股。區(qū)別在于階段1不同。前四種都屬于同步I/O命黔。
Java中四種I/O模型
上一章所述Unix中的五種I/O模型屑墨,除信號驅(qū)動I/O外,Java對其它四種I/O模型都有所支持纷铣。其中Java最早提供的blocking I/O即是阻塞I/O,而NIO即是非阻塞I/O战转,同時通過NIO實現(xiàn)的Reactor模式即是I/O復(fù)用模型的實現(xiàn)搜立,通過AIO實現(xiàn)的Proactor模式即是異步I/O模型的實現(xiàn)。
從IO到NIO
面向流 vs. 面向緩沖
Java IO是面向流的槐秧,每次從流(InputStream/OutputStream)中讀一個或多個字節(jié)啄踊,直到讀取完所有字節(jié),它們沒有被緩存在任何地方刁标。另外,它不能前后移動流中的數(shù)據(jù),如需前后移動處理憎瘸,需要先將其緩存至一個緩沖區(qū)器赞。
Java NIO面向緩沖,數(shù)據(jù)會被讀取到一個緩沖區(qū),需要時可以在緩沖區(qū)中前后移動處理硼控,這增加了處理過程的靈活性刘陶。但與此同時在處理緩沖區(qū)前需要檢查該緩沖區(qū)中是否包含有所需要處理的數(shù)據(jù),并需要確保更多數(shù)據(jù)讀入緩沖區(qū)時牢撼,不會覆蓋緩沖區(qū)內(nèi)尚未處理的數(shù)據(jù)匙隔。
阻塞 vs. 非阻塞
Java IO的各種流是阻塞的。當某個線程調(diào)用read()或write()方法時熏版,該線程被阻塞纷责,直到有數(shù)據(jù)被讀取到或者數(shù)據(jù)完全寫入。阻塞期間該線程無法處理任何其它事情撼短。
Java NIO為非阻塞模式再膳。讀寫請求并不會阻塞當前線程,在數(shù)據(jù)可讀/寫前當前線程可以繼續(xù)做其它事情阔加,所以一個單獨的線程可以管理多個輸入和輸出通道饵史。
選擇器(Selector))選擇器(Selector)
Java NIO的選擇器允許一個單獨的線程同時監(jiān)視多個通道,可以注冊多個通道到同一個選擇器上胜榔,然后使用一個單獨的線程來“選擇”已經(jīng)就緒的通道胳喷。這種“選擇”機制為一個單獨線程管理多個通道提供了可能。
零拷貝
Java NIO中提供的FileChannel擁有transferTo和transferFrom兩個方法夭织,可直接把FileChannel中的數(shù)據(jù)拷貝到另外一個Channel吭露,或者直接把另外一個Channel中的數(shù)據(jù)拷貝到FileChannel。該接口常被用于高效的網(wǎng)絡(luò)/文件的數(shù)據(jù)傳輸和大文件拷貝尊惰。在操作系統(tǒng)支持的情況下讲竿,通過該方法傳輸數(shù)據(jù)并不需要將源數(shù)據(jù)從內(nèi)核態(tài)拷貝到用戶態(tài),再從用戶態(tài)拷貝到目標通道的內(nèi)核態(tài)弄屡,同時也避免了兩次用戶態(tài)和內(nèi)核態(tài)間的上下文切換题禀,也即使用了“零拷貝”,所以其性能一般高于Java IO中提供的方法膀捷。