相關(guān)概念:
用戶空間與內(nèi)核空間
現(xiàn)在操作系統(tǒng)都是采用虛擬存儲(chǔ)器,操作系統(tǒng)的核心是內(nèi)核,獨(dú)立于普通的應(yīng)用程序,可以訪問(wèn)受保護(hù)的內(nèi)存空間养渴,也有訪問(wèn)底層硬件設(shè)備的所有權(quán)限。為了保證用戶進(jìn)程不能直接操作內(nèi)核(kernel)臂拓,保證內(nèi)核的安全厚脉,操作系統(tǒng)將虛擬空間劃分為兩部分习寸,一部分為內(nèi)核空間胶惰,一部分為用戶空間。針對(duì)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)程切換痘绎。因此可以說(shuō),任何進(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è)過(guò)程中經(jīng)過(guò)下面這些變化:
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ā)生仰美,如請(qǐng)求系統(tǒng)資源失敗、等待某種操作的完成儿礼、新數(shù)據(jù)尚未到達(dá)或無(wú)新工作做等咖杂,則由系統(tǒng)自動(dòng)執(zhí)行阻塞原語(yǔ)(Block),使自己由運(yùn)行狀態(tài)變?yōu)樽枞麪顟B(tài)蚊夫∷咦郑可見(jiàn),進(jìn)程的阻塞是進(jìn)程自身的一種主動(dòng)行為知纷,也因此只有處于運(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ù)語(yǔ),是一個(gè)用于表述指向文件的引用的抽象化概念乍桂。
文件描述符在形式上是一個(gè)非負(fù)整數(shù)冲杀。實(shí)際上,它是一個(gè)索引值睹酌,指向內(nèi)核為每一個(gè)進(jìn)程所維護(hù)的該進(jìn)程打開(kāi)文件的記錄表权谁。當(dāng)程序打開(kāi)一個(gè)現(xiàn)有文件或者創(chuàng)建一個(gè)新文件時(shí),內(nèi)核向進(jìn)程返回一個(gè)文件描述符忍疾。在程序設(shè)計(jì)中闯传,一些涉及底層的程序編寫(xiě)往往會(huì)圍繞著文件描述符展開(kāi)。但是文件描述符這一概念往往只適用于UNIX卤妒、Linux這樣的操作系統(tǒng)甥绿。
緩存 IO 和 直接IO
緩存IO:數(shù)據(jù)從磁盤(pán)先通過(guò)DMA copy到內(nèi)核空間,再?gòu)膬?nèi)核空間通過(guò)cpu copy到用戶空間则披。
直接IO:數(shù)據(jù)從磁盤(pán)通過(guò)DMA copy到用戶空間共缕。
緩存IO
緩存 IO 又被稱作標(biāo)準(zhǔn) IO,大多數(shù)文件系統(tǒng)的默認(rèn) IO 操作都是緩存 IO士复。在 Linux 的緩存 IO 機(jī)制中图谷,操作系統(tǒng)會(huì)將 IO 的數(shù)據(jù)緩存在文件系統(tǒng)的頁(yè)緩存( page cache )中翩活,也就是說(shuō),數(shù)據(jù)會(huì)先從磁盤(pán)被拷貝到操作系統(tǒng)內(nèi)核的緩沖區(qū)中便贵,然后才會(huì)從操作系統(tǒng)內(nèi)核的緩沖區(qū)拷貝到應(yīng)用程序的地址空間菠镇。
讀操作:
操作系統(tǒng)檢查內(nèi)核的緩沖區(qū)有沒(méi)有需要的數(shù)據(jù),如果已經(jīng)緩存了承璃,那么就直接從緩存中返回利耍;否則從磁盤(pán)中讀
取,然后緩存在操作系統(tǒng)的緩存中盔粹。
寫(xiě)操作:
將數(shù)據(jù)從用戶空間復(fù)制到內(nèi)核空間的緩存中隘梨。這時(shí)對(duì)用戶程序來(lái)說(shuō)寫(xiě)操作就已經(jīng)完成,至于什么時(shí)候再寫(xiě)到磁
盤(pán)中由操作系統(tǒng)決定舷嗡,除非顯示地調(diào)用了sync同步命令轴猎。
緩存I/O的優(yōu)點(diǎn):
在一定程度上分離了內(nèi)核空間和用戶空間,保護(hù)系統(tǒng)本身的運(yùn)行安全进萄;
可以減少讀盤(pán)的次數(shù)捻脖,從而提高性能。
緩存I/O的缺點(diǎn):
在緩存 I/O 機(jī)制中垮斯,DMA 方式可以將數(shù)據(jù)直接從磁盤(pán)讀到頁(yè)緩存中郎仆,或者將數(shù)據(jù)從頁(yè)緩存直接寫(xiě)回到磁盤(pán)上,而不能直接在應(yīng)用程序地址空間和磁盤(pán)之間進(jìn)行數(shù)據(jù)傳輸兜蠕,這樣,數(shù)據(jù)在傳輸過(guò)程中需要在應(yīng)用程序地址空間(用戶空間)和緩存(內(nèi)核空間)之間進(jìn)行多次數(shù)據(jù)拷貝操作抛寝,這些數(shù)據(jù)拷貝操作所帶來(lái)的CPU以及內(nèi)存開(kāi)銷是非常大的熊杨。
直接IO
直接IO就是應(yīng)用程序直接訪問(wèn)磁盤(pán)數(shù)據(jù),而不經(jīng)過(guò)內(nèi)核緩沖區(qū)盗舰,也就是繞過(guò)內(nèi)核緩沖區(qū)晶府,自己管理I/O緩存區(qū),這樣做的目的是減少一次從內(nèi)核緩沖區(qū)到用戶程序緩存的數(shù)據(jù)復(fù)制钻趋。
引入內(nèi)核緩沖區(qū)的目的在于提高磁盤(pán)文件的訪問(wèn)性能川陆,因?yàn)楫?dāng)進(jìn)程需要讀取磁盤(pán)文件時(shí),如果文件內(nèi)容已經(jīng)在內(nèi)核緩沖區(qū)中蛮位,那么就不需要再次訪問(wèn)磁盤(pán)较沪;而當(dāng)進(jìn)程需要向文件中寫(xiě)入數(shù)據(jù)時(shí),實(shí)際上只是寫(xiě)到了內(nèi)核緩沖區(qū)便告訴進(jìn)程已經(jīng)寫(xiě)成功失仁,而真正寫(xiě)入磁盤(pán)是通過(guò)一定的策略進(jìn)行延遲的尸曼。
然而,對(duì)于一些較復(fù)雜的應(yīng)用萄焦,比如數(shù)據(jù)庫(kù)服務(wù)器控轿,它們?yōu)榱顺浞痔岣咝阅埽M@過(guò)內(nèi)核緩沖區(qū),由自己在用戶態(tài)空間實(shí)現(xiàn)并管理I/O緩沖區(qū)茬射,包括緩存機(jī)制和寫(xiě)延遲機(jī)制等鹦蠕,以支持獨(dú)特的查詢機(jī)制,比如數(shù)據(jù)庫(kù)可以根據(jù)更加合理的策略來(lái)提高查詢緩存命中率在抛。另一方面片部,繞過(guò)內(nèi)核緩沖區(qū)也可以減少系統(tǒng)內(nèi)存的開(kāi)銷,因?yàn)閮?nèi)核緩沖區(qū)本身就在使用系統(tǒng)內(nèi)存霜定。
應(yīng)用程序直接訪問(wèn)磁盤(pán)數(shù)據(jù)档悠,不經(jīng)過(guò)操作系統(tǒng)內(nèi)核數(shù)據(jù)緩沖區(qū),這樣做的目的是減少一次從內(nèi)核緩沖區(qū)到用戶程序緩存的數(shù)據(jù)復(fù)制望浩。這種方式通常是在對(duì)數(shù)據(jù)的緩存管理由應(yīng)用程序?qū)崿F(xiàn)的數(shù)據(jù)庫(kù)管理系統(tǒng)中辖所。
直接I/O的缺點(diǎn):
如果訪問(wèn)的數(shù)據(jù)不在應(yīng)用程序緩存中,那么每次數(shù)據(jù)都會(huì)直接從磁盤(pán)進(jìn)行加載磨德,這種直接加載會(huì)非常緩慢缘回。通常直接I/O跟異步I/O結(jié)合使用會(huì)得到較好的性能。
訪問(wèn)步驟:
Linux提供了對(duì)這種需求的支持典挑,即在open()系統(tǒng)調(diào)用中增加參數(shù)選項(xiàng)O_DIRECT酥宴,用它打開(kāi)的文件便可以繞過(guò)內(nèi)核緩沖區(qū)的直接訪問(wèn),這樣便有效避免了CPU和內(nèi)存的多余時(shí)間開(kāi)銷您觉。
順便提一下拙寡,與O_DIRECT類似的一個(gè)選項(xiàng)是O_SYNC,后者只對(duì)寫(xiě)數(shù)據(jù)有效琳水,它將寫(xiě)入內(nèi)核緩沖區(qū)的數(shù)據(jù)立即寫(xiě)入磁盤(pán)肆糕,將機(jī)器故障時(shí)數(shù)據(jù)的丟失減少到最小,但是它仍然要經(jīng)過(guò)內(nèi)核緩沖區(qū)在孝。
Linux IO模型
網(wǎng)絡(luò)IO的本質(zhì)是socket的讀取诚啃,socket在linux系統(tǒng)被抽象為流,IO可以理解為對(duì)流的操作私沮。剛才說(shuō)了始赎,對(duì)于一次IO訪問(wèn)(以read舉例),數(shù)據(jù)會(huì)先被拷貝到操作系統(tǒng)內(nèi)核的緩沖區(qū)中仔燕,然后才會(huì)從操作系統(tǒng)內(nèi)核的緩沖區(qū)拷貝到應(yīng)用程序的地址空間造垛。所以說(shuō),當(dāng)一個(gè)read操作發(fā)生時(shí)涨享,它會(huì)經(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)。
對(duì)于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ú)非就是兩大類問(wèn)題髓迎,網(wǎng)絡(luò)IO峦朗,數(shù)據(jù)計(jì)算。相對(duì)于后者排龄,網(wǎng)絡(luò)IO的延遲波势,給應(yīng)用帶來(lái)的性能瓶頸大于后者。
網(wǎng)絡(luò)IO的模型大致有如下幾種:
同步模型(synchronous IO)
阻塞IO(bloking IO)
非阻塞IO(non-blocking IO)
多路復(fù)用IO(multiplexing IO)
信號(hào)驅(qū)動(dòng)式IO(signal-driven IO):不常用
異步IO(asynchronous IO)
常見(jiàn)的IO模型有阻塞橄维、非阻塞尺铣、IO多路復(fù)用,異步争舞。
以一個(gè)生動(dòng)形象的例子來(lái)說(shuō)明這四個(gè)概念凛忿。周末我和女友去逛街,中午餓了竞川,我們準(zhǔn)備去吃飯店溢。周末人多,吃飯需要排隊(duì)委乌,我和女友有以下幾種方案床牧。
同步阻塞 IO(blocking IO)
場(chǎng)景描述
我和女友點(diǎn)完餐后,不知道什么時(shí)候能做好遭贸,只好坐在餐廳里面等戈咳,直到做好,然后吃完才離開(kāi)革砸。女友本想還和我一起逛街的除秀,但是不知道飯能什么時(shí)候做好,只好和我一起在餐廳等算利,而不能去逛街,直到吃完飯才能去逛街泳姐,中間等待做飯的時(shí)間浪費(fèi)掉了效拭。--------------------這就是典型的阻塞。
網(wǎng)絡(luò)模型
同步阻塞 IO 模型是最常用的一個(gè)模型胖秒,也是最簡(jiǎn)單的模型缎患。在linux中,默認(rèn)情況下所有的socket都是blocking阎肝。它符合人們最常見(jiàn)的思考邏輯挤渔。阻塞就是進(jìn)程 "被" 休息, CPU處理其它進(jìn)程去了。
在這個(gè)IO模型中风题,用戶空間的應(yīng)用程序執(zhí)行一個(gè)系統(tǒng)調(diào)用(recvform)判导,這會(huì)導(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 而只是簡(jiǎn)單等待響應(yīng)的狀態(tài)昵骤,因此從處理的角度來(lái)看树碱,這是非常有效的。在調(diào)用recv()/recvfrom()函數(shù)時(shí)变秦,發(fā)生在內(nèi)核中等待數(shù)據(jù)和復(fù)制數(shù)據(jù)的過(guò)程成榜,大致如下圖:
流程描述
當(dāng)用戶進(jìn)程調(diào)用了recv()/recvfrom()這個(gè)系統(tǒng)調(diào)用,kernel就開(kāi)始了IO的第一個(gè)階段:準(zhǔn)備數(shù)據(jù)(對(duì)于網(wǎng)絡(luò)IO來(lái)說(shuō)伴栓,很多時(shí)候數(shù)據(jù)在一開(kāi)始還沒(méi)有到達(dá)伦连。比如,還沒(méi)有收到一個(gè)完整的UDP包钳垮。這個(gè)時(shí)候kernel就要等待足夠的數(shù)據(jù)到來(lái))惑淳。這個(gè)過(guò)程需要等待,也就是說(shuō)數(shù)據(jù)被拷貝到操作系統(tǒng)內(nèi)核的緩沖區(qū)中是需要一個(gè)過(guò)程的饺窿。而在用戶進(jìn)程這邊歧焦,整個(gè)進(jìn)程會(huì)被阻塞(當(dāng)然,是進(jìn)程自己選擇的阻塞)肚医。第二個(gè)階段:當(dāng)kernel一直等到數(shù)據(jù)準(zhǔn)備好了绢馍,它就會(huì)將數(shù)據(jù)從kernel中拷貝到用戶內(nèi)存,然后kernel返回結(jié)果肠套,用戶進(jìn)程才解除block的狀態(tài)舰涌,重新運(yùn)行起來(lái)。
所以你稚,blocking IO的特點(diǎn)就是在IO執(zhí)行的兩個(gè)階段都被block了瓷耙。
優(yōu)點(diǎn):
能夠及時(shí)返回?cái)?shù)據(jù),無(wú)延遲刁赖;
對(duì)內(nèi)核開(kāi)發(fā)者來(lái)說(shuō)這是省事了搁痛;
缺點(diǎn):
對(duì)用戶來(lái)說(shuō)處于等待就要付出性能的代價(jià)了;
同步非阻塞 IO(nonblocking IO)
場(chǎng)景描述
我女友不甘心白白在這等宇弛,又想去逛商場(chǎng)鸡典,又擔(dān)心飯好了。所以我們逛一會(huì)枪芒,回來(lái)詢問(wèn)服務(wù)員飯好了沒(méi)有彻况,來(lái)來(lái)回回好多次谁尸,飯都還沒(méi)吃都快累死了啦。
這就是非阻塞疗垛。需要不斷的詢問(wèn)症汹,是否準(zhǔn)備好了。
網(wǎng)絡(luò)模型
同步非阻塞就是 “每隔一會(huì)兒瞄一眼進(jìn)度條” 的輪詢(polling)方式贷腕。在這種模型中背镇,設(shè)備是以非阻塞的形式打開(kāi)的。這意味著 IO 操作不會(huì)立即完成泽裳,read 操作可能會(huì)返回一個(gè)錯(cuò)誤代碼瞒斩,說(shuō)明這個(gè)命令不能立即滿足(EAGAIN 或 EWOULDBLOCK)。
在網(wǎng)絡(luò)IO時(shí)候涮总,非阻塞IO也會(huì)進(jìn)行recvform系統(tǒng)調(diào)用胸囱,檢查數(shù)據(jù)是否準(zhǔn)備好,與阻塞IO不一樣瀑梗,"非阻塞將大的整片時(shí)間的阻塞分成N多的小的阻塞, 所以進(jìn)程不斷地有機(jī)會(huì) '被' CPU光顧"烹笔。
也就是說(shuō)非阻塞的recvform系統(tǒng)調(diào)用之后,進(jìn)程并沒(méi)有被阻塞抛丽,內(nèi)核馬上返回給進(jìn)程谤职,如果數(shù)據(jù)還沒(méi)準(zhǔn)備好,此時(shí)會(huì)返回一個(gè)error亿鲜。進(jìn)程在返回之后搀暑,可以干點(diǎn)別的事情撤奸,然后再發(fā)起recvform系統(tǒng)調(diào)用哑子。重復(fù)上面的過(guò)程岳枷,循環(huán)往復(fù)的進(jìn)行recvform系統(tǒng)調(diào)用。這個(gè)過(guò)程通常被稱之為輪詢垒探。輪詢檢查內(nèi)核數(shù)據(jù)妓蛮,直到數(shù)據(jù)準(zhǔn)備好,再拷貝數(shù)據(jù)到進(jìn)程圾叼,進(jìn)行數(shù)據(jù)處理仔引。需要注意,拷貝數(shù)據(jù)整個(gè)過(guò)程褐奥,進(jìn)程仍然是屬于阻塞的狀態(tài)。
在linux下翘簇,可以通過(guò)設(shè)置socket使其變?yōu)閚on-blocking撬码。當(dāng)對(duì)一個(gè)non-blocking socket執(zhí)行讀操作時(shí),流程如圖所示:
流程描述
當(dāng)用戶進(jìn)程發(fā)出read操作時(shí)版保,如果kernel中的數(shù)據(jù)還沒(méi)有準(zhǔn)備好呜笑,那么它并不會(huì)block用戶進(jìn)程夫否,而是立刻返回一個(gè)error。從用戶進(jìn)程角度講叫胁,它發(fā)起一個(gè)read操作后凰慈,并不需要等待,而是馬上就得到了一個(gè)結(jié)果驼鹅。用戶進(jìn)程判斷結(jié)果是一個(gè)error時(shí)微谓,它就知道數(shù)據(jù)還沒(méi)有準(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)程需要不斷的主動(dòng)詢問(wèn)kernel數(shù)據(jù)好了沒(méi)有剪验。
同步非阻塞方式相比同步阻塞方式:
優(yōu)點(diǎn):
能夠在等待任務(wù)完成的時(shí)間里干其他活了(包括提交其他任務(wù)肴焊,也就是 “后臺(tái)” 可以有多個(gè)任務(wù)在同時(shí)執(zhí)行)。
缺點(diǎn):
任務(wù)完成的響應(yīng)延遲增大了功戚,因?yàn)槊窟^(guò)一段時(shí)間才去輪詢一次read操作娶眷,而任務(wù)可能在兩次輪詢之間的任意時(shí)間完成。這會(huì)導(dǎo)致整體數(shù)據(jù)吞吐量的降低疫铜。
IO 多路復(fù)用( IO multiplexing)
場(chǎng)景描述
與第二個(gè)方案差不多茂浮,餐廳安裝了電子屏幕用來(lái)顯示點(diǎn)餐的狀態(tài),這樣我和女友逛街一會(huì)壳咕,回來(lái)就不用去詢問(wèn)服務(wù)員了席揽,直接看電子屏幕就可以了。這樣每個(gè)人的餐是否好了谓厘,都直接看電子屏幕就可以了幌羞。
這就是典型的IO多路復(fù)用。
網(wǎng)絡(luò)模型
由于同步非阻塞方式需要不斷主動(dòng)輪詢竟稳,輪詢占據(jù)了很大一部分過(guò)程属桦,輪詢會(huì)消耗大量的CPU時(shí)間,而 “后臺(tái)” 可能有多個(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)核級(jí)別的,select輪詢相對(duì)非阻塞的輪詢的區(qū)別:
前者可以等待多個(gè)socket预愤,能實(shí)現(xiàn)同時(shí)對(duì)多個(gè)IO端口進(jìn)行監(jiān)聽(tīng)沟于,當(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è)過(guò)程是阻塞的销睁。
select或poll調(diào)用之后供璧,會(huì)阻塞進(jìn)程,與blocking IO阻塞不同在于:
此時(shí)的select不是等到socket數(shù)據(jù)全部到達(dá)再處理, 而是有了一部分?jǐn)?shù)據(jù)就會(huì)調(diào)用用戶進(jìn)程來(lái)處理冻记。如何知道有一部分?jǐn)?shù)據(jù)到達(dá)了呢睡毒?監(jiān)視的事情交給了內(nèi)核,內(nèi)核負(fù)責(zé)數(shù)據(jù)到達(dá)的處理冗栗。也可以理解為"非阻塞"吧演顾。
I/O復(fù)用模型會(huì)用到select、poll隅居、epoll函數(shù)钠至,這幾個(gè)函數(shù)也會(huì)使進(jìn)程阻塞,但是和阻塞I/O所不同的胎源,這兩個(gè)函數(shù)可以同時(shí)阻塞多個(gè)I/O操作棉钧。而且可以同時(shí)對(duì)多個(gè)讀操作,多個(gè)寫(xiě)操作的I/O函數(shù)進(jìn)行檢測(cè)涕蚤,直到有數(shù)據(jù)可讀或可寫(xiě)時(shí)(注意不是全部數(shù)據(jù)可讀或可寫(xiě))宪卿,才真正調(diào)用I/O操作函數(shù)。
對(duì)于多路復(fù)用万栅,也就是輪詢多個(gè)socket佑钾。多路復(fù)用既然可以處理多個(gè)IO,也就帶來(lái)了新的問(wèn)題烦粒,多個(gè)IO之間的順序變得不確定了次绘,當(dāng)然也可以針對(duì)不同的編號(hào)。具體流程,如下圖所示:
流程描述
IO multiplexing就是我們說(shuō)的select邮偎,poll,epoll义黎,有些地方也稱這種IO方式為event driven IO禾进。select/epoll的好處就在于單個(gè)process就可以同時(shí)處理多個(gè)網(wǎng)絡(luò)連接的IO。它的基本原理就是select廉涕,poll泻云,epoll這個(gè)function會(huì)不斷的輪詢所負(fù)責(zé)的所有socket,當(dāng)某個(gè)socket有數(shù)據(jù)到達(dá)了狐蜕,就通知用戶進(jìn)程宠纯。
當(dāng)用戶進(jìn)程調(diào)用了select,那么整個(gè)進(jìn)程會(huì)被block层释,而同時(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)程乖寒。
多路復(fù)用的特點(diǎn)是通過(guò)一種機(jī)制一個(gè)進(jìn)程能同時(shí)等待IO文件描述符猴蹂,內(nèi)核監(jiān)視這些文件描述符(套接字描述符),其中的任意一個(gè)進(jìn)入讀就緒狀態(tài)楣嘁,select磅轻, poll,epoll函數(shù)就可以返回逐虚。對(duì)于監(jiān)視的方式聋溜,又可以分為 select, poll痊班, epoll三種方式勤婚。
上面的圖和blocking IO的圖其實(shí)并沒(méi)有太大的不同,事實(shí)上涤伐,還更差一些馒胆。因?yàn)檫@里需要使用兩個(gè)system call (select 和 recvfrom),而blocking IO只調(diào)用了一個(gè)system call (recvfrom)凝果。但是祝迂,用select的優(yōu)勢(shì)在于它可以同時(shí)處理多個(gè)connection。
所以器净,如果處理的連接數(shù)不是很高的話型雳,使用select/epoll的web server不一定比使用multi-threading + blocking IO的web server性能更好,可能延遲還更大。(select/epoll的優(yōu)勢(shì)并不是對(duì)于單個(gè)連接能處理得更快纠俭,而是在于能處理更多的連接沿量。)
在IO multiplexing Model中,實(shí)際中冤荆,對(duì)于每一個(gè)socket朴则,一般都設(shè)置成為non-blocking,但是钓简,如上圖所示乌妒,整個(gè)用戶的process其實(shí)是一直被block的。只不過(guò)process是被select這個(gè)函數(shù)block外邓,而不是被socket IO給block撤蚊。所以IO多路復(fù)用是阻塞在select,epoll這樣的系統(tǒng)調(diào)用之上损话,而沒(méi)有阻塞在真正的I/O系統(tǒng)調(diào)用如recvfrom之上侦啸。
在I/O編程過(guò)程中,當(dāng)需要同時(shí)處理多個(gè)客戶端接入請(qǐng)求時(shí)席镀,可以利用多線程或者I/O多路復(fù)用技術(shù)進(jìn)行處理匹中。I/O多路復(fù)用技術(shù)通過(guò)把多個(gè)I/O的阻塞復(fù)用到同一個(gè)select的阻塞上,從而使得系統(tǒng)在單線程的情況下可以同時(shí)處理多個(gè)客戶端請(qǐng)求豪诲。與傳統(tǒng)的多線程/多進(jìn)程模型比顶捷,I/O多路復(fù)用的最大優(yōu)勢(shì)是系統(tǒng)開(kāi)銷小,系統(tǒng)不需要?jiǎng)?chuàng)建新的額外進(jìn)程或者線程屎篱,也不需要維護(hù)這些進(jìn)程和線程的運(yùn)行服赎,降底了系統(tǒng)的維護(hù)工作量,節(jié)省了系統(tǒng)資源交播,I/O多路復(fù)用的主要應(yīng)用場(chǎng)景如下:
服務(wù)器需要同時(shí)處理多個(gè)處于監(jiān)聽(tīng)狀態(tài)或者多個(gè)連接狀態(tài)的套接字重虑。
服務(wù)器需要同時(shí)處理多種網(wǎng)絡(luò)協(xié)議的套接字。
了解了前面三種IO模式秦士,在用戶進(jìn)程進(jìn)行系統(tǒng)調(diào)用的時(shí)候缺厉,他們?cè)诘却龜?shù)據(jù)到來(lái)的時(shí)候,處理的方式不一樣隧土,直接等待提针,輪詢,select或poll輪詢曹傀,兩個(gè)階段過(guò)程:
第一階段:等待數(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過(guò)程來(lái)看久锥,他們都是順序執(zhí)行的家淤,因此可以歸為同步模型(synchronous)。都是進(jìn)程主動(dòng)等待且向內(nèi)核檢查狀態(tài)奴拦∶焦模【此句很重要!4硌!】
高并發(fā)的程序一般使用同步非阻塞方式 而非 多線程 + 同步阻塞方式疚沐。
要理解這一點(diǎn)暂氯,首先要扯到并發(fā)和并行的區(qū)別。比如去某部門(mén)辦事需要依次去幾個(gè)窗口亮蛔,辦事大廳里的人數(shù)就是并發(fā)數(shù)痴施,而窗口個(gè)數(shù)就是并行度。也就是說(shuō)并發(fā)數(shù)是指同時(shí)進(jìn)行的任務(wù)數(shù)(如同時(shí)服務(wù)的 HTTP 請(qǐng)求)究流,而并行數(shù)是可以同時(shí)工作的物理資源數(shù)量(如 CPU 核數(shù))辣吃。通過(guò)合理調(diào)度任務(wù)的不同階段,并發(fā)數(shù)可以遠(yuǎn)遠(yuǎn)大于并行度芬探,這就是區(qū)區(qū)幾個(gè) CPU 可以支持上萬(wàn)個(gè)用戶并發(fā)請(qǐng)求的奧秘神得。在這種高并發(fā)的情況下,為每個(gè)任務(wù)(用戶請(qǐng)求)創(chuàng)建一個(gè)進(jìn)程或線程的開(kāi)銷非常大偷仿。而同步非阻塞方式可以把多個(gè) IO 請(qǐng)求丟到后臺(tái)去哩簿,這就可以在一個(gè)進(jìn)程里服務(wù)大量的并發(fā) IO 請(qǐng)求。
注意:IO多路復(fù)用是同步阻塞模型還是異步阻塞模型酝静,在此給大家分析下:
同步與異步的根本性區(qū)別节榜,同步是需要主動(dòng)等待消息通知,而異步則是被動(dòng)接收消息通知别智,通過(guò)回調(diào)宗苍、通知、狀態(tài)等方式來(lái)被動(dòng)獲取消息薄榛。
IO多路復(fù)用在阻塞到select階段時(shí)讳窟,用戶進(jìn)程是主動(dòng)等待并調(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。
阻塞/非阻塞是相對(duì)于同步I/O來(lái)說(shuō)的馏予,與異步I/O無(wú)關(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í)蛹尝,可以通過(guò)指定timeout的值來(lái)決定是否阻塞(當(dāng)timeout<0時(shí)后豫,會(huì)無(wú)限期阻塞;當(dāng)timeout=0時(shí)突那,會(huì)立即返回)挫酿。
關(guān)于Epoll是否阻塞:
在使用epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout)函數(shù)來(lái)獲取是否有發(fā)生變化/事件的文件描述符時(shí),可以通過(guò)指定timeout來(lái)指定該調(diào)用是否阻塞(當(dāng)timeout=-1時(shí)愕难,會(huì)無(wú)限期阻塞早龟;當(dāng)timeout=0時(shí),會(huì)立即返回)猫缭。
信號(hào)驅(qū)動(dòng)式IO(signal-driven IO)
信號(hào)驅(qū)動(dòng)式I/O:首先我們?cè)试SSocket進(jìn)行信號(hào)驅(qū)動(dòng)IO,并安裝一個(gè)信號(hào)處理函數(shù)葱弟,進(jìn)程繼續(xù)運(yùn)行并不阻塞。當(dāng)數(shù)據(jù)準(zhǔn)備好時(shí)猜丹,進(jìn)程會(huì)收到一個(gè)SIGIO信號(hào)芝加,可以在信號(hào)處理函數(shù)中調(diào)用I/O操作函數(shù)處理數(shù)據(jù)。過(guò)程如下圖所示:
異步非阻塞 IO(asynchronous IO)
場(chǎng)景描述
女友不想逛街居触,又餐廳太吵了妖混,回家好好休息一下。于是我們叫外賣轮洋,打個(gè)電話點(diǎn)餐制市,然后我和女友可以在家好好休息一下,飯好了送貨員送到家里來(lái)弊予。
這就是典型的異步祥楣,只需要打個(gè)電話說(shuō)一下,然后可以做自己的事情汉柒,飯好了就送來(lái)了误褪。
網(wǎng)絡(luò)模型
相對(duì)于同步IO,異步IO不是順序執(zhí)行碾褂。用戶進(jìn)程進(jìn)行aio_read系統(tǒng)調(diào)用之后兽间,無(wú)論內(nèi)核數(shù)據(jù)是否準(zhǔn)備好,都會(huì)直接返回給用戶進(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庫(kù)函數(shù)實(shí)現(xiàn)異步讼育,但是用的很少帐姻。目前有很多開(kāi)源的異步IO庫(kù),例如libevent奶段、libev饥瓷、libuv。異步過(guò)程如下圖所示:
流程描述
用戶進(jìn)程發(fā)起aio_read操作之后痹籍,立刻就可以開(kāi)始去做其它的事扛伍。而另一方面,從kernel的角度词裤,當(dāng)它受到一個(gè)asynchronous read之后,首先它會(huì)立刻返回鳖宾,所以不會(huì)對(duì)用戶進(jìn)程產(chǎn)生任何block吼砂。然后,kernel會(huì)等待數(shù)據(jù)準(zhǔn)備完成鼎文,然后將數(shù)據(jù)拷貝到用戶內(nèi)存渔肩,當(dāng)這一切都完成之后,kernel會(huì)給用戶進(jìn)程發(fā)送一個(gè)signal或執(zhí)行一個(gè)基于線程的回調(diào)函數(shù)來(lái)完成這次 IO 處理過(guò)程拇惋,告訴它read操作完成了周偎。
在 Linux 中,通知的方式是 “信號(hào)”:
如果這個(gè)進(jìn)程正在用戶態(tài)忙著做別的事(例如在計(jì)算兩個(gè)矩陣的乘積)撑帖,那就強(qiáng)行打斷之蓉坎,調(diào)用事先注冊(cè)的信號(hào)處理函數(shù),這個(gè)函數(shù)可以決定何時(shí)以及如何處理這個(gè)異步任務(wù)胡嘿。由于信號(hào)處理函數(shù)是突然闖進(jìn)來(lái)的蛉艾,因此跟中斷處理程序一樣,有很多事情是不能做的衷敌,因此保險(xiǎn)起見(jiàn)勿侯,一般是把事件 “登記” 一下放進(jìn)隊(duì)列,然后返回該進(jìn)程原來(lái)在做的事缴罗。
如果這個(gè)進(jìn)程正在內(nèi)核態(tài)忙著做別的事助琐,例如以同步阻塞方式讀寫(xiě)磁盤(pán),那就只好把這個(gè)通知掛起來(lái)了面氓,等到內(nèi)核態(tài)的事情忙完了兵钮,快要回到用戶態(tài)的時(shí)候蛆橡,再觸發(fā)信號(hào)通知。
如果這個(gè)進(jìn)程現(xiàn)在被掛起了矢空,例如無(wú)事可做 sleep 了航罗,那就把這個(gè)進(jìn)程喚醒,下次有 CPU 空閑的時(shí)候屁药,就會(huì)調(diào)度到這個(gè)進(jìn)程粥血,觸發(fā)信號(hào)通知。
異步 API 說(shuō)來(lái)輕巧酿箭,做來(lái)難复亏,這主要是對(duì) API 的實(shí)現(xiàn)者而言的。Linux 的異步 IO(AIO)支持是 2.6.22 才引入的缭嫡,還有很多系統(tǒng)調(diào)用不支持異步 IO缔御。Linux 的異步 IO 最初是為數(shù)據(jù)庫(kù)設(shè)計(jì)的,因此通過(guò)異步 IO 的讀寫(xiě)操作不會(huì)被緩存或緩沖妇蛀,這就無(wú)法利用操作系統(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異步非阻塞 的動(dòng)機(jī):
同步阻塞模型需要在 IO 操作開(kāi)始時(shí)阻塞應(yīng)用程序嗡呼。這意味著不可能同時(shí)重疊進(jìn)行處理和 IO 操作纸俭。
同步非阻塞模型允許處理和 IO 操作重疊進(jìn)行,但是這需要應(yīng)用程序根據(jù)重現(xiàn)的規(guī)則來(lái)檢查 IO 操作的狀態(tài)晤锥。
這樣就剩下異步非阻塞 IO 了掉蔬,它允許處理和 IO 操作重疊進(jìn)行,包括 IO 操作完成的通知矾瘾。
IO多路復(fù)用除了需要阻塞之外女轿,select 函數(shù)所提供的功能(異步阻塞 IO)與 AIO 類似。不過(guò)壕翩,它是對(duì)通知事件進(jìn)行阻塞蛉迹,而不是對(duì) IO 調(diào)用進(jìn)行阻塞。
五種IO模型總結(jié)
blocking和non-blocking區(qū)別
調(diào)用blocking IO會(huì)一直block住對(duì)應(yīng)的進(jìn)程直到操作完成放妈,而non-blocking IO在kernel還準(zhǔn)備數(shù)據(jù)的情況下會(huì)立刻返回北救。
synchronous IO和asynchronous IO區(qū)別
在說(shuō)明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í)候會(huì)將process阻塞珍策。按照這個(gè)定義托启,之前所述的blocking IO,non-blocking IO攘宙,IO multiplexing都屬于synchronous IO屯耸。
有人會(huì)說(shuō),non-blocking IO并沒(méi)有被block啊蹭劈。這里有個(gè)非沉菩澹“狡猾”的地方,定義中所指的”IO operation”是指真實(shí)的IO操作铺韧,就是例子中的recvfrom這個(gè)system call多矮。non-blocking IO在執(zhí)行recvfrom這個(gè)system call的時(shí)候,如果kernel的數(shù)據(jù)沒(méi)有準(zhǔn)備好哈打,這時(shí)候不會(huì)block進(jìn)程塔逃。但是,當(dāng)kernel中數(shù)據(jù)準(zhǔn)備好的時(shí)候料仗,recvfrom會(huì)將數(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è)信號(hào)颜阐,告訴進(jìn)程說(shuō)IO完成平窘。在這整個(gè)過(guò)程中,進(jìn)程完全沒(méi)有被block凳怨。
各個(gè)IO Model的比較如圖所示:
通過(guò)上面的圖片瑰艘,可以發(fā)現(xiàn)non-blocking IO和asynchronous IO的區(qū)別還是很明顯的。在non-blocking IO中肤舞,雖然進(jìn)程大部分時(shí)間都不會(huì)被block紫新,但是它仍然要求進(jìn)程去主動(dòng)的check,并且當(dāng)數(shù)據(jù)準(zhǔn)備完成以后李剖,也需要進(jìn)程主動(dòng)的再次調(diào)用recvfrom來(lái)將數(shù)據(jù)拷貝到用戶內(nèi)存芒率。而asynchronous IO則完全不同。它就像是用戶進(jìn)程將整個(gè)IO操作交給了他人(kernel)完成篙顺,然后他人做完后發(fā)信號(hào)通知偶芍。在此期間充择,用戶進(jìn)程不需要去檢查IO操作的狀態(tài),也不需要主動(dòng)的去拷貝數(shù)據(jù)匪蟀。
IO模型舉例理解
例1:
阻塞IO, 給女神發(fā)一條短信, 說(shuō)我來(lái)找你了, 然后就默默的一直等著女神下樓, 這個(gè)期間除了等待你不會(huì)做其他事情, 屬于備胎做法.
非阻塞IO, 給女神發(fā)短信, 如果不回, 接著再發(fā), 一直發(fā)到女神下樓, 這個(gè)期間你除了發(fā)短信等待不會(huì)做其他事情, 屬于專一做法.
IO多路復(fù)用, 是找一個(gè)宿管大媽來(lái)幫你監(jiān)視下樓的女生, 這個(gè)期間你可以些其他的事情. 例如可以順便看看其他妹子,玩玩王者榮耀, 上個(gè)廁所等等. IO復(fù)用又包括 select, poll, epoll 模式. 那么它們的區(qū)別是什么?
select大媽每一個(gè)女生下樓, select大媽都不知道這個(gè)是不是你的女神, 她需要一個(gè)一個(gè)詢問(wèn), 并且select大媽能力還有限, 最多一次幫你監(jiān)視1024個(gè)妹子椎麦。
poll大媽不限制盯著女生的數(shù)量, 只要是經(jīng)過(guò)宿舍樓門(mén)口的女生, 都會(huì)幫你去問(wèn)是不是你女神。
epoll大媽不限制盯著女生的數(shù)量, 并且也不需要一個(gè)一個(gè)去問(wèn). 那么如何做呢?
epoll大媽會(huì)為每個(gè)進(jìn)宿舍樓的女生臉上貼上一個(gè)大字條,上面寫(xiě)上女生自己的名字, 只要女生下樓了, epoll大媽就知道這個(gè)是不是你女神了, 然后大媽再通知你.
上面這些同步IO有一個(gè)共同點(diǎn)就是, 當(dāng)女神走出宿舍門(mén)口的時(shí)候, 你已經(jīng)站在宿舍門(mén)口等著女神的, 此時(shí)你屬于同步等待狀態(tài)材彪。
異步IO 你告訴女神我來(lái)了, 然后你就去王者榮耀了, 一直到女神下樓了, 發(fā)現(xiàn)找不見(jiàn)你了,女神再給你打電話通知你, 說(shuō)我下樓了, 你在哪呢? 這時(shí)候你才來(lái)到宿舍門(mén)口观挎。
例2:
阻塞I/O模型 老李去火車站買票,排隊(duì)三天買到一張退票查刻。 耗費(fèi):在車站吃喝拉撒睡 3天键兜,其他事一件沒(méi)干。
非阻塞I/O模型 老李去火車站買票穗泵,隔12小時(shí)去火車站問(wèn)有沒(méi)有退票普气,三天后買到一張票。耗費(fèi):往返車站6次佃延,路上6小時(shí)现诀,其他時(shí)間做了好多事。
I/O復(fù)用模型
1.select/poll 老李去火車站買票履肃,委托黃牛仔沿,然后每隔6小時(shí)電話黃牛詢問(wèn),黃牛三天內(nèi)買到票尺棋,然后老李去火車站交錢(qián)領(lǐng)票封锉。
耗費(fèi):往返車站2次,路上2小時(shí)膘螟,黃牛手續(xù)費(fèi)100元成福,打電話17次
2.epoll 老李去火車站買票,委托黃牛荆残,黃牛買到后即通知老李去領(lǐng)奴艾,然后老李去火車站交錢(qián)領(lǐng)票。
耗費(fèi):往返車站2次内斯,路上2小時(shí)蕴潦,黃牛手續(xù)費(fèi)100元,無(wú)需打電話
信號(hào)驅(qū)動(dòng)I/O模型 老李去火車站買票俘闯,給售票員留下電話潭苞,有票后,售票員電話通知老李真朗,然后老李去火車站交錢(qián)領(lǐng)票萄传。 耗費(fèi):往返車站2次,路上2小時(shí),免黃牛費(fèi)100元秀菱,無(wú)需打電話
異步I/O模型 老李去火車站買票振诬,給售票員留下電話,有票后衍菱,售票員電話通知老李并快遞送票上門(mén)赶么。 耗費(fèi):往返車站1次,路上1小時(shí)脊串,免黃牛費(fèi)100元辫呻,無(wú)需打電話