linux系統(tǒng)IO模型簡析

相關(guān)概念:

用戶空間與內(nèi)核空間

現(xiàn)在操作系統(tǒng)都是采用虛擬存儲器,操作系統(tǒng)的核心是內(nèi)核,獨(dú)立于普通的應(yīng)用程序,可以訪問受保護(hù)的內(nèi)存空間,也有訪問底層硬件設(shè)備的所有權(quán)限。為了保證用戶進(jìn)程不能直接操作內(nèi)核(kernel),保證內(nèi)核的安全,操作系統(tǒng)將虛擬空間劃分為兩部分,一部分為內(nèi)核空間,一部分為用戶空間。針對linux操作系統(tǒng)而言,將最高的1G字節(jié),供內(nèi)核使用,稱為內(nèi)核空間;而將較低的3G字節(jié),供各個(gè)進(jìn)程使用,稱為用戶空間。

進(jìn)程切換

為了控制進(jìn)程的執(zhí)行,內(nèi)核必須有能力掛起正在CPU上運(yùn)行的進(jìn)程,并恢復(fù)以前掛起的某個(gè)進(jìn)程的執(zhí)行。
這種行為被稱為進(jìn)程切換。因此可以說,任何進(jìn)程都是在操作系統(tǒng)內(nèi)核的支持下運(yùn)行的,是與內(nèi)核緊密相關(guān)的。

從一個(gè)進(jìn)程的運(yùn)行轉(zhuǎn)到另一個(gè)進(jìn)程上運(yùn)行,這個(gè)過程中經(jīng)過下面這些變化:

1、保存處理機(jī)上下文,包括程序計(jì)數(shù)器和其他寄存器。
2、更新PCB信息。
3、把進(jìn)程的PCB移入相應(yīng)的隊(duì)列,如就緒、在某事件阻塞等隊(duì)列。
4、選擇另一個(gè)進(jìn)程執(zhí)行,并更新其PCB。
5、更新內(nèi)存管理的數(shù)據(jù)結(jié)構(gòu)。
6、恢復(fù)處理機(jī)上下文。

進(jìn)程的阻塞

正在執(zhí)行的進(jìn)程,由于期待的某些事件未發(fā)生,如請求系統(tǒng)資源失敗、等待某種操作的完成、新數(shù)據(jù)尚未到達(dá)或無新工作做等,則由系統(tǒng)自動執(zhí)行阻塞原語(Block),使自己由運(yùn)行狀態(tài)變?yōu)樽枞麪顟B(tài)??梢姡M(jìn)程的阻塞是進(jìn)程自身的一種主動行為,也因此只有處于運(yùn)行態(tài)的進(jìn)程(獲得CPU),才可能將其轉(zhuǎn)為阻塞狀態(tài)。當(dāng)進(jìn)程進(jìn)入阻塞狀態(tài),是不占用CPU資源的。

文件描述符fd

文件描述符(File descriptor)是計(jì)算機(jī)科學(xué)中的一個(gè)術(shù)語,是一個(gè)用于表述指向文件的引用的抽象化概念。

文件描述符在形式上是一個(gè)非負(fù)整數(shù)。實(shí)際上,它是一個(gè)索引值,指向內(nèi)核為每一個(gè)進(jìn)程所維護(hù)的該進(jìn)程打開文件的記錄表。當(dāng)程序打開一個(gè)現(xiàn)有文件或者創(chuàng)建一個(gè)新文件時(shí),內(nèi)核向進(jìn)程返回一個(gè)文件描述符。在程序設(shè)計(jì)中,一些涉及底層的程序編寫往往會圍繞著文件描述符展開。但是文件描述符這一概念往往只適用于UNIX、Linux這樣的操作系統(tǒng)。

緩存 IO 和 直接IO

緩存IO:數(shù)據(jù)從磁盤先通過DMA copy到內(nèi)核空間,再從內(nèi)核空間通過cpu copy到用戶空間。
直接IO:數(shù)據(jù)從磁盤通過DMA copy到用戶空間。

緩存IO

緩存 IO 又被稱作標(biāo)準(zhǔn) IO,大多數(shù)文件系統(tǒng)的默認(rèn) IO 操作都是緩存 IO。在 Linux 的緩存 IO 機(jī)制中,操作系統(tǒng)會將 IO 的數(shù)據(jù)緩存在文件系統(tǒng)的頁緩存( page cache )中,也就是說,數(shù)據(jù)會先從磁盤被拷貝到操作系統(tǒng)內(nèi)核的緩沖區(qū)中,然后才會從操作系統(tǒng)內(nèi)核的緩沖區(qū)拷貝到應(yīng)用程序的地址空間。

讀操作:

操作系統(tǒng)檢查內(nèi)核的緩沖區(qū)有沒有需要的數(shù)據(jù),如果已經(jīng)緩存了,那么就直接從緩存中返回;否則從磁盤中讀
取,然后緩存在操作系統(tǒng)的緩存中。

寫操作:

將數(shù)據(jù)從用戶空間復(fù)制到內(nèi)核空間的緩存中。這時(shí)對用戶程序來說寫操作就已經(jīng)完成,至于什么時(shí)候再寫到磁
盤中由操作系統(tǒng)決定,除非顯示地調(diào)用了sync同步命令。

緩存I/O的優(yōu)點(diǎn):

在一定程度上分離了內(nèi)核空間和用戶空間,保護(hù)系統(tǒng)本身的運(yùn)行安全;
可以減少讀盤的次數(shù),從而提高性能。

緩存I/O的缺點(diǎn):

在緩存 I/O 機(jī)制中,DMA 方式可以將數(shù)據(jù)直接從磁盤讀到頁緩存中,或者將數(shù)據(jù)從頁緩存直接寫回到磁盤上,而不能直接在應(yīng)用程序地址空間和磁盤之間進(jìn)行數(shù)據(jù)傳輸,這樣,數(shù)據(jù)在傳輸過程中需要在應(yīng)用程序地址空間(用戶空間)和緩存(內(nèi)核空間)之間進(jìn)行多次數(shù)據(jù)拷貝操作,這些數(shù)據(jù)拷貝操作所帶來的CPU以及內(nèi)存開銷是非常大的。

直接IO

直接IO就是應(yīng)用程序直接訪問磁盤數(shù)據(jù),而不經(jīng)過內(nèi)核緩沖區(qū),也就是繞過內(nèi)核緩沖區(qū),自己管理I/O緩存區(qū),這樣做的目的是減少一次從內(nèi)核緩沖區(qū)到用戶程序緩存的數(shù)據(jù)復(fù)制。

引入內(nèi)核緩沖區(qū)的目的在于提高磁盤文件的訪問性能,因?yàn)楫?dāng)進(jìn)程需要讀取磁盤文件時(shí),如果文件內(nèi)容已經(jīng)在內(nèi)核緩沖區(qū)中,那么就不需要再次訪問磁盤;而當(dāng)進(jìn)程需要向文件中寫入數(shù)據(jù)時(shí),實(shí)際上只是寫到了內(nèi)核緩沖區(qū)便告訴進(jìn)程已經(jīng)寫成功,而真正寫入磁盤是通過一定的策略進(jìn)行延遲的。

然而,對于一些較復(fù)雜的應(yīng)用,比如數(shù)據(jù)庫服務(wù)器,它們?yōu)榱顺浞痔岣咝阅埽M@過內(nèi)核緩沖區(qū),由自己在用戶態(tài)空間實(shí)現(xiàn)并管理I/O緩沖區(qū),包括緩存機(jī)制和寫延遲機(jī)制等,以支持獨(dú)特的查詢機(jī)制,比如數(shù)據(jù)庫可以根據(jù)更加合理的策略來提高查詢緩存命中率。另一方面,繞過內(nèi)核緩沖區(qū)也可以減少系統(tǒng)內(nèi)存的開銷,因?yàn)閮?nèi)核緩沖區(qū)本身就在使用系統(tǒng)內(nèi)存。

應(yīng)用程序直接訪問磁盤數(shù)據(jù),不經(jīng)過操作系統(tǒng)內(nèi)核數(shù)據(jù)緩沖區(qū),這樣做的目的是減少一次從內(nèi)核緩沖區(qū)到用戶程序緩存的數(shù)據(jù)復(fù)制。這種方式通常是在對數(shù)據(jù)的緩存管理由應(yīng)用程序?qū)崿F(xiàn)的數(shù)據(jù)庫管理系統(tǒng)中。

直接I/O的缺點(diǎn):

如果訪問的數(shù)據(jù)不在應(yīng)用程序緩存中,那么每次數(shù)據(jù)都會直接從磁盤進(jìn)行加載,這種直接加載會非常緩慢。通常直接I/O跟異步I/O結(jié)合使用會得到較好的性能。

813155-20200627200040986-198855887.png

訪問步驟:

image.png

Linux提供了對這種需求的支持,即在open()系統(tǒng)調(diào)用中增加參數(shù)選項(xiàng)O_DIRECT,用它打開的文件便可以繞過內(nèi)核緩沖區(qū)的直接訪問,這樣便有效避免了CPU和內(nèi)存的多余時(shí)間開銷。

順便提一下,與O_DIRECT類似的一個(gè)選項(xiàng)是O_SYNC,后者只對寫數(shù)據(jù)有效,它將寫入內(nèi)核緩沖區(qū)的數(shù)據(jù)立即寫入磁盤,將機(jī)器故障時(shí)數(shù)據(jù)的丟失減少到最小,但是它仍然要經(jīng)過內(nèi)核緩沖區(qū)。

Linux IO模型

網(wǎng)絡(luò)IO的本質(zhì)是socket的讀取,socket在linux系統(tǒng)被抽象為流,IO可以理解為對流的操作。剛才說了,對于一次IO訪問(以read舉例),數(shù)據(jù)會先被拷貝到操作系統(tǒng)內(nèi)核的緩沖區(qū)中,然后才會從操作系統(tǒng)內(nèi)核的緩沖區(qū)拷貝到應(yīng)用程序的地址空間。所以說,當(dāng)一個(gè)read操作發(fā)生時(shí),它會經(jīng)歷兩個(gè)階段:

第一階段:等待數(shù)據(jù)準(zhǔn)備 (Waiting for the data to be ready)。
第二階段:將數(shù)據(jù)從內(nèi)核拷貝到進(jìn)程中 (Copying the data from the kernel to the process)。

對于socket流而言,

第一步:通常涉及等待網(wǎng)絡(luò)上的數(shù)據(jù)分組到達(dá),然后被復(fù)制到內(nèi)核的某個(gè)緩沖區(qū)。
第二步:把數(shù)據(jù)從內(nèi)核緩沖區(qū)復(fù)制到應(yīng)用進(jìn)程緩沖區(qū)。

網(wǎng)絡(luò)應(yīng)用需要處理的無非就是兩大類問題,網(wǎng)絡(luò)IO,數(shù)據(jù)計(jì)算。相對于后者,網(wǎng)絡(luò)IO的延遲,給應(yīng)用帶來的性能瓶頸大于后者。

網(wǎng)絡(luò)IO的模型大致有如下幾種:

同步模型(synchronous IO)
阻塞IO(bloking IO)
非阻塞IO(non-blocking IO)
多路復(fù)用IO(multiplexing IO)
信號驅(qū)動式IO(signal-driven IO):不常用
異步IO(asynchronous IO)

常見的IO模型有阻塞、非阻塞、IO多路復(fù)用,異步。
以一個(gè)生動形象的例子來說明這四個(gè)概念。周末我和女友去逛街,中午餓了,我們準(zhǔn)備去吃飯。周末人多,吃飯需要排隊(duì),我和女友有以下幾種方案。

同步阻塞 IO(blocking IO)

場景描述

我和女友點(diǎn)完餐后,不知道什么時(shí)候能做好,只好坐在餐廳里面等,直到做好,然后吃完才離開。女友本想還和我一起逛街的,但是不知道飯能什么時(shí)候做好,只好和我一起在餐廳等,而不能去逛街,直到吃完飯才能去逛街,中間等待做飯的時(shí)間浪費(fèi)掉了。--------------------這就是典型的阻塞。

網(wǎng)絡(luò)模型

同步阻塞 IO 模型是最常用的一個(gè)模型,也是最簡單的模型。在linux中,默認(rèn)情況下所有的socket都是blocking。它符合人們最常見的思考邏輯。阻塞就是進(jìn)程 "被" 休息, CPU處理其它進(jìn)程去了。

在這個(gè)IO模型中,用戶空間的應(yīng)用程序執(zhí)行一個(gè)系統(tǒng)調(diào)用(recvform),這會導(dǎo)致應(yīng)用程序阻塞,什么也不干,直到數(shù)據(jù)準(zhǔn)備好,并且將數(shù)據(jù)從內(nèi)核復(fù)制到用戶進(jìn)程,最后進(jìn)程再處理數(shù)據(jù),在等待數(shù)據(jù)到處理數(shù)據(jù)的兩個(gè)階段,整個(gè)進(jìn)程都被阻塞。不能處理別的網(wǎng)絡(luò)IO。調(diào)用應(yīng)用程序處于一種不再消費(fèi) CPU 而只是簡單等待響應(yīng)的狀態(tài),因此從處理的角度來看,這是非常有效的。在調(diào)用recv()/recvfrom()函數(shù)時(shí),發(fā)生在內(nèi)核中等待數(shù)據(jù)和復(fù)制數(shù)據(jù)的過程,大致如下圖:


image.png

流程描述

當(dāng)用戶進(jìn)程調(diào)用了recv()/recvfrom()這個(gè)系統(tǒng)調(diào)用,kernel就開始了IO的第一個(gè)階段:準(zhǔn)備數(shù)據(jù)(對于網(wǎng)絡(luò)IO來說,很多時(shí)候數(shù)據(jù)在一開始還沒有到達(dá)。比如,還沒有收到一個(gè)完整的UDP包。這個(gè)時(shí)候kernel就要等待足夠的數(shù)據(jù)到來)。這個(gè)過程需要等待,也就是說數(shù)據(jù)被拷貝到操作系統(tǒng)內(nèi)核的緩沖區(qū)中是需要一個(gè)過程的。而在用戶進(jìn)程這邊,整個(gè)進(jìn)程會被阻塞(當(dāng)然,是進(jìn)程自己選擇的阻塞)。第二個(gè)階段:當(dāng)kernel一直等到數(shù)據(jù)準(zhǔn)備好了,它就會將數(shù)據(jù)從kernel中拷貝到用戶內(nèi)存,然后kernel返回結(jié)果,用戶進(jìn)程才解除block的狀態(tài),重新運(yùn)行起來。

所以,blocking IO的特點(diǎn)就是在IO執(zhí)行的兩個(gè)階段都被block了。

優(yōu)點(diǎn):
能夠及時(shí)返回?cái)?shù)據(jù),無延遲;
對內(nèi)核開發(fā)者來說這是省事了;
缺點(diǎn):
對用戶來說處于等待就要付出性能的代價(jià)了;

同步非阻塞 IO(nonblocking IO)

場景描述

我女友不甘心白白在這等,又想去逛商場,又擔(dān)心飯好了。所以我們逛一會,回來詢問服務(wù)員飯好了沒有,來來回回好多次,飯都還沒吃都快累死了啦。

這就是非阻塞。需要不斷的詢問,是否準(zhǔn)備好了。

網(wǎng)絡(luò)模型

同步非阻塞就是 “每隔一會兒瞄一眼進(jìn)度條” 的輪詢(polling)方式。在這種模型中,設(shè)備是以非阻塞的形式打開的。這意味著 IO 操作不會立即完成,read 操作可能會返回一個(gè)錯(cuò)誤代碼,說明這個(gè)命令不能立即滿足(EAGAIN 或 EWOULDBLOCK)。

在網(wǎng)絡(luò)IO時(shí)候,非阻塞IO也會進(jìn)行recvform系統(tǒng)調(diào)用,檢查數(shù)據(jù)是否準(zhǔn)備好,與阻塞IO不一樣,"非阻塞將大的整片時(shí)間的阻塞分成N多的小的阻塞, 所以進(jìn)程不斷地有機(jī)會 '被' CPU光顧"。

也就是說非阻塞的recvform系統(tǒng)調(diào)用之后,進(jìn)程并沒有被阻塞,內(nèi)核馬上返回給進(jìn)程,如果數(shù)據(jù)還沒準(zhǔn)備好,此時(shí)會返回一個(gè)error。進(jìn)程在返回之后,可以干點(diǎn)別的事情,然后再發(fā)起recvform系統(tǒng)調(diào)用。重復(fù)上面的過程,循環(huán)往復(fù)的進(jìn)行recvform系統(tǒng)調(diào)用。這個(gè)過程通常被稱之為輪詢。輪詢檢查內(nèi)核數(shù)據(jù),直到數(shù)據(jù)準(zhǔn)備好,再拷貝數(shù)據(jù)到進(jìn)程,進(jìn)行數(shù)據(jù)處理。需要注意,拷貝數(shù)據(jù)整個(gè)過程,進(jìn)程仍然是屬于阻塞的狀態(tài)。

在linux下,可以通過設(shè)置socket使其變?yōu)閚on-blocking。當(dāng)對一個(gè)non-blocking socket執(zhí)行讀操作時(shí),流程如圖所示:


image.png

流程描述

當(dāng)用戶進(jìn)程發(fā)出read操作時(shí),如果kernel中的數(shù)據(jù)還沒有準(zhǔn)備好,那么它并不會block用戶進(jìn)程,而是立刻返回一個(gè)error。從用戶進(jìn)程角度講,它發(fā)起一個(gè)read操作后,并不需要等待,而是馬上就得到了一個(gè)結(jié)果。用戶進(jìn)程判斷結(jié)果是一個(gè)error時(shí),它就知道數(shù)據(jù)還沒有準(zhǔn)備好,于是它可以再次發(fā)送read操作。一旦kernel中的數(shù)據(jù)準(zhǔn)備好了,并且又再次收到了用戶進(jìn)程的系統(tǒng)調(diào)用,那么它馬上就將數(shù)據(jù)拷貝到了用戶內(nèi)存,然后返回。

所以,nonblocking IO的特點(diǎn)是用戶進(jìn)程需要不斷的主動詢問kernel數(shù)據(jù)好了沒有。

同步非阻塞方式相比同步阻塞方式:

優(yōu)點(diǎn):
能夠在等待任務(wù)完成的時(shí)間里干其他活了(包括提交其他任務(wù),也就是 “后臺” 可以有多個(gè)任務(wù)在同時(shí)執(zhí)行)。

缺點(diǎn):
任務(wù)完成的響應(yīng)延遲增大了,因?yàn)槊窟^一段時(shí)間才去輪詢一次read操作,而任務(wù)可能在兩次輪詢之間的任意時(shí)間完成。這會導(dǎo)致整體數(shù)據(jù)吞吐量的降低。

IO 多路復(fù)用( IO multiplexing)

場景描述

與第二個(gè)方案差不多,餐廳安裝了電子屏幕用來顯示點(diǎn)餐的狀態(tài),這樣我和女友逛街一會,回來就不用去詢問服務(wù)員了,直接看電子屏幕就可以了。這樣每個(gè)人的餐是否好了,都直接看電子屏幕就可以了。

這就是典型的IO多路復(fù)用。

網(wǎng)絡(luò)模型

由于同步非阻塞方式需要不斷主動輪詢,輪詢占據(jù)了很大一部分過程,輪詢會消耗大量的CPU時(shí)間,而 “后臺” 可能有多個(gè)任務(wù)在同時(shí)進(jìn)行,人們就想到了循環(huán)查詢多個(gè)任務(wù)的完成狀態(tài),只要有任何一個(gè)任務(wù)完成,就去處理它。如果輪詢不是進(jìn)程的用戶態(tài),而是有人幫忙就好了。那么這就是所謂的 “IO 多路復(fù)用”。UNIX/Linux 下的 select、poll、epoll 就是干這個(gè)的(epoll 比 poll、select 效率高,做的事情是一樣的)。

IO多路復(fù)用有兩個(gè)特別的系統(tǒng)調(diào)用select、poll、epoll函數(shù)。

select調(diào)用是內(nèi)核級別的,select輪詢相對非阻塞的輪詢的區(qū)別:

前者可以等待多個(gè)socket,能實(shí)現(xiàn)同時(shí)對多個(gè)IO端口進(jìn)行監(jiān)聽,當(dāng)其中任何一個(gè)socket的數(shù)據(jù)準(zhǔn)好了,就能返回進(jìn)行可讀,然后進(jìn)程再進(jìn)行recvform系統(tǒng)調(diào)用,將數(shù)據(jù)由內(nèi)核拷貝到用戶進(jìn)程,當(dāng)然這個(gè)過程是阻塞的。

select或poll調(diào)用之后,會阻塞進(jìn)程,與blocking IO阻塞不同在于:

此時(shí)的select不是等到socket數(shù)據(jù)全部到達(dá)再處理, 而是有了一部分?jǐn)?shù)據(jù)就會調(diào)用用戶進(jìn)程來處理。如何知道有一部分?jǐn)?shù)據(jù)到達(dá)了呢?監(jiān)視的事情交給了內(nèi)核,內(nèi)核負(fù)責(zé)數(shù)據(jù)到達(dá)的處理。也可以理解為"非阻塞"吧。

I/O復(fù)用模型會用到select、poll、epoll函數(shù),這幾個(gè)函數(shù)也會使進(jìn)程阻塞,但是和阻塞I/O所不同的,這兩個(gè)函數(shù)可以同時(shí)阻塞多個(gè)I/O操作。而且可以同時(shí)對多個(gè)讀操作,多個(gè)寫操作的I/O函數(shù)進(jìn)行檢測,直到有數(shù)據(jù)可讀或可寫時(shí)(注意不是全部數(shù)據(jù)可讀或可寫),才真正調(diào)用I/O操作函數(shù)。

對于多路復(fù)用,也就是輪詢多個(gè)socket。多路復(fù)用既然可以處理多個(gè)IO,也就帶來了新的問題,多個(gè)IO之間的順序變得不確定了,當(dāng)然也可以針對不同的編號。具體流程,如下圖所示:


image.png

流程描述

IO multiplexing就是我們說的select,poll,epoll,有些地方也稱這種IO方式為event driven IO。select/epoll的好處就在于單個(gè)process就可以同時(shí)處理多個(gè)網(wǎng)絡(luò)連接的IO。它的基本原理就是select,poll,epoll這個(gè)function會不斷的輪詢所負(fù)責(zé)的所有socket,當(dāng)某個(gè)socket有數(shù)據(jù)到達(dá)了,就通知用戶進(jìn)程。

當(dāng)用戶進(jìn)程調(diào)用了select,那么整個(gè)進(jìn)程會被block,而同時(shí),kernel會“監(jiān)視”所有select負(fù)責(zé)的socket,當(dāng)任何一個(gè)socket中的數(shù)據(jù)準(zhǔn)備好了,select就會返回。這個(gè)時(shí)候用戶進(jìn)程再調(diào)用read操作,將數(shù)據(jù)從kernel拷貝到用戶進(jìn)程。

多路復(fù)用的特點(diǎn)是通過一種機(jī)制一個(gè)進(jìn)程能同時(shí)等待IO文件描述符,內(nèi)核監(jiān)視這些文件描述符(套接字描述符),其中的任意一個(gè)進(jìn)入讀就緒狀態(tài),select, poll,epoll函數(shù)就可以返回。對于監(jiān)視的方式,又可以分為 select, poll, epoll三種方式。

上面的圖和blocking IO的圖其實(shí)并沒有太大的不同,事實(shí)上,還更差一些。因?yàn)檫@里需要使用兩個(gè)system call (select 和 recvfrom),而blocking IO只調(diào)用了一個(gè)system call (recvfrom)。但是,用select的優(yōu)勢在于它可以同時(shí)處理多個(gè)connection。

所以,如果處理的連接數(shù)不是很高的話,使用select/epoll的web server不一定比使用multi-threading + blocking IO的web server性能更好,可能延遲還更大。(select/epoll的優(yōu)勢并不是對于單個(gè)連接能處理得更快,而是在于能處理更多的連接。)

在IO multiplexing Model中,實(shí)際中,對于每一個(gè)socket,一般都設(shè)置成為non-blocking,但是,如上圖所示,整個(gè)用戶的process其實(shí)是一直被block的。只不過process是被select這個(gè)函數(shù)block,而不是被socket IO給block。所以IO多路復(fù)用是阻塞在select,epoll這樣的系統(tǒng)調(diào)用之上,而沒有阻塞在真正的I/O系統(tǒng)調(diào)用如recvfrom之上。

在I/O編程過程中,當(dāng)需要同時(shí)處理多個(gè)客戶端接入請求時(shí),可以利用多線程或者I/O多路復(fù)用技術(shù)進(jìn)行處理。I/O多路復(fù)用技術(shù)通過把多個(gè)I/O的阻塞復(fù)用到同一個(gè)select的阻塞上,從而使得系統(tǒng)在單線程的情況下可以同時(shí)處理多個(gè)客戶端請求。與傳統(tǒng)的多線程/多進(jìn)程模型比,I/O多路復(fù)用的最大優(yōu)勢是系統(tǒng)開銷小,系統(tǒng)不需要?jiǎng)?chuàng)建新的額外進(jìn)程或者線程,也不需要維護(hù)這些進(jìn)程和線程的運(yùn)行,降底了系統(tǒng)的維護(hù)工作量,節(jié)省了系統(tǒng)資源,I/O多路復(fù)用的主要應(yīng)用場景如下:

服務(wù)器需要同時(shí)處理多個(gè)處于監(jiān)聽狀態(tài)或者多個(gè)連接狀態(tài)的套接字。

服務(wù)器需要同時(shí)處理多種網(wǎng)絡(luò)協(xié)議的套接字。

了解了前面三種IO模式,在用戶進(jìn)程進(jìn)行系統(tǒng)調(diào)用的時(shí)候,他們在等待數(shù)據(jù)到來的時(shí)候,處理的方式不一樣,直接等待,輪詢,select或poll輪詢,兩個(gè)階段過程:

第一階段:等待數(shù)據(jù)準(zhǔn)備 (Waiting for the data to be ready)。

第二階段:將數(shù)據(jù)從內(nèi)核拷貝到進(jìn)程中 (Copying the data from the kernel to the process)。

第一個(gè)階段有的阻塞,有的不阻塞,有的可以阻塞又可以不阻塞。

第二個(gè)階段都是阻塞的。

從整個(gè)IO過程來看,他們都是順序執(zhí)行的,因此可以歸為同步模型(synchronous)。都是進(jìn)程主動等待且向內(nèi)核檢查狀態(tài)?!敬司浜苤匾。?!】

高并發(fā)的程序一般使用同步非阻塞方式 而非 多線程 + 同步阻塞方式。

要理解這一點(diǎn),首先要扯到并發(fā)和并行的區(qū)別。比如去某部門辦事需要依次去幾個(gè)窗口,辦事大廳里的人數(shù)就是并發(fā)數(shù),而窗口個(gè)數(shù)就是并行度。也就是說并發(fā)數(shù)是指同時(shí)進(jìn)行的任務(wù)數(shù)(如同時(shí)服務(wù)的 HTTP 請求),而并行數(shù)是可以同時(shí)工作的物理資源數(shù)量(如 CPU 核數(shù))。通過合理調(diào)度任務(wù)的不同階段,并發(fā)數(shù)可以遠(yuǎn)遠(yuǎn)大于并行度,這就是區(qū)區(qū)幾個(gè) CPU 可以支持上萬個(gè)用戶并發(fā)請求的奧秘。在這種高并發(fā)的情況下,為每個(gè)任務(wù)(用戶請求)創(chuàng)建一個(gè)進(jìn)程或線程的開銷非常大。而同步非阻塞方式可以把多個(gè) IO 請求丟到后臺去,這就可以在一個(gè)進(jìn)程里服務(wù)大量的并發(fā) IO 請求。

注意:IO多路復(fù)用是同步阻塞模型還是異步阻塞模型,在此給大家分析下:

同步與異步的根本性區(qū)別,同步是需要主動等待消息通知,而異步則是被動接收消息通知,通過回調(diào)、通知、狀態(tài)等方式來被動獲取消息。

IO多路復(fù)用在阻塞到select階段時(shí),用戶進(jìn)程是主動等待并調(diào)用select函數(shù)獲取數(shù)據(jù)就緒狀態(tài)消息,并且其進(jìn)程狀態(tài)為阻塞。

所以,把IO多路復(fù)用歸為同步阻塞模式。
參考:https://github.com/CyC2018/CS-Notes/issues/194

多路復(fù)用是同步的,阻塞/非阻塞取決于調(diào)用時(shí)的參數(shù)設(shè)置。

所有I/O多路復(fù)用操作都是同步的,涵蓋select/poll。
阻塞/非阻塞是相對于同步I/O來說的,與異步I/O無關(guān)。
select/poll/epoll本身是同步的,可以阻塞也可以不阻塞。

關(guān)于Select是否阻塞:
在使用int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout)函數(shù)時(shí),可以設(shè)置timeval決定該系統(tǒng)調(diào)用是否阻塞。

關(guān)于Poll是否阻塞:
在使用int poll(struct pollfd *fds, nfds_t nfds, int timeout)函數(shù)獲取信息時(shí),可以通過指定timeout的值來決定是否阻塞(當(dāng)timeout<0時(shí),會無限期阻塞;當(dāng)timeout=0時(shí),會立即返回)。

關(guān)于Epoll是否阻塞:
在使用epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout)函數(shù)來獲取是否有發(fā)生變化/事件的文件描述符時(shí),可以通過指定timeout來指定該調(diào)用是否阻塞(當(dāng)timeout=-1時(shí),會無限期阻塞;當(dāng)timeout=0時(shí),會立即返回)。

信號驅(qū)動式IO(signal-driven IO)

信號驅(qū)動式I/O:首先我們允許Socket進(jìn)行信號驅(qū)動IO,并安裝一個(gè)信號處理函數(shù),進(jìn)程繼續(xù)運(yùn)行并不阻塞。當(dāng)數(shù)據(jù)準(zhǔn)備好時(shí),進(jìn)程會收到一個(gè)SIGIO信號,可以在信號處理函數(shù)中調(diào)用I/O操作函數(shù)處理數(shù)據(jù)。過程如下圖所示:


image.png

異步非阻塞 IO(asynchronous IO)

場景描述

女友不想逛街,又餐廳太吵了,回家好好休息一下。于是我們叫外賣,打個(gè)電話點(diǎn)餐,然后我和女友可以在家好好休息一下,飯好了送貨員送到家里來。

這就是典型的異步,只需要打個(gè)電話說一下,然后可以做自己的事情,飯好了就送來了。

網(wǎng)絡(luò)模型

相對于同步IO,異步IO不是順序執(zhí)行。用戶進(jìn)程進(jìn)行aio_read系統(tǒng)調(diào)用之后,無論內(nèi)核數(shù)據(jù)是否準(zhǔn)備好,都會直接返回給用戶進(jìn)程,然后用戶態(tài)進(jìn)程可以去做別的事情。等到socket數(shù)據(jù)準(zhǔn)備好了,內(nèi)核直接復(fù)制數(shù)據(jù)給進(jìn)程,然后從內(nèi)核向進(jìn)程發(fā)送通知。IO兩個(gè)階段,進(jìn)程都是非阻塞的。

Linux提供了AIO庫函數(shù)實(shí)現(xiàn)異步,但是用的很少。目前有很多開源的異步IO庫,例如libevent、libev、libuv。異步過程如下圖所示:

image.png

流程描述

用戶進(jìn)程發(fā)起aio_read操作之后,立刻就可以開始去做其它的事。而另一方面,從kernel的角度,當(dāng)它受到一個(gè)asynchronous read之后,首先它會立刻返回,所以不會對用戶進(jìn)程產(chǎn)生任何block。然后,kernel會等待數(shù)據(jù)準(zhǔn)備完成,然后將數(shù)據(jù)拷貝到用戶內(nèi)存,當(dāng)這一切都完成之后,kernel會給用戶進(jìn)程發(fā)送一個(gè)signal或執(zhí)行一個(gè)基于線程的回調(diào)函數(shù)來完成這次 IO 處理過程,告訴它read操作完成了。

在 Linux 中,通知的方式是 “信號”:

如果這個(gè)進(jìn)程正在用戶態(tài)忙著做別的事(例如在計(jì)算兩個(gè)矩陣的乘積),那就強(qiáng)行打斷之,調(diào)用事先注冊的信號處理函數(shù),這個(gè)函數(shù)可以決定何時(shí)以及如何處理這個(gè)異步任務(wù)。由于信號處理函數(shù)是突然闖進(jìn)來的,因此跟中斷處理程序一樣,有很多事情是不能做的,因此保險(xiǎn)起見,一般是把事件 “登記” 一下放進(jìn)隊(duì)列,然后返回該進(jìn)程原來在做的事。

如果這個(gè)進(jìn)程正在內(nèi)核態(tài)忙著做別的事,例如以同步阻塞方式讀寫磁盤,那就只好把這個(gè)通知掛起來了,等到內(nèi)核態(tài)的事情忙完了,快要回到用戶態(tài)的時(shí)候,再觸發(fā)信號通知。

如果這個(gè)進(jìn)程現(xiàn)在被掛起了,例如無事可做 sleep 了,那就把這個(gè)進(jìn)程喚醒,下次有 CPU 空閑的時(shí)候,就會調(diào)度到這個(gè)進(jìn)程,觸發(fā)信號通知。

異步 API 說來輕巧,做來難,這主要是對 API 的實(shí)現(xiàn)者而言的。Linux 的異步 IO(AIO)支持是 2.6.22 才引入的,還有很多系統(tǒng)調(diào)用不支持異步 IO。Linux 的異步 IO 最初是為數(shù)據(jù)庫設(shè)計(jì)的,因此通過異步 IO 的讀寫操作不會被緩存或緩沖,這就無法利用操作系統(tǒng)的緩存與緩沖機(jī)制。

很多人把 Linux 的 O_NONBLOCK 認(rèn)為是異步方式,但事實(shí)上這是前面講的同步非阻塞方式。需要指出的是,雖然 Linux 上的 IO API 略顯粗糙,但每種編程框架都有封裝好的異步 IO 實(shí)現(xiàn)。操作系統(tǒng)少做事,把更多的自由留給用戶,正是 UNIX 的設(shè)計(jì)哲學(xué),也是 Linux 上編程框架百花齊放的一個(gè)原因。

從前面 IO 模型的分類中,我們可以看出 AIO異步非阻塞 的動機(jī):

同步阻塞模型需要在 IO 操作開始時(shí)阻塞應(yīng)用程序。這意味著不可能同時(shí)重疊進(jìn)行處理和 IO 操作。

同步非阻塞模型允許處理和 IO 操作重疊進(jìn)行,但是這需要應(yīng)用程序根據(jù)重現(xiàn)的規(guī)則來檢查 IO 操作的狀態(tài)。

這樣就剩下異步非阻塞 IO 了,它允許處理和 IO 操作重疊進(jìn)行,包括 IO 操作完成的通知。

IO多路復(fù)用除了需要阻塞之外,select 函數(shù)所提供的功能(異步阻塞 IO)與 AIO 類似。不過,它是對通知事件進(jìn)行阻塞,而不是對 IO 調(diào)用進(jìn)行阻塞。

五種IO模型總結(jié)

blocking和non-blocking區(qū)別

調(diào)用blocking IO會一直block住對應(yīng)的進(jìn)程直到操作完成,而non-blocking IO在kernel還準(zhǔn)備數(shù)據(jù)的情況下會立刻返回。

synchronous IO和asynchronous IO區(qū)別

在說明synchronous IO和asynchronous IO的區(qū)別之前,需要先給出兩者的定義。POSIX的定義是這樣子的:

A synchronous I/O operation causes the requesting process to be blocked until that I/O operation completes;

An asynchronous I/O operation does not cause the requesting process to be blocked;

兩者的區(qū)別就在于synchronous IO做 ”IO operation” 的時(shí)候會將process阻塞。按照這個(gè)定義,之前所述的blocking IO,non-blocking IO,IO multiplexing都屬于synchronous IO。

有人會說,non-blocking IO并沒有被block啊。這里有個(gè)非?!敖苹钡牡胤?,定義中所指的”IO operation”是指真實(shí)的IO操作,就是例子中的recvfrom這個(gè)system call。non-blocking IO在執(zhí)行recvfrom這個(gè)system call的時(shí)候,如果kernel的數(shù)據(jù)沒有準(zhǔn)備好,這時(shí)候不會block進(jìn)程。但是,當(dāng)kernel中數(shù)據(jù)準(zhǔn)備好的時(shí)候,recvfrom會將數(shù)據(jù)從kernel拷貝到用戶內(nèi)存中,這個(gè)時(shí)候進(jìn)程是被block了,在這段時(shí)間內(nèi),進(jìn)程是被block的。

而asynchronous IO則不一樣,當(dāng)進(jìn)程發(fā)起IO 操作之后,就直接返回再也不理睬了,直到kernel發(fā)送一個(gè)信號,告訴進(jìn)程說IO完成。在這整個(gè)過程中,進(jìn)程完全沒有被block。

各個(gè)IO Model的比較如圖所示:

image.png

通過上面的圖片,可以發(fā)現(xiàn)non-blocking IO和asynchronous IO的區(qū)別還是很明顯的。在non-blocking IO中,雖然進(jìn)程大部分時(shí)間都不會被block,但是它仍然要求進(jìn)程去主動的check,并且當(dāng)數(shù)據(jù)準(zhǔn)備完成以后,也需要進(jìn)程主動的再次調(diào)用recvfrom來將數(shù)據(jù)拷貝到用戶內(nèi)存。而asynchronous IO則完全不同。它就像是用戶進(jìn)程將整個(gè)IO操作交給了他人(kernel)完成,然后他人做完后發(fā)信號通知。在此期間,用戶進(jìn)程不需要去檢查IO操作的狀態(tài),也不需要主動的去拷貝數(shù)據(jù)。

IO模型舉例理解

例1:

阻塞IO, 給女神發(fā)一條短信, 說我來找你了, 然后就默默的一直等著女神下樓, 這個(gè)期間除了等待你不會做其他事情, 屬于備胎做法.

非阻塞IO, 給女神發(fā)短信, 如果不回, 接著再發(fā), 一直發(fā)到女神下樓, 這個(gè)期間你除了發(fā)短信等待不會做其他事情, 屬于專一做法.

IO多路復(fù)用, 是找一個(gè)宿管大媽來幫你監(jiān)視下樓的女生, 這個(gè)期間你可以些其他的事情. 例如可以順便看看其他妹子,玩玩王者榮耀, 上個(gè)廁所等等. IO復(fù)用又包括 select, poll, epoll 模式. 那么它們的區(qū)別是什么?

select大媽每一個(gè)女生下樓, select大媽都不知道這個(gè)是不是你的女神, 她需要一個(gè)一個(gè)詢問, 并且select大媽能力還有限, 最多一次幫你監(jiān)視1024個(gè)妹子。

poll大媽不限制盯著女生的數(shù)量, 只要是經(jīng)過宿舍樓門口的女生, 都會幫你去問是不是你女神。

epoll大媽不限制盯著女生的數(shù)量, 并且也不需要一個(gè)一個(gè)去問. 那么如何做呢?

epoll大媽會為每個(gè)進(jìn)宿舍樓的女生臉上貼上一個(gè)大字條,上面寫上女生自己的名字, 只要女生下樓了, epoll大媽就知道這個(gè)是不是你女神了, 然后大媽再通知你.

上面這些同步IO有一個(gè)共同點(diǎn)就是, 當(dāng)女神走出宿舍門口的時(shí)候, 你已經(jīng)站在宿舍門口等著女神的, 此時(shí)你屬于同步等待狀態(tài)。

異步IO 你告訴女神我來了, 然后你就去王者榮耀了, 一直到女神下樓了, 發(fā)現(xiàn)找不見你了,女神再給你打電話通知你, 說我下樓了, 你在哪呢? 這時(shí)候你才來到宿舍門口。

例2:
阻塞I/O模型 老李去火車站買票,排隊(duì)三天買到一張退票。 耗費(fèi):在車站吃喝拉撒睡 3天,其他事一件沒干。

非阻塞I/O模型 老李去火車站買票,隔12小時(shí)去火車站問有沒有退票,三天后買到一張票。耗費(fèi):往返車站6次,路上6小時(shí),其他時(shí)間做了好多事。

I/O復(fù)用模型

1.select/poll 老李去火車站買票,委托黃牛,然后每隔6小時(shí)電話黃牛詢問,黃牛三天內(nèi)買到票,然后老李去火車站交錢領(lǐng)票。

耗費(fèi):往返車站2次,路上2小時(shí),黃牛手續(xù)費(fèi)100元,打電話17次

2.epoll 老李去火車站買票,委托黃牛,黃牛買到后即通知老李去領(lǐng),然后老李去火車站交錢領(lǐng)票。

耗費(fèi):往返車站2次,路上2小時(shí),黃牛手續(xù)費(fèi)100元,無需打電話

信號驅(qū)動I/O模型 老李去火車站買票,給售票員留下電話,有票后,售票員電話通知老李,然后老李去火車站交錢領(lǐng)票。 耗費(fèi):往返車站2次,路上2小時(shí),免黃牛費(fèi)100元,無需打電話

異步I/O模型 老李去火車站買票,給售票員留下電話,有票后,售票員電話通知老李并快遞送票上門。 耗費(fèi):往返車站1次,路上1小時(shí),免黃牛費(fèi)100元,無需打電話

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容