一区二区三区日韩精品-日韩经典一区二区三区-五月激情综合丁香婷婷-欧美精品中文字幕专区

分享

五種IO模型詳解

 啟云_9137 2020-10-20

架構(gòu)師-網(wǎng)絡(luò)文章匯總

1 基礎(chǔ)

在引入IO模型前,先對(duì)io等待時(shí)某一段數(shù)據(jù)的'經(jīng)歷'做一番解釋。如圖:

五種IO模型詳解

當(dāng)某個(gè)程序或已存在的進(jìn)程/線程(后文將不加區(qū)分的只認(rèn)為是進(jìn)程)需要某段數(shù)據(jù)時(shí),它只能在用戶空間中屬于它自己的內(nèi)存中訪問、修改,這段內(nèi)存暫且稱之為app buffer。假設(shè)需要的數(shù)據(jù)在磁盤上,那么進(jìn)程首先得發(fā)起相關(guān)系統(tǒng)調(diào)用,通知內(nèi)核去加載磁盤上的文件。但正常情況下,數(shù)據(jù)只能加載到內(nèi)核的緩沖區(qū),暫且稱之為kernel buffer。數(shù)據(jù)加載到kernel buffer之后,還需將數(shù)據(jù)復(fù)制到app buffer。到了這里,進(jìn)程就可以對(duì)數(shù)據(jù)進(jìn)行訪問、修改了。

現(xiàn)在有幾個(gè)需要說明的問題。

(1).為什么不能直接將數(shù)據(jù)加載到app buffer呢?

實(shí)際上是可以的,有些程序或者硬件為了提高效率和性能,可以實(shí)現(xiàn)內(nèi)核旁路的功能,避過內(nèi)核的參與,直接在存儲(chǔ)設(shè)備和app buffer之間進(jìn)行數(shù)據(jù)傳輸,例如RDMA技術(shù)就需要實(shí)現(xiàn)這樣的內(nèi)核旁路功能。

但是,最普通也是絕大多數(shù)的情況下,為了安全和穩(wěn)定性,數(shù)據(jù)必須先拷入內(nèi)核空間的kernel buffer,再復(fù)制到app buffer,以防止進(jìn)程串進(jìn)內(nèi)核空間進(jìn)行破壞。

(2).上面提到的數(shù)據(jù)幾次拷貝過程,拷貝方式是一樣的嗎?

不一樣。現(xiàn)在的存儲(chǔ)設(shè)備(包括網(wǎng)卡)基本上都支持DMA操作。什么是DMA(direct memory access,直接內(nèi)存訪問)?簡單地說,就是內(nèi)存和設(shè)備之間的數(shù)據(jù)交互可以直接傳輸,不再需要計(jì)算機(jī)的CPU參與,而是通過硬件上的芯片(可以簡單地認(rèn)為是一個(gè)小cpu)進(jìn)行控制。

假設(shè),存儲(chǔ)設(shè)備不支持DMA,那么數(shù)據(jù)在內(nèi)存和存儲(chǔ)設(shè)備之間的傳輸,必須由內(nèi)核線程占用CPU去完成數(shù)據(jù)拷貝(比如網(wǎng)卡不支持DMA時(shí),內(nèi)核負(fù)責(zé)將數(shù)據(jù)從網(wǎng)卡拷貝到kernel buffer)。而DMA就釋放了計(jì)算機(jī)的CPU,讓它可以去處理其他任務(wù),DMA也釋放了從用戶進(jìn)程切換到內(nèi)核的過程,從而避免了用戶進(jìn)程在這個(gè)拷貝階段被阻塞。

再說kernel buffer和app buffer之間的復(fù)制方式,這是兩段內(nèi)存空間的數(shù)據(jù)傳輸,只能由內(nèi)核占用CPU來完成拷貝。

所以,在加載硬盤數(shù)據(jù)到kernel buffer的過程是DMA拷貝方式,而從kernel buffer到app buffer的過程是CPU參與的拷貝方式。

(3).如果數(shù)據(jù)要通過TCP連接傳輸出去要怎么辦?

例如,web服務(wù)對(duì)客戶端的響應(yīng)數(shù)據(jù),需要通過TCP連接傳輸給客戶端。

TCP/IP協(xié)議棧維護(hù)著兩個(gè)緩沖區(qū):send buffer和recv buffer,它們合稱為socket buffer。需要通過TCP連接傳輸出去的數(shù)據(jù),需要先復(fù)制到send buffer,再復(fù)制給網(wǎng)卡通過網(wǎng)絡(luò)傳輸出去。如果通過TCP連接接收到數(shù)據(jù),數(shù)據(jù)首先通過網(wǎng)卡進(jìn)入recv buffer,再被復(fù)制到用戶空間的app buffer。

同樣,在數(shù)據(jù)復(fù)制到send buffer或從recv buffer復(fù)制到app buffer時(shí),是內(nèi)核占用CPU來完成的數(shù)據(jù)拷貝。從send buffer復(fù)制到網(wǎng)卡或從網(wǎng)卡復(fù)制到recv buffer時(shí),是DMA方式的拷貝,這個(gè)階段不需要切換到內(nèi)核,也不需要計(jì)算機(jī)自身的CPU。

如下圖所示,是通過TCP連接傳輸數(shù)據(jù)時(shí)的過程。

五種IO模型詳解

(4).網(wǎng)絡(luò)數(shù)據(jù)一定要從kernel buffer復(fù)制到app buffer再復(fù)制到send buffer嗎?

不是。如果進(jìn)程不需要修改數(shù)據(jù),就直接發(fā)送給TCP連接的另一端,可以不用從kernel buffer復(fù)制到app buffer,而是直接復(fù)制到send buffer。這就是零復(fù)制技術(shù)。

例如,如果httpd進(jìn)程不需要訪問和修改任何數(shù)據(jù),那么將數(shù)據(jù)原原本本地復(fù)制到app buffer再原原本本地復(fù)制到send buffer然后傳輸出去的過程中,從kernel buffer復(fù)制到app buffer的過程是可以省略的。使用零復(fù)制技術(shù),就可以減少一次拷貝過程,提升效率。

當(dāng)然,實(shí)現(xiàn)零復(fù)制技術(shù)的方法有多種,見我的另一篇結(jié)束零復(fù)制的文章:零復(fù)制(zero copy)技術(shù)。

以下是以httpd進(jìn)程處理文件類請(qǐng)求時(shí)比較完整的數(shù)據(jù)操作流程。

五種IO模型詳解

大致解釋下:客戶端發(fā)起對(duì)某個(gè)文件的請(qǐng)求,通過TCP連接,請(qǐng)求數(shù)據(jù)進(jìn)入TCP的recv buffer,再通過recv()函數(shù)將數(shù)據(jù)讀入到app buffer,此時(shí)httpd工作進(jìn)程對(duì)數(shù)據(jù)進(jìn)行一番解析,知道請(qǐng)求的是某個(gè)文件,于是發(fā)起read系統(tǒng)調(diào)用,于是內(nèi)核加載該文件,數(shù)據(jù)從磁盤復(fù)制到kernel buffer再復(fù)制到app buffer,此時(shí)httpd就要開始構(gòu)建響應(yīng)數(shù)據(jù)了,可能會(huì)對(duì)數(shù)據(jù)進(jìn)行一番修改,例如在響應(yīng)首部中加一個(gè)字段,最后將修改或未修改的數(shù)據(jù)復(fù)制(例如send()函數(shù))到send buffer中,再通過TCP連接傳輸給客戶端。

2 I/O模型

所謂的IO模型,描述的是出現(xiàn)I/O等待時(shí)進(jìn)程的狀態(tài)以及處理數(shù)據(jù)的方式。圍繞著進(jìn)程的狀態(tài)、數(shù)據(jù)準(zhǔn)備到kernel buffer再到app buffer的兩個(gè)階段展開。其中數(shù)據(jù)復(fù)制到kernel buffer的過程稱為數(shù)據(jù)準(zhǔn)備階段,數(shù)據(jù)從kernel buffer復(fù)制到app buffer的過程稱為數(shù)據(jù)復(fù)制階段。請(qǐng)記住這兩個(gè)概念,后面描述I/O模型時(shí)會(huì)一直用這兩個(gè)概念。

本文某些地方以httpd進(jìn)程的TCP連接方式處理本地文件為例,請(qǐng)無視httpd是否真的實(shí)現(xiàn)了如此、那般的功能,也請(qǐng)無視TCP連接處理數(shù)據(jù)的細(xì)節(jié),這里僅僅只是作為方便解釋的示例而已。

再次說明,從硬件設(shè)備到內(nèi)存的數(shù)據(jù)傳輸過程是不需要CPU參與的,而內(nèi)存間傳輸數(shù)據(jù)是需要內(nèi)核線程占用CPU來參與的。

2.1 Blocking I/O模型

如圖:

五種IO模型詳解

假設(shè)客戶端發(fā)起index.html的文件請(qǐng)求,httpd需要將index.html的數(shù)據(jù)從磁盤中加載到自己的httpd app buffer中,然后復(fù)制到send buffer中發(fā)送出去。

但是在httpd想要加載index.html時(shí),它首先檢查自己的app buffer中是否有index.html對(duì)應(yīng)的數(shù)據(jù),沒有就發(fā)起系統(tǒng)調(diào)用讓內(nèi)核去加載數(shù)據(jù),例如read(),內(nèi)核會(huì)先檢查自己的kernel buffer中是否有index.html對(duì)應(yīng)的數(shù)據(jù),如果沒有,則從磁盤中加載,然后將數(shù)據(jù)準(zhǔn)備到kernel buffer,再復(fù)制到app buffer中,最后被httpd進(jìn)程處理。

如果使用Blocking I/O模型:

(1).當(dāng)設(shè)置為blocking i/o模型,httpd從到都是被阻塞的。

(2).只有當(dāng)數(shù)據(jù)復(fù)制到app buffer完成后,或者發(fā)生了錯(cuò)誤,httpd才被喚醒處理它app buffer中的數(shù)據(jù)。

(3).cpu會(huì)經(jīng)過兩次上下文切換:用戶空間到內(nèi)核空間再到用戶空間,第一次是發(fā)起系統(tǒng)調(diào)用的切換,第二次是內(nèi)核將數(shù)據(jù)拷貝到app buffer完成后的切換。

(4).由于階段的拷貝是不需要CPU參與的,所以在階段準(zhǔn)備數(shù)據(jù)的過程中,cpu可以去處理其它進(jìn)程的任務(wù)。

(5).階段的數(shù)據(jù)復(fù)制需要CPU參與,將httpd阻塞。

(6).這是最省事、最簡單的IO模式。

如下圖:

五種IO模型詳解

2.2 Non-Blocking I/O模型

(1).當(dāng)設(shè)置為non-blocking時(shí),httpd第一次發(fā)起系統(tǒng)調(diào)用(如read())后,立即返回一個(gè)錯(cuò)誤值EWOULDBLOCK,而不是讓httpd進(jìn)入睡眠狀態(tài)。UNP中也正是這么描述的。

When we set a socket to be nonblocking, we are telling the kernel 'when an I/O operation that I request cannot be completed without putting the process to sleep, do not put the process to sleep, but return an error instead.

(2).雖然read()立即返回了,但httpd還要不斷地去發(fā)送read()檢查內(nèi)核:數(shù)據(jù)是否已經(jīng)成功拷貝到kernel buffer了?這稱為輪詢(polling)。每次輪詢時(shí),只要內(nèi)核沒有把數(shù)據(jù)準(zhǔn)備好,read()就返回錯(cuò)誤信息EWOULDBLOCK。

(3).直到kernel buffer中數(shù)據(jù)準(zhǔn)備完成,再去輪詢時(shí)不再返回EWOULDBLOCK,而是將httpd阻塞,以等待數(shù)據(jù)復(fù)制到app buffer。

(4).httpd在到階段不被阻塞,但是會(huì)不斷去發(fā)送read()輪詢。在被阻塞,將cpu交給內(nèi)核把數(shù)據(jù)copy到app buffer。

如下圖:

五種IO模型詳解

2.3 I/O Multiplexing模型

稱為多路IO模型或IO復(fù)用,意思是可以檢查多個(gè)IO等待的狀態(tài)。有三種IO復(fù)用模型:select、poll和epoll。其實(shí)它們都是一種函數(shù),用于監(jiān)控指定文件描述符的數(shù)據(jù)是否就緒。

就緒指的是對(duì)某個(gè)系統(tǒng)調(diào)用不再阻塞了,可以直接執(zhí)行IO。例如對(duì)于read()來說,數(shù)據(jù)準(zhǔn)備好了就是就緒狀態(tài),此時(shí)read()可以直接去讀取數(shù)據(jù)且能立即讀取到數(shù)據(jù),對(duì)write()來說,就是有空間可以寫入數(shù)據(jù)了(比如緩沖區(qū)未滿),此時(shí)write()可以直接寫入。

就緒種類包括是否可讀、是否可寫以及是否異常,其中可讀條件中就包括了數(shù)據(jù)是否準(zhǔn)備好,也即數(shù)據(jù)是否已經(jīng)在kernel buffer中。當(dāng)就緒之后,將通知進(jìn)程,進(jìn)程再發(fā)送對(duì)數(shù)據(jù)操作的系統(tǒng)調(diào)用,如read()。

所以,這三個(gè)函數(shù)僅僅只是處理了數(shù)據(jù)是否準(zhǔn)備好以及如何通知進(jìn)程的問題。可以將這幾個(gè)函數(shù)結(jié)合阻塞和非阻塞IO模式使用,但通常IO復(fù)用都會(huì)結(jié)合非阻塞IO模式。

select()和poll()差不多,它們的監(jiān)控和通知手段是類似的,只不過poll()要更聰明一點(diǎn),某些時(shí)候效率也更高些,此處僅以select()監(jiān)控單個(gè)文件請(qǐng)求為例簡單介紹IO復(fù)用,至于更具體的、監(jiān)控多個(gè)文件以及epoll的方式,在本文的最后專門解釋。

(1).當(dāng)想要加載某個(gè)文件時(shí),假如httpd要發(fā)起read()系統(tǒng)調(diào)用,如果是阻塞或者非阻塞情形,那么read()會(huì)根據(jù)數(shù)據(jù)是否準(zhǔn)備好而決定是否返回。是否可以主動(dòng)去監(jiān)控這個(gè)數(shù)據(jù)是否準(zhǔn)備到了kernel buffer中呢,亦或者是否可以監(jiān)控send buffer中是否有新數(shù)據(jù)進(jìn)入呢?這就是select()/poll()/epoll的作用。

(2).當(dāng)使用select()時(shí),進(jìn)程被select()所『阻塞』,之所以阻塞要加上引號(hào),是因?yàn)閟elect()有時(shí)間間隔選項(xiàng)可用控制阻塞時(shí)長,如果該選項(xiàng)設(shè)置為0,則select不阻塞而是立即返回,還可以設(shè)置為永久阻塞。

(3).當(dāng)select()的監(jiān)控對(duì)象就緒時(shí),httpd進(jìn)程通過輪詢判斷知道可以執(zhí)行read()了,于是httpd再發(fā)起read()系統(tǒng)調(diào)用,此時(shí)數(shù)據(jù)會(huì)從kernel buffer復(fù)制到app buffer中并read()成功。

(4).httpd發(fā)起read()系統(tǒng)調(diào)用后切換到內(nèi)核,由內(nèi)核占用CPU來復(fù)制數(shù)據(jù)到app buffer,所以httpd進(jìn)程被阻塞。

上面的描述可能還太過抽象,這里用shell偽代碼來簡單描述select()的工作方式(細(xì)節(jié)并非準(zhǔn)確,但易于理解)。假設(shè)有一個(gè)select命令,作用和select()函數(shù)相同。偽代碼如下:

# select監(jiān)控指定的文件描述符,并返回已就緒的描述符數(shù)量給x # 進(jìn)程將阻塞在select命令上,直到select返回 x=$(select fd1 fd2 fd3) # 如果x大于0,說明有文件描述符數(shù)據(jù)就緒,于是遍歷所有fd, # 并分別使用read去讀取這些fd,但并不知道具體是哪個(gè)fd已 # 就緒,所以read時(shí)最好是非阻塞的讀取,否則read每一個(gè)未 # 就緒的fd時(shí)都會(huì)阻塞 if [ x -gt 0 ];then for fd in fd1 fd2 fd3;do read -t 0 -u $fd # read操作最好是非阻塞的 done fi

所以,在使用IO復(fù)用模型時(shí),真正的IO操作(比如read)最好是非阻塞方式的,但并非必須。比如只監(jiān)控一個(gè)文件描述符時(shí),select能返回意味著這個(gè)文件描述符一定是就緒的(select還有其它返回值,但這里不考慮其它返回值。

IO多路復(fù)用時(shí),模型如圖:

五種IO模型詳解

select/poll的性能問題

select()/poll()的性能會(huì)隨著監(jiān)控的文件描述符數(shù)量增多而快速下降。其原因是多方面的。

其中一個(gè)原因是它們所監(jiān)控的文件描述符會(huì)以某種數(shù)據(jù)結(jié)構(gòu)全部傳送給內(nèi)核,讓內(nèi)核負(fù)責(zé)監(jiān)控這些文件描述符,當(dāng)內(nèi)核發(fā)現(xiàn)某個(gè)文件描述符已經(jīng)就緒時(shí),會(huì)修改數(shù)據(jù)結(jié)構(gòu),然后將修改后的數(shù)據(jù)結(jié)構(gòu)傳回給進(jìn)程。所以涉及了兩次數(shù)據(jù)傳輸?shù)倪^程。

對(duì)于select()來說,每次傳遞的數(shù)據(jù)結(jié)構(gòu)的大小是固定的,都是1024個(gè)描述符的大小。對(duì)于poll()來說,只會(huì)傳遞被監(jiān)控的文件描述符,所以文件描述符少的時(shí)候,poll()的性能是可以的,此外poll()可以超出1024個(gè)文件描述符的監(jiān)控?cái)?shù)量限制,但隨著描述符數(shù)量的增多,來回傳遞的數(shù)據(jù)量也是非常大的。

基于這方面的性能考慮,更建議使用信號(hào)驅(qū)動(dòng)IO或epoll模型,它們都是直接告訴內(nèi)核要監(jiān)控哪些文件描述符,內(nèi)核會(huì)以合適的數(shù)據(jù)結(jié)構(gòu)安排這些待監(jiān)控的文件描述符(如epoll,內(nèi)核采用紅黑樹的方式),換句話說,它們不會(huì)傳遞一大片的文件描述符數(shù)據(jù)結(jié)構(gòu),效率更高。

使用IO復(fù)用還有很多細(xì)節(jié),本文在此僅只是對(duì)其作最基本的功能性描述,在本文末還會(huì)多做一些擴(kuò)展,至于更多的內(nèi)容需自行了解。

2.4 Signal-driven I/O模型

即信號(hào)驅(qū)動(dòng)IO模型。當(dāng)文件描述符上設(shè)置了O_ASYNC標(biāo)記時(shí),就表示該文件描述符是信號(hào)驅(qū)動(dòng)的IO。

注:可能你覺得O_ASYNC應(yīng)該表示的是異步IO,但并非如此。

在歷史上,信號(hào)驅(qū)動(dòng)IO也稱為異步IO,比如這個(gè)標(biāo)記就暗含了這一點(diǎn)歷史。

如今常說的術(shù)語【異步】,是由POSIX AIO規(guī)范所提供的功能,這個(gè)異步表示某進(jìn)程發(fā)起IO操作時(shí),立即返回,當(dāng)IO完成或有錯(cuò)誤時(shí),該進(jìn)程會(huì)收到通知,于是該進(jìn)程可以去處理這個(gè)通知,比如執(zhí)行回調(diào)函數(shù)。

當(dāng)某個(gè)文件描述符使用信號(hào)驅(qū)動(dòng)IO模型時(shí),要求進(jìn)程配置信號(hào)SIGIO的信號(hào)處理程序,然后進(jìn)程就可以做其他任何事情。當(dāng)該文件描述符就緒時(shí),內(nèi)核會(huì)向該進(jìn)程發(fā)送SIGIO信號(hào)。該進(jìn)程收到SIGIO信號(hào)后,就會(huì)去執(zhí)行已經(jīng)配置號(hào)的信號(hào)處理程序。

通常來說,SIGIO的信號(hào)處理程序中會(huì)編寫read()類的讀取代碼,這表示在收到SIGIO時(shí)在信號(hào)處理程序中執(zhí)行read操作,另一種常見的作法是在SIGIO的信號(hào)處理程序中設(shè)置某變量標(biāo)記,然后在外部判斷該標(biāo)記是否為true,如果標(biāo)記為true,則執(zhí)行read類的操作。

使用Shell偽代碼如下:

 # 第一種模式:在信號(hào)處理程序中執(zhí)行IO操作 trap 'read...' SIGIO  # 設(shè)置SIGIO的信號(hào)處理程序 # 然后就可以執(zhí)行其它任意任務(wù) # 當(dāng)在執(zhí)行其它任務(wù)過程中,內(nèi)核發(fā)送了SIGIO,進(jìn)程會(huì) # 立即去執(zhí)行SIGIO信號(hào)處理程序 ...other codes...  # 第二種模式:在信號(hào)處理程序中設(shè)置變量標(biāo)記,在外部執(zhí)行IO操作 trap 'a=1' SIGIO ... other codes... #  while [ $a -eq 1 ];do   read... done

很明顯,使用信號(hào)驅(qū)動(dòng)IO模型時(shí),進(jìn)程對(duì)該描述符的讀取是被動(dòng)的,進(jìn)程不會(huì)主動(dòng)在描述符就緒前執(zhí)行讀取操作。

其實(shí)信號(hào)驅(qū)動(dòng)IO模型就像是小姐姐在閑逛,小姐姐本沒有想過要買什么東西,但如果發(fā)現(xiàn)有合適的,也會(huì)去買下來,在逛了一段時(shí)間后,一件超短裙閃現(xiàn)到小姐姐的視線,小姐姐很喜歡這個(gè)款式,于是立即決定買下來。

這里可以做出大膽的推測,并非所有文件描述符類型都能使用信號(hào)驅(qū)動(dòng)的IO模型。如果某文件描述符想要開啟信號(hào)驅(qū)動(dòng)IO,要求有某個(gè)另一端會(huì)主動(dòng)向該描述符發(fā)送數(shù)據(jù),比如管道、套接字、終端等都符合這種要求。顯然,普通文件系統(tǒng)上的文件IO是無法使用信號(hào)驅(qū)動(dòng)IO的。

回到信號(hào)驅(qū)動(dòng)IO模型,由于進(jìn)程沒有主動(dòng)執(zhí)行IO操作,所以不會(huì)阻塞,當(dāng)數(shù)據(jù)就緒后,進(jìn)程收到內(nèi)核發(fā)送的SIGIO信號(hào),進(jìn)程會(huì)去執(zhí)行SIGIO的信號(hào)處理程序,當(dāng)進(jìn)程執(zhí)行read()時(shí),由于數(shù)據(jù)已經(jīng)就緒,所以可以直接將數(shù)據(jù)從kernel buffer復(fù)制到app buffer,read()過程中,進(jìn)程將被阻塞。

注意:sigio信號(hào)只是通知了數(shù)據(jù)就緒,但并不知道有多少數(shù)據(jù)已經(jīng)就緒。

如圖:

五種IO模型詳解

2.5 Asynchronous I/O模型

即異步IO模型。

異步IO來自于POSIX AIO規(guī)范,它專門提供了能異步IO的讀寫類函數(shù),如aio_read(),aio_write()等。

使用異步IO函數(shù)時(shí),要求指定IO完成時(shí)或IO出現(xiàn)錯(cuò)誤時(shí)的通知方式,通知方式主要分兩類:

發(fā)送指定的信號(hào)來通知

在另一個(gè)線程中執(zhí)行指定的回調(diào)函數(shù)

為了幫助理解,這里假設(shè)aio_read()的語法如下(真實(shí)的語法要復(fù)雜的多):

aio_read(x,y,z,notify_mode,notify_value)

其中nofity_mode允許的值有兩種:

當(dāng)notify_mode參數(shù)的值為SIGEV_SIGNAL時(shí),notify_value參數(shù)的值為一個(gè)信號(hào)

當(dāng)notify_mode參數(shù)的值為SIGEV_THREAD,notify_value參數(shù)的值為一個(gè)函數(shù),這個(gè)函數(shù)稱為回調(diào)函數(shù)

當(dāng)使用異步IO函數(shù)時(shí),進(jìn)程不會(huì)因?yàn)橐獔?zhí)行IO操作而阻塞,而是立即返回。

例如,當(dāng)進(jìn)程執(zhí)行異步IO函數(shù)aio_read()時(shí),它會(huì)請(qǐng)求內(nèi)核執(zhí)行具體的IO操作,當(dāng)數(shù)據(jù)已經(jīng)就緒且從kernel buffer拷貝到app buffer后,內(nèi)核認(rèn)為IO操作已經(jīng)完成,于是內(nèi)核會(huì)根據(jù)調(diào)用異步IO函數(shù)時(shí)指定的通知方式來執(zhí)行對(duì)應(yīng)的操作:

如果通知模式是信號(hào)通知方式(SIGEV_SIGNAL),則在IO完成時(shí),內(nèi)核會(huì)向進(jìn)程發(fā)送notify_value指定的信號(hào)

如果通知模式是信號(hào)回調(diào)方式(SIGEV_THREAD),則在IO完成時(shí),內(nèi)核會(huì)在一個(gè)獨(dú)立的線程中執(zhí)行notify_value指定的回調(diào)函數(shù)

回顧一下信號(hào)驅(qū)動(dòng)IO,信號(hào)驅(qū)動(dòng)IO要求有另一端主動(dòng)向文件描述符寫入數(shù)據(jù),所以它支持像socket、pipe、terminal這類文件描述符,但不支持普通文件IO的文件描述符。

而異步IO則沒有這個(gè)限制,異步IO操作借助的是那些具有神力的異步函數(shù),只要文件描述符能讀寫,就能使用異步IO函數(shù)來實(shí)現(xiàn)異步IO。

所以,異步IO在整個(gè)過程中都不會(huì)被阻塞。如圖:

五種IO模型詳解

看上去異步很好,但是注意,在復(fù)制kernel buffer數(shù)據(jù)到app buffer中時(shí)是需要CPU參與的,這意味著不受阻的進(jìn)程會(huì)和異步調(diào)用函數(shù)爭用CPU。以httpd為例,如果并發(fā)量比較大,httpd接入的連接數(shù)可能就越多,CPU爭用情況就越嚴(yán)重,異步函數(shù)返回成功信號(hào)的速度就越慢。如果不能很好地處理這個(gè)問題,異步IO模型也不一定就好。

2.6 同步IO和異步IO、阻塞和非阻塞的區(qū)分

阻塞和非阻塞,體現(xiàn)在當(dāng)前進(jìn)程是否可執(zhí)行,是否能獲取到CPU。

當(dāng)阻塞和非阻塞的概念體現(xiàn)在IO模型上:

阻塞IO:從開始發(fā)起IO操作開始就阻塞,直到IO完成才返回,所以進(jìn)程會(huì)立即進(jìn)入睡眠態(tài)

非阻塞IO:發(fā)起IO操作時(shí),如果當(dāng)前數(shù)據(jù)已就緒,則切換到內(nèi)核態(tài)由內(nèi)核完成數(shù)據(jù)拷貝(從kernel buffer拷貝到app buffer),此時(shí)進(jìn)程被阻塞,因?yàn)樗腃PU已經(jīng)被內(nèi)核搶走了。如果發(fā)起IO操作時(shí)數(shù)據(jù)未就緒,則立即返回而不阻塞,即進(jìn)程繼續(xù)享有CPU,可以繼續(xù)任務(wù)。但進(jìn)程不知道數(shù)據(jù)何時(shí)就緒,所以通常會(huì)采用輪循代碼(比如while循環(huán))不斷判斷數(shù)據(jù)是否就緒,當(dāng)數(shù)據(jù)最終就緒后,切換到內(nèi)核態(tài),進(jìn)程仍然被阻塞

同步和異步,考慮的是兩邊數(shù)據(jù)是否同步(比如kernel buffer和app buffer之間數(shù)據(jù)是否同步)。同步和異步的區(qū)別體現(xiàn)在兩邊數(shù)據(jù)尚未完成同步時(shí)的行為:

同步:在保持兩邊數(shù)據(jù)同步的過程中,進(jìn)程被阻塞,由內(nèi)核搶占其CPU去完成數(shù)據(jù)同步,直到兩邊數(shù)據(jù)同步,進(jìn)程才被喚醒

異步:在保持兩邊數(shù)據(jù)同步的過程中,由內(nèi)核默默地在后臺(tái)完成數(shù)據(jù)同步(如果不理解,可認(rèn)為是單獨(dú)開了一個(gè)內(nèi)核線程負(fù)責(zé)數(shù)據(jù)同步),內(nèi)核不會(huì)搶占進(jìn)程的CPU,所以進(jìn)程自身不被阻塞,當(dāng)內(nèi)核完成兩端數(shù)據(jù)同步時(shí),通知進(jìn)程已同步完成

這里阻塞和非阻塞、同步和異步都是廣義的概念,上面所做的解釋適用于所有使用這些術(shù)語的情況,而不僅僅是本文所專注的IO模型。

回到阻塞、非阻塞、同步、異步的IO模型,再對(duì)它們啰嗦啰嗦。

阻塞、非阻塞、IO復(fù)用、信號(hào)驅(qū)動(dòng)都是同步IO模型。需注意,雖然不同IO模型在加載數(shù)據(jù)到kernel buffer的數(shù)據(jù)準(zhǔn)備過程中可能阻塞、可能不阻塞,但kernel buffer才是read()函數(shù)讀取數(shù)據(jù)時(shí)的對(duì)象,同步的意思是讓kernel buffer和app buffer數(shù)據(jù)同步。在保持kernel buffer和app buffer同步的過程中,CPU將從執(zhí)行read()操作的進(jìn)程切換到內(nèi)核態(tài),內(nèi)核獲取CPU拷貝數(shù)據(jù)到app buffer,所以執(zhí)行read()操作的進(jìn)程在這個(gè)同步的階段中是被阻塞的。

只有異步IO模型才是異步的,因?yàn)樗{(diào)用的是具有【神力】的異步IO函數(shù)(如aio_read()),調(diào)用這些函數(shù)時(shí)會(huì)請(qǐng)求內(nèi)核,當(dāng)數(shù)據(jù)已經(jīng)拷貝到app buffer后,通知進(jìn)程并執(zhí)行指定的操作。

需要注意的是,無論是哪種IO模型,在將數(shù)據(jù)從kernel buffer拷貝到app buffer的這個(gè)階段,都是需要CPU參與的。只不過,同步IO模型和異步IO模型中,CPU參與的方式不一樣:

同步IO模型中,調(diào)用read()的進(jìn)程會(huì)切換到內(nèi)核,由內(nèi)核占用CPU來執(zhí)行數(shù)據(jù)拷貝,所以原進(jìn)程在此階段一直被阻塞

異步IO模型中,由內(nèi)核在后臺(tái)默默的執(zhí)行數(shù)據(jù)拷貝,所以原進(jìn)程在此階段不被阻塞

如圖:

五種IO模型詳解

2.7 信號(hào)驅(qū)動(dòng)IO和異步IO的區(qū)別

很多人都不理解信號(hào)驅(qū)動(dòng)IO和異步IO之間的區(qū)別,一方面是因?yàn)樗鼈兌剂⒓捶祷兀硪环矫媸且驗(yàn)樗鼈兛此贫际潜粍?dòng)的或后臺(tái)的。

但其實(shí)在前文已經(jīng)分析清楚了它們的區(qū)別,這里僅做總結(jié)性分析。在此之前,還是借用前文使用過的類比。

信號(hào)驅(qū)動(dòng)IO模型:小姐姐在逛街,小姐姐本沒有想過要買什么東西,但如果發(fā)現(xiàn)有合適的,也會(huì)去買下來,在逛了一段時(shí)間后,一件超短裙閃現(xiàn)到小姐姐的視線,小姐姐很喜歡這個(gè)款式,于是立即決定買下來,買的時(shí)候小姐姐不能再干其它事情。

異步IO模型:小姐姐在逛街,她這次帶上了男朋友,只要想買東西,都可以讓男朋友去幫忙買,而小姐姐可以繼續(xù)自己逛自己的,男朋友買好后通知小姐姐即可。

1.異步IO

異步IO通過調(diào)用具有異步IO能力的函數(shù)來實(shí)現(xiàn)。在調(diào)用異步函數(shù)時(shí),要求指定IO完成時(shí)的通知方式。

當(dāng)IO完成后,內(nèi)核(這里的內(nèi)核是廣義的,不再局限于操作系統(tǒng)內(nèi)核,它也可以是瀏覽器內(nèi)核,或語言的解釋器,或語言的虛擬機(jī))要么通知進(jìn)程,要么執(zhí)行回調(diào)函數(shù)。

這里所謂的IO完成,表示的是已經(jīng)保持了兩邊數(shù)據(jù)的同步(比如kernel buffer和app buffer之間)。而異步之所以稱為異步,就體現(xiàn)在完成兩邊數(shù)據(jù)同步的階段中,它表示由內(nèi)核在后臺(tái)默默完成數(shù)據(jù)的同步任務(wù)。

對(duì)于異步IO來說,它不在乎什么類型的文件描述符,socket、pipe、fifo、terminal以及普通文件都可以執(zhí)行異步IO。

2.信號(hào)驅(qū)動(dòng)IO

信號(hào)驅(qū)動(dòng)IO是同步IO模型。

當(dāng)某個(gè)文件描述符設(shè)置了O_ASYNC標(biāo)記時(shí)(前文說過,稱呼為O_ASYNC是歷史原因),表示該文件描述符開啟信號(hào)驅(qū)動(dòng)IO的功能。

使用信號(hào)驅(qū)動(dòng)IO,要求進(jìn)程注冊(cè)SIGIO的信號(hào)處理程序,注冊(cè)之后,進(jìn)程就可以做其他任務(wù)。

當(dāng)有另一端向該描述符寫入數(shù)據(jù)時(shí),就意味著該文件描述符已經(jīng)就緒,內(nèi)核會(huì)發(fā)送SIGIO信號(hào)給進(jìn)程,于是進(jìn)程會(huì)去執(zhí)行已經(jīng)注冊(cè)的SIGIO信號(hào)處理程序。一般來說,信號(hào)處理程序中,要么是read()類的讀取函數(shù),要么是為后面是否讀取做判斷的變量標(biāo)記。

但是,內(nèi)核發(fā)送SIGIO信號(hào)只是通知進(jìn)程數(shù)據(jù)已經(jīng)就緒,但就緒了多少數(shù)據(jù)量,進(jìn)程并不知道。

而且,進(jìn)程因?yàn)槭盏酵ㄖJ(rèn)為可以數(shù)據(jù)已就緒,于是執(zhí)行read(),進(jìn)程在執(zhí)行read()的時(shí)候,CPU將從用戶態(tài)切換到內(nèi)核態(tài),由內(nèi)核獲取CPU來執(zhí)行數(shù)據(jù)同步操作,所以在這個(gè)階段中,進(jìn)程的read()是被阻塞的。

因?yàn)樾盘?hào)驅(qū)動(dòng)要求有另一端主動(dòng)寫入數(shù)據(jù),所以socket、pipe、fifo、terminal等文件描述符類型是可以信號(hào)驅(qū)動(dòng)IO 的,但是不支持對(duì)普通文件使用信號(hào)驅(qū)動(dòng)IO。

3.select()、poll()和epoll

前面說了,這三個(gè)函數(shù)是文件描述符狀態(tài)監(jiān)控的函數(shù),它們可以監(jiān)控一系列文件的一系列事件,當(dāng)出現(xiàn)滿足條件的事件后,就認(rèn)為是就緒或者錯(cuò)誤。事件大致分為3類:可讀事件、可寫事件和異常事件。它們通常都放在循環(huán)結(jié)構(gòu)中進(jìn)行循環(huán)監(jiān)控。

select()和poll()函數(shù)處理方式的本質(zhì)類似,只不過poll()稍微先進(jìn)一點(diǎn),而epoll處理方式就比這兩個(gè)函數(shù)先進(jìn)多了。當(dāng)然,就算是先進(jìn)分子,在某些情況下性能也不一定就比老家伙們強(qiáng)。

select() & poll()

首先,通過FD_SET宏函數(shù)創(chuàng)建待監(jiān)控的描述符集合,并將此描述符集合作為select()函數(shù)的參數(shù),可以在指定select()函數(shù)阻塞時(shí)間間隔,于是select()就創(chuàng)建了一個(gè)監(jiān)控對(duì)象。

除了普通文件描述符,還可以監(jiān)控套接字,因?yàn)樘捉幼忠彩俏募?,所以select()也可以監(jiān)控套接字文件描述符,例如recv buffer中是否收到了數(shù)據(jù),也即監(jiān)控套接字的可讀性,send buffer中是否滿了,也即監(jiān)控套接字的可寫性。select()默認(rèn)最大可監(jiān)控1024個(gè)文件描述符。而poll()則沒有此限制。

select()的時(shí)間間隔參數(shù)分3種:

(1).設(shè)置為指定時(shí)間間隔內(nèi)阻塞,除非之前有就緒事件發(fā)生。

(2).設(shè)置為永久阻塞,除非有就緒事件發(fā)生。

(3).設(shè)置為完全不阻塞,即立即返回。但因?yàn)閟elect()通常在循環(huán)結(jié)構(gòu)中,所以這是輪詢監(jiān)控的方式。

當(dāng)創(chuàng)建了監(jiān)控對(duì)象后,由內(nèi)核監(jiān)控這些描述符集合,于此同時(shí)調(diào)用select()的進(jìn)程被阻塞(或輪詢)。當(dāng)監(jiān)控到滿足就緒條件時(shí)(監(jiān)控事件發(fā)生),select()將被喚醒(或暫停輪詢),于是select()返回滿足就緒條件的描述符數(shù)量,之所以是數(shù)量而不僅僅是一個(gè),是因?yàn)槎鄠€(gè)文件描述符可能在同一時(shí)間滿足就緒條件。由于只是返回?cái)?shù)量,并沒有返回哪一個(gè)或哪幾個(gè)文件描述符,所以通常在使用select()之后,還會(huì)在循環(huán)結(jié)構(gòu)中的if語句中使用宏函數(shù)FD_ISSET進(jìn)行遍歷,直到找出所有的滿足就緒條件的描述符。最后將描述符集合通過指定函數(shù)拷貝回用戶空間,以便被進(jìn)程處理。

監(jiān)聽描述符集合的大致過程如下圖所示,其中select()只是其中的一個(gè)環(huán)節(jié):

五種IO模型詳解

大概描述下這個(gè)循環(huán)監(jiān)控的過程:

(1).首先通過FD_ZERO宏函數(shù)初始化描述符集合。圖中每個(gè)小方格表示一個(gè)文件描述符。

(2).通過FD_SET宏函數(shù)創(chuàng)建描述符集合,此時(shí)集合中的文件描述符都被打開,也就是稍后要被select()監(jiān)控的對(duì)象。

(3).使用select()函數(shù)監(jiān)控描述符集合。當(dāng)某個(gè)文件描述符滿足就緒條件時(shí),select()函數(shù)返回集合中滿足條件的數(shù)量。圖中標(biāo)黃色的小方塊表示滿足就緒條件的描述符。

(4).通過FD_ISSET宏函數(shù)遍歷整個(gè)描述符集合,并將滿足就緒條件的描述符發(fā)送給進(jìn)程。同時(shí),使用FD_CLR宏函數(shù)將滿足就緒條件的描述符從集合中移除。

(5).進(jìn)入下一個(gè)循環(huán),繼續(xù)使用FD_SET宏函數(shù)向描述符集合中添加新的待監(jiān)控描述符。然后重復(fù)(3)、(4)兩個(gè)步驟。

如果使用簡單的偽代碼來描述:

FD_ZEROfor() {    FD_SET()    select()    if(){        FD_ISSET()        FD_CLR()    }    writen()}

以上所說只是一種需要循環(huán)監(jiān)控的示例,具體如何做卻是不一定的。不過從中也能看出這一系列的流程。

epoll

epoll比poll()、select()先進(jìn),考慮以下幾點(diǎn),自然能看出它的優(yōu)勢(shì)所在:

(1).epoll_create()創(chuàng)建的epoll實(shí)例可以隨時(shí)通過epoll_ctl()來新增和刪除感興趣的文件描述符,不用再和select()每個(gè)循環(huán)后都要使用FD_SET更新描述符集合的數(shù)據(jù)結(jié)構(gòu)。

(2).在epoll_create()創(chuàng)建epoll實(shí)例時(shí),還創(chuàng)建了一個(gè)epoll就緒鏈表list。而epoll_ctl()每次向epoll實(shí)例添加描述符時(shí),還會(huì)注冊(cè)該描述符的回調(diào)函數(shù)。當(dāng)epoll實(shí)例中的描述符滿足就緒條件時(shí)將觸發(fā)回調(diào)函數(shù),被移入到就緒鏈表list中。

(3).當(dāng)調(diào)用epoll_wait()進(jìn)行監(jiān)控時(shí),它只需確定就緒鏈表中是否有數(shù)據(jù)即可,如果有,將復(fù)制到用戶空間以被進(jìn)程處理,如果沒有,它將被阻塞。當(dāng)然,如果監(jiān)控的對(duì)象設(shè)置為非阻塞模式,它將不會(huì)被阻塞,而是不斷地去檢查。

也就是說,epoll的處理方式中,根本就無需遍歷描述符集合。

備注:

這篇文章摘抄來自網(wǎng)絡(luò)。我打算總結(jié)一些列架構(gòu)師需要的優(yōu)秀文章,由于自己寫會(huì)花太多時(shí)間,我決定做一個(gè)搬運(yùn)工,為大家篩選優(yōu)秀的文章,最后我會(huì)做成索引方便大家查找。

    本站是提供個(gè)人知識(shí)管理的網(wǎng)絡(luò)存儲(chǔ)空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點(diǎn)。請(qǐng)注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購買等信息,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請(qǐng)點(diǎn)擊一鍵舉報(bào)。
    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評(píng)論

    發(fā)表

    請(qǐng)遵守用戶 評(píng)論公約

    類似文章 更多

    日韩精品区欧美在线一区| 91日韩在线观看你懂的| 99久久精品免费看国产高清| 国产毛片不卡视频在线| 婷婷色国产精品视频一区| 亚洲精品一区二区三区日韩| 国产午夜精品在线免费看| 欧美一区二区三区喷汁尤物| 国产精品免费精品一区二区| 久久精品国产亚洲av久按摩| 99久久人妻精品免费一区| 欧美精品二区中文乱码字幕高清| 日韩欧美一区二区久久婷婷| 日韩人妻精品免费一区二区三区| 日韩午夜老司机免费视频| 国产91色综合久久高清| 中文字幕不卡欧美在线| 亚洲国产成人av毛片国产| 欧美国产在线观看精品| 少妇丰满a一区二区三区| 亚洲欧美日韩熟女第一页| 久久综合九色综合欧美| 台湾综合熟女一区二区| 亚洲一区二区三区三州| 日韩人妻精品免费一区二区三区 | 国产成人精品在线播放| 中文字幕精品一区二区三| 一区二区三区免费公开| 日本人妻免费一区二区三区| 午夜成年人黄片免费观看| 欧美一级片日韩一级片| 日本黄色美女日本黄色| 办公室丝袜高跟秘书国产| 国产视频一区二区三区四区| 免费黄片视频美女一区| 中文字幕中文字幕在线十八区| 国产麻豆一线二线三线| 亚洲国产精品av在线观看| 亚洲一区精品二人人爽久久| 日本女优一区二区三区免费| 国产精品不卡高清在线观看|