Linux中的Page Cache [一]

Buffer Cache

Buffer cache是指磁碟裝置上的raw data(指不以檔案的方式組織)以block為單位在記憶體中的快取,早在1975年釋出的Unix第六版就有了它的雛形,Linux最開始也只有buffer cache。事實上,page cache是1995年發行的1。3。50版本中才引入的。不同於buffer cache以磁碟的block為單位,page cache是以記憶體常用的page為單位的,位於虛擬檔案系統層(VFS)與具體的檔案系統之間。

在很長一段時間內,buffer cache和page cache在Linux中都是共存的,但是這會存在一個問題:一個磁碟block上的資料,可能既被buffer cache快取了,又因為它是基於磁碟建立的檔案的一部分,也被page cache快取了,這時一份資料在記憶體裡就有兩份複製,這顯然是對物理記憶體的一種浪費。更麻煩的是,核心還要負責保持這份資料在buffer cache和page cache中的一致性。所以,現在Linux中已經基本不再使用buffer cache了。

讀寫操作

上文提到,address_space中的a_ops定義了關於page和磁碟檔案互動的一系列操作,它是由struct address_space_operations包含的一組函式指標組成的,其中最重要的就是readpage()和writepage()。

struct

address_space_operations

{

int

*

writepage

)(

struct

page

*

page

struct

writeback_control

*

wbc

);

int

*

readpage

)(

struct

file

*

struct

page

*

);

/* Set a page dirty。 Return true if this dirtied it */

int

*

set_page_dirty

)(

struct

page

*

page

);

int

*

releasepage

struct

page

*

gfp_t

);

void

*

freepage

)(

struct

page

*

);

。。。

}

之所以使用函式指標的方式,是因為不同的檔案系統對此的實現會有所不同,比如在ext3中,page——>mapping——>a_ops——>writepage呼叫的就是ext3_writeback_writepage()。

struct

address_space_operations

ext3_writeback_aops

=

{

readpage

=

ext3_readpage

writepage

=

ext3_writeback_writepage

releasepage

=

ext3_releasepage

。。。

}

readpage()會阻塞直到核心往使用者buffer裡填充滿了請求的位元組數,如果遇到page cache miss,那要等的時間就比較長了(取決於磁碟I/O的速度)。既然訪問一次磁碟那麼不容易,那幹嘛不一次多預讀幾個page大小的內容過來呢?

是否採用預讀(

readahead

)要看對檔案的訪問是連續的還是隨機的,如果是連續訪問,自然會對效能帶來提升,如果是隨機訪問,預讀則是既浪費磁碟I/O頻寬,又浪費物理記憶體。

那核心怎麼能預知程序接下來對檔案的訪問是不是連續的呢?看起來只有程序主動告知了,可以採用的方法有madvise()和posix_fadvise(),前者主要配合基於檔案的mmap對映使用。advise如果是

NORMAL

,那核心會做適量的預讀;如果是RANDOM,那核心就不做預讀;如果是

SEQUENTIAL

,那核心會做大量的預讀。

預讀的page數被稱作預讀視窗(有點像TCP裡的滑動視窗),其大小直接影響預讀的最佳化效果。程序的advise畢竟只是建議,核心在執行過程中會動態地調節

預讀視窗

的大小,如果核心發現一個程序一直使用預讀的資料,它就會增加預讀視窗,它的目標(或者說KPI吧)就是保證在預讀視窗中儘可能高的命中率(也就是預讀的內容後續會被實際使用到)。

Linux中的Page Cache [二]

Page cache快取最近使用的磁碟資料,利用的是“時間區域性性”原理,依據是最近訪問到的資料很可能接下來再訪問到,而預讀磁碟的資料放入page cache,利用的是“空間區域性性”原理,依據是資料往往是連續訪問的。

同readpage()一樣,writepage()的時候,如果要訪問的內容不在page cache中,也要先從磁碟複製,類似於硬體cache中的

write allocate

機制。

Page cache這種核心提供的快取機制並不是強制使用的,如果程序在open()一個檔案的時候指定flags為

O_DIRECT

,那程序和這個檔案的資料互動就直接在使用者提供的buffer和磁碟之間進行,page cache就被bypass了,借用硬體cache的術語就是

uncachable,

這種檔案訪問方式被稱為direct I/O,適用於使用者使用自己裝置提供的快取機制的場景,比如某些資料庫應用。

回寫與同步

Page cache畢竟是為了提高效能佔用的物理記憶體,隨著越來越多的磁碟資料被快取到記憶體中,page cache也變得越來越大,如果一些重要的任務需要被page cache佔用的記憶體,核心將回收page cache以支援這些需求。

以elf檔案為例,一個elf映象檔案通常由text(code)和data組成,這兩部分的屬性是不同的,text是隻讀的,調入記憶體後不會被修改,page cache裡的內容和磁碟上的檔案內容始終是一致的,回收的時候只要將對應的所有PTEs的P位和PFN清0,直接丟棄就可以了, 不需要和磁碟檔案同步,這種page cache被稱為

discardable

的。

而data是可讀寫的,當data對應的page被修改後,硬體會將PTE中的Dirty位置1(參考這篇文章),Linux透過SetPageDirty(page)設定這個page對應的struct page的flags為PG_Dirty(參考這篇文章),而後將PTE中的Dirty位清0。

在之後的某個時間點,這些修改過的page裡的內容需要同步到外部的磁碟檔案,這一過程就是page writeback,和硬體cache的

writeback

原理是一樣的,區別僅在於CPU的cache是由硬體維護一致性,而page cache需要由軟體來維護一致性,這種page cache被稱為

syncable

的。

觸發條件

那什麼時候才會觸發page的writeback呢?分下面幾種情況:

從空間的層面

,當系統中“dirty”的記憶體大於某個閾值時。該閾值以在dirtyable memory中的佔比“dirty_background_ratio”(預設為

10%

),或者絕對的位元組數“dirty_background_bytes”(2。6。29核心引入)給出。如果兩者同時設定的話,那麼以“bytes”為更高優先順序。

此外,還有“dirty_ratio”(預設為

20%

)和“dirty_bytes”[注1],它們的意思是當“dirty”的記憶體達到這個數量(屋裡太髒),程序自己都看不過去了,寧願停下手頭的write操作(被阻塞,

同步

),先去把這些“dirty”的writeback了(把屋裡打掃乾淨)。

而如果“dirty”的程度介於這個值和“background”的值之間(

10% - 20%

),就交給後面要介紹的專門負責writeback的background執行緒去做就好了(專職的清潔工,

非同步

)。

從時間的層面

,即

週期性

的掃描(掃描間隔用“dirty_writeback_interval”表示,以毫秒為單位),發現存在最近一次更新時間超過某個閾值的pages(該閾值用“dirty_expire_interval”表示, 以毫秒為單位)。

要想知道page的最近更新時間,最簡單的方法當然是每個page維護一個timestamp,但這個開銷太大了,而且全部掃描一次也會非常耗時,因此具體實現中不會以page為粒度,而是按照inode中記錄的dirtying-time來算。

使用者主動發起

sync()/msync()/fsync()呼叫時。

可透過/proc/sys/vm資料夾檢視或修改以上提到的幾個引數:

Linux中的Page Cache [二]

centisecs是0。01s,因此上圖所示系統的的“dirty_writeback_interval”是

5s

,“dirty_expire_interval”是

30s

來對比下硬體cache的writeback機制。對於硬體cache,writeback會在兩種情況觸發:

記憶體有新的內容需要換入cache時,替換掉一個老的cache line。你說為什麼page cache不也這樣操作,而是要週期性的掃描呢?

替換掉一個cache line對CPU來說是很容易的,直接靠硬體電路完成,而替換page cache的操作本身也是需要消耗記憶體的(比如函式呼叫的堆疊開銷),如果這個外部backing store是個網路上的裝置,那麼還需要先建立socket之類的,才能透過網路傳輸完成writeback,那這記憶體開銷就更大了。所以啊,對於page cache,必須未雨綢繆,不能等記憶體都快耗光了才來writeback。

以x86為例,軟體呼叫WBINVD指令重新整理整個cache,呼叫CLFLUSH重新整理指定的cache line(參考這篇文章),分別類似於page cache的sync()和msync()。

執行執行緒

2.4核心

中用的是一個叫

bdflush

的執行緒來專門負責writeback操作,因為磁碟I/O操作很慢,而現代系統通常具備多個塊裝置(比如多個disk spindles),如果bdflush在其中一個塊裝置上等待I/O操作的完成,可能會需要很長的時間,此時其他塊裝置還閒著呢,這時單執行緒模式的bdflush就成為了影響效能的瓶頸。而且,bdflush是沒有周期掃描功能的,因此它需要配合kupdated執行緒一起使用。

Linux中的Page Cache [二]

於是在

2.6核心

中,bdflush和它的好搭檔kupdated一起被

pdflush

(page dirty flush)取代了。 pdflush是一組執行緒,根據塊裝置的I/O負載情況,數量從最少2個到最多8個不等。如果1秒內都沒有空閒的pdflush執行緒可用,核心將建立一個新的pdflush執行緒,反之,如果某個pdflush執行緒的空閒時間已經超過1秒,則該執行緒將被銷燬。一個塊裝置可能有多個可以傳輸資料的佇列,為了避免在佇列上的擁塞(congestion),pdflush執行緒會動態的選擇系統中相對空閒的佇列。

Linux中的Page Cache [二]

這種方法在理論上是很優秀的,然而現實的情況是外部I/O和CPU的速度差異巨大,但I/O系統的其他部分並沒有都使用擁塞控制,因此pdflush單獨使用複雜的擁塞演算法的效果並不明顯,可以說是“獨木難支”。

於是在更後來的核心實現中(

2.6.32版本

),乾脆化繁為簡,直接一個塊裝置對應一個thread,這種核心執行緒被稱為

flusher threads

,執行緒名為“writeback”,執行體為“wb_workfn”,透過workqueue機制實現排程。

Linux中的Page Cache [二]

無論是核心週期性掃描,還是使用者手動觸發,flusher threads的writeback都是間隔一段時間才進行的,如果在這段時間內系統掉電了(power failure),那還沒來得及writeback的資料修改就面臨丟失的風險,這是page cache機制存在的一個缺點。writeback越頻繁,資料因意外丟失的風險越低,但同時I/O壓力也越大。

前面介紹的O_DIRECT設定並不能解決這個問題,O_DIRECT只是繞過了page cache,但它並不等待資料真正寫到了磁碟上。open()中flags引數使用

O_SYNC

才能保證writepage()會等到資料可靠的寫入磁碟後再返回,適用於某些不容許資料丟失的關鍵應用。O_SYNC模式下可以使用或者不使用page cache,如果使用page cache,則相當於硬體cache的

write through

機制。

注1:

在面向伺服器應用的RHEL-8中,latency profile的“dirty_background_ratio”和“dirty_ratio”分別被設定為3%和10%,而throughput profile則將這個2個引數分別設定為10%和40%。

筆者對此調整的理解是:更頻繁的writeback會影響到業務的throughput,但保持記憶體中有更多的clean page,可以避免在記憶體緊張時,試圖獲取記憶體的應用陷入direct reclaim,影響latency。

參考:

Linux 髒資料回刷引數與調優

http://

jake。dothome。co。kr/zonn

ed-allocator-alloc-pages-fastpath/

原創文章,轉載請註明出處。