計算機讀取文件時,程序經(jīng)歷了什么?
你有沒有想過當我們執(zhí)行I/O操作時計算機底層都發(fā)生了些什么?
在回答這個問題之前,我們先來看下為什么對于計算機來說I/O是極其重要的。
不能執(zhí)行I/O的計算機是什么?
相信對于程序員來說I/O操作是最為熟悉不過的了:
當我們使用C語言中的printf、C++中的"<<",Python中的print,Java中的System.out.println等時,這是I/O;當我們使用各種語言讀寫文件時,這也是I/O;當我們通過TCP/IP進行網(wǎng)絡通信時,這同樣是I/O;當我們使用鼠標龍飛鳳舞時,當我們扛起鍵盤在評論區(qū)里指點江山亦或是埋頭苦干努力制造bug時、當我們能看到屏幕上的漂亮的圖形界面時等等,這一切都是I/O。
想一想,如果沒有I/O計算機該是一種多么枯燥的設備,不能看電影、不能玩游戲,也不能上網(wǎng),這樣的計算機最多就是一個大號的計算器。
既然I/O這么重要,那么到底什么才是I/O呢?
什么是I/O
I/O就是簡單的數(shù)據(jù)Copy,僅此而已。
這一點很重要,為了加深大家的印象,來,Everybody,F(xiàn)ollow me,那邊樹上的朋友,還有那邊墻上的朋友們,舉起你們的雙手,跟我唱,蒼茫的天涯是。。。Sorry,I/O僅僅就是數(shù)據(jù)copy、I/O僅僅就是數(shù)據(jù)copy。
讓我們先把演唱會的事情放在一邊,既然是copy數(shù)據(jù),又是從哪里copy到哪里呢?
如果數(shù)據(jù)是從外部設備copy到內(nèi)存中,這就是Input。
如果數(shù)據(jù)是從內(nèi)存copy到外部設備,這就是Output。
內(nèi)存與外部設備之間不嫌麻煩的來回copy數(shù)據(jù)就是Input and Output,簡稱I/O(Input/Output),僅此而已。

I/O與CPU
現(xiàn)在我們知道了什么是I/O,接下來就是重點部分了,大家注意,坐穩(wěn)了。
我們知道現(xiàn)在的CPU其主頻都是數(shù)GHz起步,這是什么意思呢?簡單說就是CPU執(zhí)行機器指令的速度是納秒級別的,而通常的I/O比如磁盤操作,一次磁盤seek大概在毫秒級別,因此如果我們把CPU的速度比作戰(zhàn)斗機的話,那么I/O操作的速度就是肯德雞。

也就是說當我們的程序跑起來時(CPU執(zhí)行機器指令),其速度是要遠遠快于I/O速度的,那么接下來的問題就是二者速度相差這么大,那么我們該如何設計、該如何更加合理的高效利用系統(tǒng)資源呢?
既然有速度差異,而且進程在執(zhí)行完I/O操作前不能繼續(xù)向前推進,那么顯然只有一個辦法,那就是等待,wait。
同樣是等待,有聰明的等待,也有傻傻的等待,簡稱傻等,那么是選擇聰明的等待呢還是選擇傻等呢?
假設你是一個急性子(CPU),需要等待一個重要的文件,不巧的是這個文件只能快遞過來(I/O),那么這時你是選擇什么事情都不干了,深情的注視著門口就像盼望著你的哈尼一樣專心等待這個快遞呢?還是暫時先不要管快遞了,玩?zhèn)€游戲看個電影刷會兒短視頻等快遞來了再說呢?
很顯然,更好的方法就是先去干其它事情,快遞來了再說。
因此這里的關(guān)鍵點就是快遞沒到前手頭上的事情可以先暫停,切換到其它任務,等快遞過來了再切換回來。
理解了這一點你就能明白執(zhí)行I/O操作時底層都發(fā)生了什么。
接下來讓我們以讀取磁盤文件為例來講解這一過程。
執(zhí)行I/O時底層都發(fā)生了什么
在上一篇《一文徹底理解高并發(fā)高性能中的線程與線程池》中,我們引入了進程和線程的概念,在支持線程的操作系統(tǒng)中,實際上被調(diào)度的是線程而不是進程,為了更加清晰的理解I/O過程,我們暫時假設操作系統(tǒng)只有進程這樣的概念,先不去考慮線程,這并不會影響我們的討論。
現(xiàn)在內(nèi)存中有兩個進程,進程A和進程B,當前進程A正在運行,如圖所示:

進程A中有一段讀取文件的代碼,不管在什么語言中通常我們定義一個用來裝數(shù)據(jù)的buff,然后調(diào)用read之類的函數(shù),像這樣:
read(buff);這就是一種典型的I/O操作,當CPU執(zhí)行到這段代碼的時候會向磁盤發(fā)送讀取請求,注意與CPU執(zhí)行指令的速度相比,I/O操作操作是非常慢的,因此操作系統(tǒng)是不可能把寶貴的CPU計算資源浪費在無謂的等待上的,這時重點來了,注意接下來是重點哦。
由于外部設備執(zhí)行I/O操作是相當慢的,因此在I/O操作完成之前進程是無法繼續(xù)向前推進的,這就是所謂的阻塞,即通常所說的block。操作系統(tǒng)檢測到進程向I/O設備發(fā)起請求后就暫停進程的運行,怎么暫停運行呢?很簡單,只需要記錄下當前進程的運行狀態(tài)并把CPU的PC寄存器指向其它進程的指令就可以了。
進程有暫停就會有繼續(xù)執(zhí)行,因此操作系統(tǒng)必須保存被暫停的進程以備后續(xù)繼續(xù)執(zhí)行,顯然我們可以用隊列來保存被暫停執(zhí)行的進程,如圖所示,進程A被暫停執(zhí)行并被放到阻塞隊列中(注意,不同的操作系統(tǒng)會有不同的實現(xiàn),可能每個I/O設備都有一個對應的阻塞隊列,但這種實現(xiàn)細節(jié)上的差異不影響我們的討論)。

這時操作系統(tǒng)已經(jīng)向磁盤發(fā)送了I/O請求,因此磁盤driver開始將磁盤中的數(shù)據(jù)copy到進程A的buff中,雖然這時進程A已經(jīng)被暫停執(zhí)行了,但這并不妨礙磁盤向內(nèi)存中copy數(shù)據(jù)。注意,現(xiàn)代磁盤向內(nèi)存copy數(shù)據(jù)時無需借助CPU的幫助,這就是所謂的DMA(Direct Memory Access),這個過程如圖所示:

讓磁盤先copy著數(shù)據(jù),我們接著聊。
實際上操作系統(tǒng)中除了有阻塞隊列之外也有就緒隊列,所謂就緒隊列是指隊列里的進程準備就緒可以被CPU執(zhí)行了,你可能會問為什么不直接執(zhí)行非要有個就緒隊列呢?答案很簡單,那就是僧多粥少,在即使只有1個核的機器上也可以創(chuàng)建出成千上萬個進程,CPU不可能同時執(zhí)行這么多的進程,因此必然存在這樣的進程,即使其一切準備就緒也不能被分配到計算資源,這樣的進程就被放到了就緒隊列。
現(xiàn)在進程B就位于就緒隊列,萬事俱備只欠CPU,如圖所示:

當進程A被暫停執(zhí)行后CPU是不可以閑下來的,因為就緒隊列中還有嗷嗷待哺的進程B,這時操作系統(tǒng)開始在就緒隊列中找下一個可以執(zhí)行的進程,也就是這里的進程B。
此時操作系統(tǒng)將進程B從就緒隊列中取出,找出進程B被暫停時執(zhí)行到的機器指令的位置,然后將CPU的PC寄存器指向該位置,這樣進程B就開始運行啦,如圖所示:

注意,注意,接下來的這段是重點中的重點。
注意觀察上圖,此時進程B在被CPU執(zhí)行,磁盤在向進程A的內(nèi)存空間中copy數(shù)據(jù),看出來了嗎,大家都在忙,誰都沒有閑著,數(shù)據(jù)copy和指令執(zhí)行在同時進行,在操作系統(tǒng)的調(diào)度下,CPU、磁盤都得到了充分的利用,這就是程序員的智慧所在。
現(xiàn)在你應該理解為什么操作系統(tǒng)這么重要了吧。
此后磁盤終于將全部數(shù)據(jù)都copy到了進程A的內(nèi)存中,這時磁盤通知操作系統(tǒng)任務完成啦,你可能會問怎么通知呢?這就是中斷。
操作系統(tǒng)接收到磁盤中斷后發(fā)現(xiàn)數(shù)據(jù)copy完畢,進程A重新獲得繼續(xù)運行的資格,這時操作系統(tǒng)小心翼翼的把進程A從阻塞隊列放到了就緒隊列當中,如圖所示:

注意,從前面關(guān)于就緒狀態(tài)的討論中我們知道,操作系統(tǒng)是不會直接運行進程A的,進程A必須被放到就緒隊列中等待,這樣對大家都公平。
此后進程B繼續(xù)執(zhí)行,進程A繼續(xù)等待,進程B執(zhí)行了一會兒后操作系統(tǒng)認為進程B執(zhí)行的時間夠長了,因此把進程B放到就緒隊列,把進程A取出并繼續(xù)執(zhí)行。
注意操作系統(tǒng)把進程B放到的是就緒隊列,因此進程B被暫停運行僅僅是因為時間片到了而不是因為發(fā)起I/O請求被阻塞,如圖所示:

進程A繼續(xù)執(zhí)行,此時buff中已經(jīng)裝滿了想要的數(shù)據(jù),進程A就這樣愉快的運行下去了,就好像從來沒有被暫停過一樣,進程對于自己被暫停一事一無所知,這就是操作系統(tǒng)的魔法。
現(xiàn)在你應該明白了I/O是一個怎樣的過程了吧。
這種進程執(zhí)行I/O操作被阻塞暫停執(zhí)行的方式被稱為阻塞式I/O,blocking I/O,這也是最常見最容易理解的I/O方式,有阻塞式I/O就有非阻塞式I/O,在這里我們暫時先不考慮這種方式。
在本節(jié)開頭我們說過暫時只考慮進程而不考慮線程,現(xiàn)在我們放寬這個條件,實際上也非常簡單,只需要把前圖中調(diào)度的進程改為線程就可以了,這里的討論對于線程一樣成立。
零拷貝,Zero-copy
最后需要注意的一點就是上面的講解中我們直接把磁盤數(shù)據(jù)copy到了進程空間中,但實際上一般情況下I/O數(shù)據(jù)是要首先copy到操作系統(tǒng)內(nèi)部,然后操作系統(tǒng)再copy到進程空間中。因此我們可以看到這里其實還有一層經(jīng)過操作系統(tǒng)的copy,對于性能要求很高的場景其實也是可以繞過操作系統(tǒng)直接進行數(shù)據(jù)copy的,這也是本文描述的場景,這種繞過操作系統(tǒng)直接進行數(shù)據(jù)copy的技術(shù)被稱為Zero-copy,也就零拷貝,高并發(fā)、高性能場景下常用的一種技術(shù),原理上很簡單吧。
總結(jié)
本文講解的是程序員常用的I/O,一般來說作為程序員我們無需關(guān)心,但是理解I/O背后的底層原理對于設計高性能、高并發(fā)系統(tǒng)是極為有益的,希望這篇能對大家加深對I/O的認識有所幫助。
最近熱文:
2T技術(shù)資源大放送!包括但不限于:C/C++,Linux,Python,Java,人工智能,考研,軟考,英語,等等。在公眾號內(nèi)回復「資源」,即可免費獲??!回復「社群」,可以邀請你加入讀者群!
最近熱文:
2T技術(shù)資源大放送!包括但不限于:C/C++,Linux,Python,Java,人工智能,考研,軟考,英語,等等。在公眾號內(nèi)回復「資源」,即可免費獲??!回復「社群」,可以邀請你加入讀者群!
點個在看少個 bug??
