對(duì)于傳統(tǒng)的操作系統(tǒng)來說,普通的 I/O 操作一般會(huì)被內(nèi)核緩存,這種 I/O 被稱作緩存 I/O。緩存 I/O 又被稱作標(biāo)準(zhǔn) I/O,大多數(shù)文件系統(tǒng)的默認(rèn) I/O 操作都是緩存 I/O。在 Linux 的緩存 I/O 機(jī)制中,操作系統(tǒng)會(huì)將 I/O 的數(shù)據(jù)緩存在文件系統(tǒng)的頁緩存( page cache )中,也就是說,數(shù)據(jù)會(huì)先被拷貝到操作系統(tǒng)內(nèi)核的緩沖區(qū)中,然后才會(huì)從操作系統(tǒng)內(nèi)核的緩沖區(qū)拷貝到應(yīng)用程序的地址空間。
(1) 對(duì)于讀操作來說,當(dāng)應(yīng)用程序嘗試讀取某塊數(shù)據(jù)的時(shí)候,如果這塊數(shù)據(jù)已經(jīng)存放在了頁緩存中,那么這塊數(shù)據(jù)就可以立即返回給應(yīng)用程序,而不需要經(jīng)過實(shí)際的物理讀盤操作。當(dāng)然,如果數(shù)據(jù)在應(yīng)用程序讀取之前并未被存放在頁緩存中,那么就需要先將數(shù)據(jù)從磁盤讀到頁緩存中去。一個(gè)典型的讀取磁盤中數(shù)據(jù)的流程圖如下所示:
(2) 對(duì)于寫操作來說,應(yīng)用程序也會(huì)將數(shù)據(jù)先寫到頁緩存中去,數(shù)據(jù)是否被立即寫到磁盤上去取決于應(yīng)用程序所采用的寫操作機(jī)制:如果用戶采用的是同步寫機(jī)制( synchronous writes ), 那么數(shù)據(jù)會(huì)立即被寫回到磁盤上,應(yīng)用程序會(huì)一直等到數(shù)據(jù)被寫完為止;如果用戶采用的是延遲寫機(jī)制( deferred writes ),那么應(yīng)用程序就完全不需要等到數(shù)據(jù)全部被寫回到磁盤,數(shù)據(jù)只要被寫到頁緩存中去就可以了。在延遲寫機(jī)制的情況下,操作系統(tǒng)會(huì)定期地將放在頁緩存中的數(shù)據(jù)刷到磁盤上。與異步寫機(jī)制( asynchronous writes )不同的是,延遲寫機(jī)制在數(shù)據(jù)完全寫到磁盤上的時(shí)候不會(huì)通知應(yīng)用程序,而異步寫機(jī)制在數(shù)據(jù)完全寫到磁盤上的時(shí)候是會(huì)返回給應(yīng)用程序的。所以延遲寫機(jī)制本身是存在數(shù)據(jù)丟失的風(fēng)險(xiǎn)的,而異步寫機(jī)制則不會(huì)有這方面的擔(dān)心。
緩存 I/O 優(yōu)點(diǎn):
緩存I/O缺點(diǎn):
網(wǎng)絡(luò)IO的本質(zhì)就是socket的讀取,socket在linux系統(tǒng)被抽象為流,IO可以理解為對(duì)流的操作。文章開始的時(shí)候也提到了,對(duì)于一次IO訪問(以read為例),數(shù)據(jù)會(huì)先被拷貝到操作系統(tǒng)內(nèi)核的緩沖區(qū),然后才會(huì)從操作系統(tǒng)內(nèi)核的緩沖區(qū)拷貝到應(yīng)用程序的地址空間中。
所以說,當(dāng)一個(gè)read操作發(fā)生時(shí),它會(huì)經(jīng)歷兩個(gè)階段:
第一個(gè)階段:等待數(shù)據(jù)準(zhǔn)備。
第二個(gè)階段:將數(shù)據(jù)從內(nèi)核拷貝到進(jìn)程中
對(duì)于socket流而言:
第一步:通常涉及等待網(wǎng)絡(luò)上的數(shù)據(jù)分組到達(dá),然后復(fù)制到內(nèi)核的某個(gè)緩沖區(qū)。
第二步:把數(shù)據(jù)從內(nèi)核緩沖區(qū)復(fù)制到應(yīng)用進(jìn)程緩沖區(qū)。
Linux環(huán)境下的五種IO Modle: blocking IO, nonblocking IO, IO multiplexing, signal driven IO, asynchronous IO. 其中前四種比較常見。
(1) blocking IO
在阻塞IO模型中,從調(diào)用系統(tǒng)函數(shù)獲取數(shù)據(jù)開始到得到數(shù)據(jù),當(dāng)前的進(jìn)程或者線程始終是處于阻塞狀態(tài)的,也就是什么都不干,直到等完數(shù)據(jù)準(zhǔn)備好和將數(shù)據(jù)搬遷到用戶空間為止:
1) 用戶首先發(fā)出系統(tǒng)調(diào)用函數(shù)希望獲取數(shù)據(jù);
2) 系統(tǒng)調(diào)用會(huì)進(jìn)入內(nèi)核檢查是否有數(shù)據(jù)準(zhǔn)備完畢,如果沒有就一直等待;
3) 當(dāng)數(shù)據(jù)準(zhǔn)備完畢的時(shí)候會(huì)將數(shù)據(jù)拷貝到用戶空間;
4) 拷貝完畢返回一個(gè)獲取數(shù)據(jù)成功的返回值來告訴用戶可以進(jìn)行數(shù)據(jù)的處理了;在此期間,用戶進(jìn)程或者線程一直是處于阻塞狀態(tài)的,無論是等待數(shù)據(jù)還是進(jìn)行數(shù)據(jù)的拷貝;
(2) nonblocking IO
和阻塞式的IO模型不同,當(dāng)發(fā)出了系統(tǒng)調(diào)用的時(shí)候,如果這時(shí)候數(shù)據(jù)還沒有準(zhǔn)備好,進(jìn)程或線程并不會(huì)進(jìn)入阻塞模式一直等待,而是會(huì)反復(fù)輪詢“數(shù)據(jù)好沒…數(shù)據(jù)好沒…數(shù)據(jù)好沒…”,這是比較耗CPU資源的,而當(dāng)數(shù)據(jù)準(zhǔn)備好之后會(huì)和阻塞IO模型一樣進(jìn)行數(shù)據(jù)的拷貝:
1) 首先用戶發(fā)出系統(tǒng)調(diào)用,去向內(nèi)核申請獲取想要的數(shù)據(jù);
2) 內(nèi)核檢查發(fā)現(xiàn)數(shù)據(jù)還沒準(zhǔn)備好,就會(huì)返回一個(gè)錯(cuò)誤值;
3) 用戶接收到錯(cuò)誤值并不甘心,就會(huì)反復(fù)反復(fù)詢問內(nèi)核是否有數(shù)據(jù)準(zhǔn)備好;
4) 內(nèi)核一直檢查直到有數(shù)據(jù)準(zhǔn)備完畢,進(jìn)行數(shù)據(jù)的搬遷,這時(shí)用戶會(huì)進(jìn)入阻塞等待狀態(tài)等待數(shù)據(jù)拷貝完畢;
5) 數(shù)據(jù)提取到用戶空間之后會(huì)返回一個(gè)成功狀態(tài)通知用戶數(shù)據(jù)完畢,這時(shí)用戶就可以進(jìn)行數(shù)據(jù)的處理了;
(3) IO multiplexing
既然是復(fù)用,說明一次性可以管理或處理多個(gè)IO,主要是靠select函數(shù)或者poll(epoll)函數(shù)來完成;這些函數(shù)同樣會(huì)阻塞進(jìn)程,但是和阻塞式IO不同的是IO復(fù)用模型可以一次性阻塞多個(gè)IO操作。同時(shí),kernel會(huì)“監(jiān)視”所有select負(fù)責(zé)的socket,當(dāng)任何一個(gè)socket中的數(shù)據(jù)準(zhǔn)備好了,select就會(huì)返回。這個(gè)時(shí)候用戶進(jìn)程再調(diào)用read操作,將數(shù)據(jù)從kernel拷貝到用戶進(jìn)程。
1) 用戶首先調(diào)用select或者poll函數(shù)對(duì)多個(gè)IO接口操作進(jìn)行檢測,同時(shí)會(huì)使進(jìn)程或者線程阻塞;
2) 當(dāng)有至少一個(gè)IO接口響應(yīng)的時(shí)候,系統(tǒng)就會(huì)通知內(nèi)核調(diào)用相應(yīng)的函數(shù)來獲取數(shù)據(jù);
3) 這時(shí)內(nèi)核將數(shù)據(jù)拷貝遷移至內(nèi)核空間,進(jìn)程或者線程仍然處于阻塞狀態(tài);
4) 數(shù)據(jù)就緒,返回一個(gè)成功值告訴用戶可以處理數(shù)據(jù)了;
(4) signal driven IO
用戶首先注冊一個(gè)處理IO信號(hào)的信號(hào)處理函數(shù),當(dāng)數(shù)據(jù)還沒準(zhǔn)備好的時(shí)候進(jìn)程或者線程并不阻塞,當(dāng)數(shù)據(jù)準(zhǔn)備好的時(shí)候用戶進(jìn)程或者線程會(huì)收到一個(gè)信號(hào)SIGIO,這時(shí)候就會(huì)調(diào)用信號(hào)處理函數(shù),在信號(hào)處理函數(shù)中調(diào)用IO函數(shù)操作數(shù)據(jù),完成之后通知用戶:
1) 用戶程序中事先注冊好一個(gè)對(duì)于SIGIO的信號(hào)處理函數(shù);
2) 當(dāng)數(shù)據(jù)準(zhǔn)備完畢的時(shí)候會(huì)向用戶進(jìn)程或者線程發(fā)送一個(gè)SIGIO信號(hào),這時(shí)信號(hào)就會(huì)被捕捉;
3) 捕捉信號(hào)之后就會(huì)執(zhí)行用戶自定義的一個(gè)信號(hào)處理函數(shù),并且在函數(shù)中調(diào)用系統(tǒng)函數(shù)去獲取數(shù)據(jù);
4) 獲取數(shù)據(jù)同樣會(huì)將數(shù)據(jù)進(jìn)行拷貝到用戶空間,這時(shí)進(jìn)程或者線程仍然會(huì)被阻塞;
5) 當(dāng)數(shù)據(jù)準(zhǔn)備完畢同樣會(huì)通知用戶,之后就可以進(jìn)行數(shù)據(jù)的處理了;
(5) asynchronous IO
對(duì)于異步的IO模型來說,數(shù)據(jù)的等待和搬遷都不由當(dāng)前的進(jìn)程或者線程來處理,調(diào)用相應(yīng)系統(tǒng)函數(shù)之后就會(huì)直接返回繼續(xù)執(zhí)行,因此當(dāng)前用戶程并不會(huì)被阻塞,當(dāng)數(shù)據(jù)已經(jīng)在用戶空間準(zhǔn)備就緒之后會(huì)以狀態(tài)、通知或者回調(diào)來告訴用戶可以進(jìn)行數(shù)據(jù)的處理了:
1) 用戶程序調(diào)用aio_read函數(shù),告訴內(nèi)核描述字、緩沖區(qū)指針、緩沖區(qū)大小、文件偏移以及通知的方式,之后便立即返回;
2) 這時(shí)內(nèi)核相應(yīng)的數(shù)據(jù)操作組件會(huì)進(jìn)行數(shù)據(jù)的等待和搬遷,期間用戶程序并不受影響繼續(xù)執(zhí)行;
3) 當(dāng)數(shù)據(jù)都已經(jīng)在用戶空間準(zhǔn)備就緒之后就會(huì)通過在函數(shù)中預(yù)留的通知方式來通知用戶程序處理數(shù)據(jù);
總結(jié):
從上面的分析中不難發(fā)現(xiàn),在對(duì)于數(shù)據(jù)的獲取過程中都是進(jìn)行了兩個(gè)主要的部分:數(shù)據(jù)的等待和數(shù)據(jù)的搬遷;除此相同點(diǎn)之外,下面就總結(jié)一下各種IO模型的區(qū)別:
從上面的比較可以發(fā)現(xiàn):前四種IO模型也就是阻塞IO、非阻塞IO、IO復(fù)用和信號(hào)驅(qū)動(dòng)IO模型都是同步的,只有最后一種是異步的異步IO模型;默認(rèn)情況下所創(chuàng)建出來的socket都是以阻塞的形式,比如網(wǎng)絡(luò)通信中的recvfrom和sendto,或者read和write等函數(shù)都是以阻塞的方式來實(shí)現(xiàn)的。
聯(lián)系客服