1. 程式人生 > >計算機底層知識拾遺(六)理解頁快取page cache和地址空間address_space

計算機底層知識拾遺(六)理解頁快取page cache和地址空間address_space

在這篇計算機底層知識拾遺(五)理解塊IO層 中講了塊快取buffer cache塊快取,這篇說說頁快取page cache以及相關的地址空間address_space的要點。

在Linux 2.4核心中塊快取buffer cache和頁快取page cache是並存的,表現的現象是同一份檔案的資料,可能即出現在buffer cache中,又出現在頁快取中,這樣就造成了實體記憶體的浪費。Linux 2.6核心對兩個cache進行了合併,統一使用頁快取在做快取,只有極少數的情況下才使用到buffer cache。後面會說buffer cache和page cache的區別。先直觀看看兩者的容量是如何統計的。

在 /proc/meminfo中儲存了當前系統的記憶體使用情況,比如下面這個例子,

Buffers表示buffer cache的容量

Cached表示位於實體記憶體中的頁快取page cache

SwapCached表示位於磁碟交換區的頁快取page cache

所以實際頁快取page cache的容量 =  Cached + SwapCached

buffer cache和page cache的區別

buffer cache是Unix和早期的Linux核心中主要的快取元件。我們要理解的是不管是buffer cache還是page cache都是為了處理塊裝置和記憶體互動時高速訪問的問題

1. buffer cache是面向底層塊裝置的,所以它的粒度是檔案系統的塊,塊裝置和系統採用塊進行互動。塊再轉換成磁碟的基本物理結構扇區。扇區的大小是512KB,而檔案系統的塊一般是2KB, 4KB, 8KB。扇區和塊之間是可以快速轉換的

隨著核心的功能越來越完善,塊粒度的快取已經不能滿足效能的需要。核心的記憶體管理元件採用了比檔案系統的塊更高級別的抽象,頁page,頁的大小一般從4KB到2MB,粒度更大,處理的效能更高。所以快取元件為了和記憶體管理元件更好地互動,建立了頁快取page cache來代替原來的buffer cache。

頁快取是面向檔案,面向記憶體的。通過一系列的資料結構,比如inode, address_space, page,將一個檔案對映到頁的級別,通過page + offset就可以定位到一個檔案的具體位置

2. buffer cache實際操作時按塊為基本單位,page cache操作時按頁為基本單位,新建了一個BIO的抽象,可以同時處理多個非連續的頁的IO操作,也就是所謂的scatter/gather IO

3. buffer cache目前主要用在需要按塊傳輸的場景下,比如超級塊的讀寫等。而page cache可以用在所有以檔案為單元的場景下,比如網路檔案系統等等,快取元件抽象了地址空間address_space這個概念來作為檔案系統和頁快取的中間介面卡,遮蔽了底層裝置的細節

4. buffer cache可以和page cache整合在一起,屬於一個page的塊快取使用buffer_head連結串列的方式組織,page_cache維護了一個private指標指向這個buffer_head連結串列,buffer_head連結串列維護了一個指標指向這個頁page。這樣只需要在頁快取中儲存一份資料即可

5. 檔案系統的inode實際維護了這個檔案所有的塊block的塊號,通過對檔案偏移量offset取模可以很快定位到這個偏移量所在的檔案系統的塊號,磁碟的扇區號。同樣,通過對檔案偏移量offset進行取模可以計算出偏移量所在的頁的偏移量,地址空間address_space通過指標可以方便的獲取兩端inode和page的資訊,所以可以很方便地定位到一個檔案的offset在各個元件中的位置:

檔案位元組偏移量 --> 頁偏移量 --> 檔案系統塊號 block  -->  磁碟扇區

頁快取page cache和地址空間address_space

上面比較page cache和buffer cache的時候基本把page cache的特點說了,它是面向記憶體,面向檔案的。這正好說明了頁快取的作用,它位於記憶體和檔案之間,檔案IO操作實際上只和頁快取互動,不直接和記憶體互動

Linux核心使用page資料結構來描述實體記憶體頁幀,核心建立了mem_map陣列來表示所有的物理頁幀,mem_map的陣列項就是page。

page結構不僅表示了實體記憶體頁幀,

1. 一些標誌位flags來表示該頁是否是髒頁,是否正在被寫回等等

2. _count, _mapcount表示這個頁被多少個程序使用和對映

3. private指標指向了這個頁對應的buffer cache的buffer_head連結串列,建立了頁快取和塊快取的聯絡

4. mapping指向了地址空間address_space,表示這個頁是一個頁快取中頁,和一個檔案的地址空間對應

5. index是這個頁在檔案中的頁偏移量,通過檔案的位元組偏移量可以計算出檔案的頁偏移量


頁快取實際上就是採用了一個基數樹結構將一個檔案的內容組織起來存放在實體記憶體page中。檔案IO操作直接和頁快取互動。採用快取原理來管理塊裝置的IO操作


一個檔案inode對應一個地址空間address_space。而一個address_space對應一個頁快取基數樹。這幾個元件的關係如下


再看一下地址空間address_space的概念。address_space是Linux核心中的一個關鍵抽象,它是頁快取和外部裝置中檔案系統的橋樑,可以說關聯了記憶體系統和檔案系統,檔案系統可以理解成資料來源。

1. inode指向這個地址空間的宿主,也就是資料來源

2. page_tree指向了這個地址空間對應的頁快取的基數樹。這樣就可以通過inode --> address_space -->  page_tree找打一個檔案對應的頁快取頁



讀檔案時,首先通過要讀取的檔案內容的偏移量offset計算出要讀取的頁,然後通過該檔案的inode找到這個檔案對應的地址空間address_space,然後在address_space中訪問該檔案的頁快取,如果頁快取命中,那麼直接返回檔案內容,如果頁快取缺失,那麼產生一個頁缺失異常,創業一個頁快取頁,然後從磁碟中讀取相應檔案的頁填充該快取頁,租後從頁缺失異常中恢復,繼續往下讀。

寫檔案時,首先通過所寫內容在檔案中的偏移量計算出相應的頁,然後還是通過inode找到address_space,通過address_space找到頁快取中頁,如果頁快取命中,直接把檔案內容修改更新在頁快取的頁中。寫檔案就結束了。這時候檔案修改位於頁快取,並沒有寫回writeback到磁碟檔案中去。

一個頁快取中的頁如果被修改,那麼會被標記成髒頁。髒頁需要寫回到磁碟中的檔案塊。有兩種方式可以把髒頁寫回磁碟,也就是flush。

1. 手動呼叫sync()或者fsync()系統呼叫把髒頁寫回

2. pdflush程序會定時把髒頁寫回到磁碟

髒頁不能被置換出記憶體,如果髒頁正在被寫回,那麼會被設定寫回標記,這時候該頁就被上鎖,其他寫請求被阻塞直到鎖釋放

在某些情況下我們可能需要繞過頁快取機制,比如系統存在大日誌的情況,比如資料庫系統,日誌不會被經常重複讀取,如果都快取在記憶體中會影響系統的效能。核心提供了直接IO的方式,O_DIRECT,可以繞過頁快取,直接把檔案內容從堆中寫到磁碟檔案。


關於檔案IO我們常說兩句話“普通檔案IO需要複製兩次,記憶體對映檔案mmap複製一次”,"普通檔案IO是堆內操作,記憶體對映檔案是堆外操作"。我們來看一下這兩句話。

對於普通檔案需要複製兩次,我們要理解到底是哪兩次,大部分的書都沒說清楚,只說是第一次複製是從磁碟到記憶體緩衝區,第二次是從記憶體緩衝區到程序的堆。這裡的記憶體緩衝區實際上就是頁快取。

加入一個程序render要讀取一個scene.dat檔案,實際發生的步驟如下

1. render程序向核心發起讀scene.dat檔案的請求

2. 核心根據scene.dat的inode找到對應的address_space,在address_space中查詢頁快取,如果沒有找到,那麼分配一個記憶體頁page加入到頁快取

3. 從磁碟中讀取scene.dat檔案相應的頁填充頁快取中的頁,也就是第一次複製

4. 從頁快取的頁複製內容到render程序的堆空間的記憶體中,也就是第二次複製

最後實體記憶體的內容是這樣的,同一個檔案scene.dat的內容存在了兩份拷貝,一份是頁快取,一份是使用者程序的堆空間對應的實體記憶體空間


再來看看記憶體對映檔案mmap只複製一次是如何做的,mmap只有一次頁快取的複製,從磁碟檔案複製到也快取中。

mmap會建立一個虛擬記憶體區域vm_area_struct,程序的task_struct維護著這個程序所有的虛擬記憶體區域資訊,虛擬記憶體區域會更新相應的程序頁表項,讓這些頁表項直接指向頁快取所在的物理頁page。mmap新建的這個虛擬記憶體區域和程序堆的虛擬記憶體區域不是同一個,所以mmap是在堆外空間。


最後明確幾個概念

1. 使用者程序訪問記憶體只能通過頁表結構,核心可以通過虛擬地址直接訪問實體記憶體。

2. 使用者程序不能訪問核心的地址空間,這裡的地址空間指的是虛擬地址空間,這是肯定的,因為使用者程序的虛擬地址空間和核心的虛擬地址空間是不重合的,核心虛擬地址空間必須特權訪問

3. page結構表示實體記憶體頁幀,同一個實體記憶體地址可以同時被核心程序和使用者程序訪問,只要將使用者程序的頁表項也指向這個實體記憶體地址。也就是mmap的實現原理。

參考資料:

《深入Linux核心架構》