1. 程式人生 > 實用技巧 >讀取檔案時,程式經歷了什麼?

讀取檔案時,程式經歷了什麼?

你有沒有想過當我們執行I/O操作時計算機底層都發生了些什麼?

在回答這個問題之前,我們先來看下為什麼對於計算機來說I/O是極其重要的。

不能執行I/O的計算機是什麼?

相信對於程式設計師來說I/O操作是最為熟悉不過的了:

當我們使用C語言中的printf、C++中的"<<",Python中的print,Java中的System.out.println等時,這是I/O;當我們使用各種語言讀寫檔案時,這也是I/O;當我們通過TCP/IP進行網路通訊時,這同樣是I/O;當我們使用滑鼠龍飛鳳舞時,當我們扛起鍵盤在評論區裡指點江山亦或是埋頭苦幹努力製造bug時、當我們能看到螢幕上的漂亮的圖形介面時等等,這一切都是I/O。

想一想,如果沒有I/O計算機該是一種多麼枯燥的裝置,不能看電影、不能玩遊戲,也不能上網,這樣的計算機最多就是一個大號的計算器。

既然I/O這麼重要,那麼到底什麼才是I/O呢?

什麼是I/O

I/O就是簡單的資料Copy,僅此而已。

這一點很重要,為了加深大家的印象,來,Everybody,Follow me,那邊樹上的朋友,還有那邊牆上的朋友們,舉起你們的雙手,跟我唱,蒼茫的天涯是。。。Sorry,I/O僅僅就是資料copy、I/O僅僅就是資料copy。

讓我們先把演唱會的事情放在一邊,既然是copy資料,又是從哪裡copy到哪裡呢?

如果資料是從外部裝置copy到記憶體中,這就是Input。

如果資料是從記憶體copy到外部裝置,這就是Output。

記憶體與外部裝置之間不嫌麻煩的來回copy資料就是Input and Output,簡稱I/O(Input/Output),僅此而已。

img

I/O與CPU

現在我們知道了什麼是I/O,接下來就是重點部分了,大家注意,坐穩了。

我們知道現在的CPU其主頻都是數GHz起步,這是什麼意思呢?簡單說就是CPU執行機器指令的速度是納秒級別的,而通常的I/O比如磁碟操作,一次磁碟seek大概在毫秒級別,因此如果我們把CPU的速度比作戰鬥機的話,那麼I/O操作的速度就是肯德雞

img

也就是說當我們的程式跑起來時(CPU執行機器指令),其速度是要遠遠快於I/O速度的,那麼接下來的問題就是二者速度相差這麼大,那麼我們該如何設計、該如何更加合理的高效利用系統資源呢?

既然有速度差異,而且程序在執行完I/O操作前不能繼續向前推進,那麼顯然只有一個辦法,那就是等待,wait

同樣是等待,有聰明的等待,也有傻傻的等待,簡稱傻等,那麼是選擇聰明的等待呢還是選擇傻等呢?

假設你是一個急性子(CPU),需要等待一個重要的檔案,不巧的是這個檔案只能快遞過來(I/O),那麼這時你是選擇什麼事情都不幹了,深情的注視著門口就像盼望著你的哈尼一樣專心等待這個快遞呢?還是暫時先不要管快遞了,玩個遊戲看個電影刷會兒短視訊等快遞來了再說呢?

很顯然,更好的方法就是先去幹其它事情,快遞來了再說。

因此這裡的關鍵點就是快遞沒到前手頭上的事情可以先暫停,切換到其它任務,等快遞過來了再切換回來

理解了這一點你就能明白執行I/O操作時底層都發生了什麼。

接下來讓我們以讀取磁碟檔案為例來講解這一過程。

執行I/O時底層都發生了什麼

在上一篇《一文徹底理解高併發高效能中的執行緒與執行緒池》中,我們引入了程序和執行緒的概念,在支援執行緒的作業系統中,實際上被排程的是執行緒而不是程序,為了更加清晰的理解I/O過程,我們暫時假設作業系統只有程序這樣的概念,先不去考慮執行緒,這並不會影響我們的討論。

現在記憶體中有兩個程序,程序A和程序B,當前程序A正在執行,如圖所示:

img

程序A中有一段讀取檔案的程式碼,不管在什麼語言中通常我們定義一個用來裝資料的buff,然後呼叫read之類的函式,像這樣:

read(buff);

這就是一種典型的I/O操作,當CPU執行到這段程式碼的時候會向磁碟傳送讀取請求,注意與CPU執行指令的速度相比,I/O操作操作是非常慢的,因此作業系統是不可能把寶貴的CPU計算資源浪費在無謂的等待上的,這時重點來了,注意接下來是重點哦。

由於外部裝置執行I/O操作是相當慢的,因此在I/O操作完成之前程序是無法繼續向前推進的,這就是所謂的阻塞,即通常所說的block。作業系統檢測到程序向I/O裝置發起請求後就暫停程序的執行,怎麼暫停執行呢?很簡單,只需要記錄下當前程序的執行狀態並把CPU的PC暫存器指向其它程序的指令就可以了。

程序有暫停就會有繼續執行,因此作業系統必須儲存被暫停的程序以備後續繼續執行,顯然我們可以用佇列來儲存被暫停執行的程序,如圖所示,程序A被暫停執行並被放到阻塞佇列中(注意,不同的作業系統會有不同的實現,可能每個I/O裝置都有一個對應的阻塞佇列,但這種實現細節上的差異不影響我們的討論)。

img

這時作業系統已經向磁碟傳送了I/O請求,因此磁碟driver開始將磁碟中的資料copy到程序A的buff中,雖然這時程序A已經被暫停執行了,但這並不妨礙磁碟向記憶體中copy資料。注意,現代磁碟向記憶體copy資料時無需藉助CPU的幫助,這就是所謂的DMA(Direct Memory Access),這個過程如圖所示:

img

讓磁碟先copy著資料,我們接著聊。

實際上作業系統中除了有阻塞佇列之外也有就緒佇列,所謂就緒佇列是指佇列裡的程序準備就緒可以被CPU執行了,你可能會問為什麼不直接執行非要有個就緒佇列呢?答案很簡單,那就是僧多粥少,在即使只有1個核的機器上也可以創建出成千上萬個程序,CPU不可能同時執行這麼多的程序,因此必然存在這樣的程序,即使其一切準備就緒也不能被分配到計算資源,這樣的程序就被放到了就緒佇列。

現在程序B就位於就緒佇列,萬事俱備只欠CPU,如圖所示:

img

當程序A被暫停執行後CPU是不可以閒下來的,因為就緒佇列中還有嗷嗷待哺的程序B,這時作業系統開始在就緒佇列中找下一個可以執行的程序,也就是這裡的程序B。

此時作業系統將程序B從就緒佇列中取出,找出程序B被暫停時執行到的機器指令的位置,然後將CPU的PC暫存器指向該位置,這樣程序B就開始執行啦,如圖所示:

img

注意,注意,接下來的這段是重點中的重點。

注意觀察上圖,你能看出這種設計的精妙之處嗎,這對於理解作業系統至關重要,關注公眾號“碼農的荒島求生”回覆“過程”二字你就能得到答案以及該過程的最後兩個步驟啦。

零拷貝,Zero-copy

最後需要注意的一點就是上面的講解中我們直接把磁碟資料copy到了程序空間中,但實際上一般情況下I/O資料是要首先copy到作業系統內部,然後作業系統再copy到程序空間中。因此我們可以看到這裡其實還有一層經過作業系統的copy,對於效能要求很高的場景其實也是可以繞過作業系統直接進行資料copy的,這也是本文描述的場景,這種繞過作業系統直接進行資料copy的技術被稱為Zero-copy,也就零拷貝,高併發、高效能場景下常用的一種技術,原理上很簡單吧。

總結

本文講解的是程式設計師常用的I/O,一般來說作為程式設計師我們無需關心,但是理解I/O背後的底層原理對於設計高效能、高併發系統是極為有益的,希望這篇能對大家加深對I/O的認識有所幫助。