page cache的淘汰策略和組織形式

page cache和buffer cache的區別

free -m 看到的cached列表示當前的頁緩存(page cache)佔用量,buffers列表示當前的塊緩存(buffer cache)佔用量。用一句話來解釋:page cache用於緩存文件的頁數據,buffer cache用於緩存塊設備(如磁盤)的塊數據。頁是邏輯上的概念,因此page cache是與文件系統同級的;塊是物理上的概念,因此buffer cache是與塊設備驅動程序同級的。

page cache與buffer cache的共同目的都是加速數據I/O:寫數據時首先寫到緩存,將寫入的頁標記爲dirty,然後向外部存儲flush,也就是緩存寫機制中的write-back(另一種是write-through,Linux未採用);讀數據時首先讀取緩存,如果未命中,再去外部存儲讀取,並且將讀取來的數據也加入緩存。操作系統總是積極地將所有空閒內存都用作page cache和buffer cache,當內存不夠用時也會用LRU等算法淘汰緩存頁。

在Linux 2.4版本的內核之前,page cache與buffer cache是完全分離的。但是,塊設備大多是磁盤,磁盤上的數據又大多通過文件系統來組織,這種設計導致很多數據被緩存了兩次,浪費內存。所以在2.4版本內核之後,兩塊緩存近似融合在了一起:如果一個文件的頁加載到了page cache,那麼同時buffer cache只需要維護塊指向頁的指針就可以了。只有那些沒有文件表示的塊,或者繞過了文件系統直接操作(如dd命令)的塊,纔會真正放到buffer cache裏。因此,我們現在提起page cache,基本上都同時指page cache和buffer cache兩者,本文之後也不再區分,直接統稱爲page cache。

下圖近似地示出32-bit Linux系統中可能的一種page cache結構,其中block size大小爲1KB,page size大小爲4KB。

作者:LittleMagic

page cache的淘汰算法

1 LRU 和 two-list LRU
將磁盤的內容放到內存固然可以提高後來的訪問性能,但因爲內存是有限的,肯定會又內存不足的情況。 這時就應該將一些在將來被用到可能性小的數據趕出內存(如果這個數據已經被修改了,則要先寫回磁盤,再挪位)。那麼哪些數據是將來可能不被訪問的呢? 早起採取的算法就是LRU(Least Recently Used), 就是按照最後訪問時間進行排序,將越長時間沒有訪問的數據 就越有可能被敢出去。但LRU 的不足時, 有些情況,一些大的文件只被訪問了一次, 例如編譯了一個大的項目, 後面就不會被訪問了, 太慢佔用了大量的內存,但卻需要較長的時間才被趕出去。 爲了解決這個問題, 就引入了two-list LRU 機制。 任何文件第一次訪問的時候,都會被放到一個 inactive LRU 上, 只有第二次訪問的時候纔會被放到acitve LRU 上。 而清除內存的時候總是先在Inactive LRU 上進行的。

2 writeback 和 flusher threads
一般情況下,寫操作都是寫到內存。然後有linux 進程將髒數據寫回磁盤。 以下三種情況會觸發writeback 操作:
2.1 當free memory 下降的一定程度, 就會觸發寫回操作
2.2 當髒頁的數目增加到一定程度,那些夠老的髒頁就會被寫回
2.3 應用程序要求寫回。通常是爲了考慮到數據安全性的問題。 可以通過sync, fsync, fdatasync 等方式來實現。

2.1 的需求是受vm.dirty_background_ratio 控制,顯示的是一個百分比。 如果髒塊的數量的百分比超過這個值,就會喚醒flusher 去寫回髒數據。
2.2 的需求是受vm.dirty_writeback_centisecs 和 vm.dirty_expire_centisecs 控制,單位都是1/100 秒。 前者控制flusher 被喚醒的週期, 後者表示 喚醒後,多老的數據會被寫回。

另外 vm.dirty_ratio 總內存的百分比, 但是是隻一個進程的髒頁如果高於這個值,就會被寫回。

在調整這些參數之前,先要了解程序的寫是否是因爲2.3 的需求。 如果是這樣,修改其他參數也沒什麼用了。 例如在Mysql 中有一系列這種參數, 控制一次寫入後是否需要fsync。 如果要提高性能,先要關閉這些fsync, 然後再考慮調節這幾個vm 參數。

在調整這些參數以後,再通過sar 或者pidstat 來查看io 的狀況是否如預期。

3 flusher thread 和 bdflush, kupdated , pdfush
簡單的說 flusher 是2.6.32的內核才引用的。主要優點是 flusher 可以啓動多個線程, 不同的線程負責不同的disk spindles. 從而產生更好的性能。 這個特性經常被稱爲Per-backing-device based writeback。
其他的幾個以後應該看不到了。也就不談了。 它們都對device 不敏感的。

如果想清空page cache 的髒頁,可以:
echo 1 > /proc/sys/vm/drop_caches

更多的vm 參數含義可以訪問:
https://www.kernel.org/doc/Documentation/sysctl/vm.txt

page cache的組織形式

基於raix tree的管理形式:
https://en.wikipedia.org/wiki/File:Patricia_trie.svg
page cache的淘汰策略和組織形式「參考https://en.wikipedia.org/wiki/Radix_tree」

page cache中那麼多的page frames,怎麼管理和查找呢?這就要說到之前的文章提到的address_space結構體,一個address_space管理了一個文件在內存中緩存的所有pages。這個address_space可不是進程虛擬地址空間的address space,但是兩者之間也是由很多聯繫的。

這篇文章講到,mmap映射可以將文件的一部分區域映射到虛擬地址空間的一個VMA,如果有5個進程,每個進程mmap同一個文件兩次(文件的兩個不同部分),那麼就有10個VMAs,但address_space只有一個。

每個進程打開一個文件的時候,都會生成一個表示這個文件的struct file,但是文件的struct inode只有一個,inode纔是文件的唯一標識,指向address_space的指針就是內嵌在inode結構體中的。在page cache中,每個page都有對應的文件,這個文件就是這個page的owner,address_space將屬於同一owner的pages聯繫起來,將這些pages的操作方法與文件所屬的文件系統聯繫起來。

來看下address_space結構體具體是怎樣構成的:

struct address_space {
struct inode host; / Owner, either the inode or the block_device /
struct radix_tree_root page_tree; /
Cached pages /
spinlock_t tree_lock; /
page_tree lock /
struct prio_tree_root i_mmap; /
Tree of private and shared mappings /
struct spinlock_t i_mmap_lock; /
Protects @i_mmap /
unsigned long nrpages; /
total number of pages /
struct address_space_operations
a_ops; / operations table /
...
}
host指向address_space對應文件的inode。
address_space中的page cache之前一直是用radix tree的數據結構組織的,tree_lock是訪問這個radix tree的spinlcok(現在已換成xarray)。
i_mmap是管理address_space所屬文件的多個VMAs映射的,用priority search tree的數據結構組織,i_mmap_lock是訪問這個priority search tree的spinlcok。
nrpages是address_space中含有的page frames的總數。
a_ops是關於page cache如何與磁盤(backing store)交互的一系列operations。
從Radix Tree到XArray

Radix tree的每個節點可以存放64個slots(由RADIX_TREE_MAP_SHIFT設定,小型系統爲了節省內存可以配置爲16),每個slot的指針指向下一層節點,最後一層slot的指針指向struct page(關於struct page請參考這篇文章),因此一個高度爲2的radix tree可以容納64個pages,高度爲3則可以容納4096個pages。

如何在radix tree中找到一個指定的page呢?那就要回顧下struct page中的mapping和index域了,mapping指向page所屬文件對應的address_space,進而可以找到address_space的radix tree,index既是page在文件內的offset,也可作爲查找這個radix tree的索引,因爲radix tree就是按page的index來組織struct page的。

具體的查找方法和使用***做索引的page table(參考這篇文章)以及使用PPN做索引的sparse section查找(參考這篇文章)都是類似的。這裏是用page index中的一部分bits作爲radix tree第一層的索引,另一部分bits作爲第二層的索引,以此類推。因爲一個radix tree節點存放64個slots,因此一層索引需要6個bits,如果radix tree高度爲2,則需要12個bits。

內核中具體的查找函數是find_get_page(mapping, offset),如果在page cache中沒有找到,就會觸發page fault,調用__page_cache_alloc()在內存中分配若干物理頁面,然後將數據從磁盤對應位置copy過來,通過add_to_page_cache()-->radix_tree_insert()放入radix tree中。在將一個page添加到page cache和從page cache移除時,需要將page和對應的radix tree都上鎖。

Linux中radix tree的每個slot除了存放指針,還存放着標誌page和磁盤文件同步狀態的tag。如果page cache中一個page在內存中被修改後沒有同步到磁盤,就說這個page是dirty的,此時tag就是PAGE_CACHE_DIRTY。如果正在同步,tag就是PAGE_CACHE_WRITEBACK。只要下一層中有一個slot指向的page是dirty的,那麼上一層的這個slot的tag就是PAGE_CACHE_DIRTY的,就像一滴墨水一樣,放入清水後,清水也就不再完全清澈了。

前面介紹struct page中的flags時提到,flags可以是PG_dirty或PG_writeback,既然struct page中已經有了標識同步狀態的信息,爲什麼這裏radix tree還要再加上tag來標記呢?這是爲了管理的方便,內核可以據此快速判斷某個區域中是否有dirty page或正在write back的page,而無須掃描該區域中的所有pages。

想想進程虛擬地址空間中管理VMA的red black tree(參考這篇文章),這個叫radix tree長的跟我們平時見到的樹好像不太一樣啊,它的每一個節點更像是一個指針數組吧。所以啊,現在address_space中radix tree已經被xarray取代了(參考這篇文章)。

Reverse Mapping

如果要回收page cache中一個頁面,可不僅僅是釋放掉那麼簡單,別忘了Linux中進程和內核都是使用虛擬地址的,多少個PTE頁表項還指向這個page呢,回收之前,需要將這些PTE中P標誌位設爲0(not present),同時將page的物理頁面號PFN也全部設成0,要不然下次PTE指向的位置存放的就是無效的數據了。可是struct page中好像並沒有一個維護所有指向這個page的PTEs組成的鏈表。

前面的文章說過,struct page數量極其龐大,如果每個page都有這樣一個鏈表,那將顯著增加內存佔用,而且PTE中的內容是在不斷變化的,維護這一鏈表的開銷也是很大的。那如何找到這些PTE呢?從虛擬地址映射到物理地址是正向映射,而通過物理頁面尋找映射它的虛擬地址,則是reverse mapping(逆向映射)。page的確沒有直接指向PTE的反向指針,但是page所屬的文件是和VMA有mmap線性映射關係的啊,通過page在文件中的offset/index,就可以知道VMA中的哪個虛擬地址映射了這個page。

在代碼中的實現是這樣的:

__vma_address(struct page page, struct vm_area_struct vma)
{
pgoff_t pgoff = page_to_pgoff(page);
return vma->vm_start + ((pgoff - vma->vm_pgoff) << PAGE_SHIFT);
}
映射了某個address_space中至少一個page的所有進程的所有VMAs,就共同構成了這個address_space的priority search tree(PST)。

PST是一種糅合了radix tree和heap的數據結構,其實現較爲複雜,現在已經被基於augmented rbtree的interval tree所取代,詳情請參考這篇文章。

對比一下,一個進程所含有的所有VMAs是通過鏈表和紅黑樹組織起來的,一個文件所對應的所有VMA是通過基於紅黑樹的interval tree組織起來的。因此,一個VMA被創建之後,需要通過vma_link()插入到這3種數據結構中。

__vma_link_list(mm, vma, prev, rb_parent);
vma_link_rb(mm, vma, rb_link, rb_parent);
vma_link_file(vma);
下文將主要介紹page cache的同步問題。

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章