寫在前面
在開始正式的討論前,我先丟擲幾個問題:
-
談到磁碟時,常說的HDD磁碟和SSD磁碟最大的區別是什麼?這些差異會影響我們的系統設計嗎?
-
單執行緒寫檔案有點慢,那多開幾個執行緒一起寫是不是可以加速呢?
-
write(2)
函式成功傳回了,資料就已經成功寫入磁碟了嗎?此時裝置斷電會有影響嗎?會丟失資料嗎? -
write(2)
呼叫是原子的嗎?多執行緒寫檔案是否要對檔案加鎖?有沒有例外,比如O_APPEND
方式? -
坊間傳聞,
mmap(2)
的方式讀檔案比傳統的方式要快,因為少一次複製。真是這樣嗎?為什麼少一次複製?
如果你覺得這些問題都很簡單,都能很明確的回答上來。那麼很遺憾這篇文章不是為你準備的,你可以關掉網頁去做其他更有意義的事情了。如果你覺得無法明確的回答這些問題,那麼就耐心地讀完這篇文章,相信不會浪費你的時間。受限於個人時間和文章篇幅,部分議題如果我不能給出更好的解釋或者已有專業和嚴謹的資料,就只會給出相關的參考文獻的連結,請讀者自行參閱。
言歸正傳,我們的討論從儲存器的層次結構開始。
儲存器的金字塔結構
受限於儲存介質的存取速率和成本,現代計算機的儲存結構呈現為金字塔型[1]。越往塔頂,存取效率越高、但成本也越高,所以容量也就越小。得益於程式訪問的區域性性原理[2],這種節省成本的做法也能取得不俗的執行效率。從儲存器的層次結構以及計算機對資料的處理方式來看,上層一般作為下層的Cache層來使用(廣義上的Cache)。比如暫存器快取CPU Cache的資料,CPU Cache L1~L3層視具體實現彼此快取或直接快取記憶體的資料,而記憶體往往快取來自本地磁碟的資料。
本文主要討論磁碟IO操作,故只聚焦於Local Disk的訪問特性和其與DRAM之間的資料互動。
無處不在的快取
如圖,當程式呼叫各類檔案操作函式後,使用者資料(User Data)到達磁碟(Disk)的流程如圖所示[3]。圖中描述了Linux下檔案操作函式的層級關係和記憶體快取層的存在位置。中間的黑色實線是使用者態和核心態的分界線。
從上往下分析這張圖,首先是C語言stdio
庫定義的相關檔案操作函式,這些都是使用者態實現的跨平臺封裝函式。stdio
中實現的檔案操作函式有自己的stdio buffer
,這是在使用者態實現的快取。此處使用快取的原因很簡單——系統呼叫總是昂貴的。如果使用者程式碼以較小的size不斷的讀或寫檔案的話,stdio
庫將多次的讀或者寫操作透過buffer進行聚合是可以提高程式執行效率的。stdio
庫同時也支援fflush(3)
函式來主動的掃清buffer,主動的呼叫底層的系統呼叫立即更新buffer裡的資料。特別地,setbuf(3)
函式可以對stdio
庫的使用者態buffer進行設定,甚至取消buffer的使用。
系統呼叫的read(2)/write(2)
和真實的磁碟讀寫之間也存在一層buffer,這裡用術語Kernel buffer cache
來指代這一層快取。在Linux下,檔案的快取習慣性的稱之為Page Cache
,而更低一級的裝置的快取稱之為Buffer Cache
. 這兩個概念很容易混淆,這裡簡單的介紹下概念上的區別:Page Cache
用於快取檔案的內容,和檔案系統比較相關。檔案的內容需要對映到實際的物理磁碟,這種對映關係由檔案系統來完成;Buffer Cache
用於快取儲存裝置塊(比如磁碟扇區)的資料,而不關心是否有檔案系統的存在(檔案系統的元資料快取在Buffer Cache
中)。
綜上,既然討論Linux下的IO操作,自然是跳過stdio
庫的使用者態這一堆東西,直接討論系統呼叫層面的概念了。對stdio
庫的IO層有興趣的同學可以自行去瞭解。從上文的描述中也介紹了檔案的核心級快取是儲存在檔案系統的Page Cache
中的。所以下篇的討論基本上是討論IO相關的系統呼叫和檔案系統Page Cache
的一些機制。
(未完待續…)