你們知道當(dāng)程序需要讀取或者寫入數(shù)據(jù)的時(shí)候,CPU是如何操作我們的磁盤的嗎?首先CPU肯定是要把讀寫數(shù)據(jù)的命令告訴給磁盤,這個(gè)命令可以通過IO總線傳給磁盤,那這里有個(gè)細(xì)節(jié),其實(shí)我們常說的磁盤不僅僅是只包含存儲(chǔ)數(shù)據(jù)的媒介,還有接口,接口相信大家都熟悉,接口的意義不僅僅是為了連接到IO總線上的,其實(shí)這個(gè)接口里還有個(gè)叫做控制器的東西,控制器才是真正控制磁盤讀寫的東西,當(dāng)CPU發(fā)出讀寫指令的時(shí)候,這個(gè)指令其實(shí)是告訴磁盤控制器的。以讀為例,當(dāng)控制器收到讀的請(qǐng)求時(shí),它告訴磁盤:“你把xx數(shù)據(jù)給我吧”,當(dāng)機(jī)械硬盤經(jīng)過轉(zhuǎn)動(dòng)、尋道找到目標(biāo)扇區(qū)后,把目標(biāo)數(shù)據(jù)給磁盤控制器:“哥,這是你要的數(shù)據(jù)”,控制器收到數(shù)據(jù)之后,其實(shí)不會(huì)立馬通知CPU,因?yàn)樾枰x的數(shù)據(jù)可能涉及到多個(gè)扇區(qū),如果每讀一個(gè)扇區(qū)的數(shù)據(jù)就通知,會(huì)導(dǎo)致效率低下。
CPU:“控制器老弟,你這是搞事啊,我很忙的,每次搞這么點(diǎn)數(shù)據(jù)就通知我,能不能把我需要的數(shù)據(jù)都準(zhǔn)備好,再通知我”。
“控制器”:“好的,CPU老哥”。
于是控制器內(nèi)部就搞了個(gè)緩沖區(qū),把讀到的數(shù)據(jù)先緩存起來,然后通知CPU來取數(shù)據(jù),但是問題又發(fā)生了...
CPU:“控制器老弟,數(shù)據(jù)你是準(zhǔn)備好了,但是你給我的數(shù)據(jù)已經(jīng)是損壞的,玩我呢!”
“控制器”:“CPU老哥,俺錯(cuò)了,下次一定不會(huì)”。
于是控制器為了判斷讀到的數(shù)據(jù)是否發(fā)生了損壞,會(huì)先計(jì)算下校驗(yàn)和,如果校驗(yàn)和不通過,那么就不會(huì)通知CPU來取壞的數(shù)據(jù)了。
當(dāng)緩沖區(qū)快要滿了或者需要讀的數(shù)據(jù)已經(jīng)讀完了并且校驗(yàn)數(shù)據(jù)也是OK的,這時(shí)控制器就會(huì)發(fā)出個(gè)中斷:“CPU老哥,你要的數(shù)據(jù)好了,過來取吧”,于是CPU屁顛屁顛的過來拿數(shù)據(jù),當(dāng)然它也是分批拿的,每次從控制器的緩沖區(qū)中一個(gè)字節(jié)一個(gè)字節(jié)的拿,直至取完。整個(gè)過程看起來還不錯(cuò),但是有個(gè)很嚴(yán)重的效率問題:CPU每次取數(shù)據(jù)的單位有點(diǎn)小(一個(gè)字節(jié)),這樣勢(shì)必造成CPU多次往返,那有什么辦法解決這個(gè)問題呢?我們接著往下看。
緩沖
在講緩沖之前,我們先了解一下當(dāng)我們的程序發(fā)出read的時(shí)候,數(shù)據(jù)是怎么返回的,首先和設(shè)備打交道的時(shí)候,需要發(fā)起系統(tǒng)調(diào)用,系統(tǒng)調(diào)用會(huì)導(dǎo)致進(jìn)入內(nèi)核態(tài),然后CPU去讀數(shù)據(jù),讀到數(shù)據(jù)后,在把數(shù)據(jù)返給用戶程序,這時(shí)又回到用戶態(tài)。
這里我們先著重看下數(shù)據(jù)從內(nèi)核態(tài)到到用戶態(tài)的過程,通過上文我們知道CPU是一個(gè)字節(jié)一個(gè)字節(jié)的讀取數(shù)據(jù)的,當(dāng)CPU拿到數(shù)據(jù)之后,可以有這樣幾個(gè)選擇:
每次讀到一個(gè)字節(jié)后立馬發(fā)出中斷,然后由中斷程序把每個(gè)字節(jié)交給用戶進(jìn)程,用戶進(jìn)程收到數(shù)據(jù)之后,再發(fā)起下個(gè)字節(jié)的讀取,就這樣不停的循環(huán)...,直至把數(shù)據(jù)讀完。這種模式的問題在于每個(gè)字節(jié)都要喚起進(jìn)程,然后用戶進(jìn)程繼續(xù)阻塞等待下個(gè)字節(jié)的到來,很傻很低效。
用戶程序可以每次多讀點(diǎn)數(shù)據(jù),比如每次告訴CPU:“我要讀n個(gè)字節(jié)”,CPU收到指令后去磁盤把數(shù)據(jù)讀到,當(dāng)然這里肯定不是一個(gè)字節(jié)一個(gè)字節(jié)的發(fā)起中斷,不然和1無區(qū)別,由于一開始已經(jīng)告訴CPU要讀n個(gè)字節(jié),所以要等讀滿n個(gè)字節(jié)后才能發(fā)起中斷,那如何知道讀滿n個(gè)字節(jié)了呢?這就需要緩沖了,可以在用戶空間開辟一個(gè)n個(gè)字節(jié)的緩沖區(qū),當(dāng)緩沖區(qū)滿了,再發(fā)起中斷,相比第一種n次中斷,這里只需要一次中斷,是不是效率提高了許多。
第二種方法解決了用戶程序低效的問題,但是不要忘記了還有CPU,CPU還是一個(gè)字節(jié)一個(gè)字節(jié)的把數(shù)據(jù)搬運(yùn)到用戶的緩沖區(qū)中,這樣看CPU還是挺辛苦的,不僅要讀取數(shù)據(jù),還要低效的把數(shù)據(jù)從內(nèi)核空間搬運(yùn)到用戶空間,注意這個(gè)在內(nèi)核空間和用戶空間之間的切換還是挺耗費(fèi)時(shí)間的,于是為了減少切換開銷,內(nèi)核空間干脆也搞個(gè)緩沖區(qū),等緩沖區(qū)有足夠多的數(shù)據(jù)之后,一次性的給到用戶程序,這樣是不是就高效多了。
可以發(fā)現(xiàn)最后一種肯定是效率最高的,這也是現(xiàn)代操作系統(tǒng)普遍使用的方式,然而這種模式也不是百分百的完美,我們來看下相關(guān)的時(shí)序圖。
時(shí)序圖中我們先重點(diǎn)看下CPU這塊,可以發(fā)現(xiàn)當(dāng)控制器的緩沖區(qū)滿了之后需要CPU把數(shù)據(jù)copy到內(nèi)核緩沖區(qū),然后CPU再把內(nèi)核緩沖區(qū)的數(shù)據(jù)copy到用戶緩沖區(qū),CPU不僅要負(fù)責(zé)數(shù)據(jù)的讀寫還要負(fù)責(zé)數(shù)據(jù)的搬運(yùn)。