Page Cache
- 由內存中的物理page組成,其內容對應磁盤上的block。
- page cache的大小是動態變化的。
- backing store: cache緩存的存儲設備
- 一個page通常包含多個block, 而block不一定是連續的。
讀Cache
-
當內核發起一個讀請求時, 先會檢查請求的數據是否緩存到了page cache中。
- 如果有,那么直接從內存中讀取,不需要訪問磁盤, 此即 cache hit(緩存命中)
- 如果沒有, 就必須從磁盤中讀取數據, 然后內核將讀取的數據再緩存到cache中, 如此后續的讀請求就可以命中緩存了。
-
page可以值緩存一個文件的部分內容, 而不需要吧整個文件都緩存進來。
寫Cache
- 當內核發起一個寫請求時, 也是直接往cache中寫入, 后備存儲中的內容不會直接更新。
- 內核會將被寫入的page標記為dirty, 並將其加入到dirty list中。
- 內核會周期性地將dirty list中的page寫回到磁盤上, 從而使磁盤上的數據和內存中緩存的數據一致。
Cache回收
- Page cache的另一個重要工作是釋放page, 從而釋放內存空間。
- cache回收的任務是選擇合適的page釋放
- 如果page是dirty的, 需要將page寫回到磁盤中再釋放。
LRU算法
- LRU(Least Recently used): 最近最少使用算法, Redis中也有此策略, 該算法在Java中可以使用LinkedHashMap進行實現。
Two-List策略
- Two-List策略維護了兩個list: active list && inactive list
- 在active list上的page被認為是hot的,不能釋放。
- 只有inactive list上的page可以被釋放的。
- 首次緩存的數據的page會被加入到inactive list中,已經在inactive list中的page如果再次被訪問,就會移入active list中。
- 兩個鏈表都使用了偽LRU算法維護,新的page從尾部加入,移除時從頭部移除
- 如果active list中page的數量遠大於inactive list,那么active list頭部的頁面會被移入inactive list中,從而實現兩個表的平衡。
Linux中Page Cache的具體實現
-
address_space
struct address_space { struct inode *host; /* owning inode */ struct radix_tree_root page_tree; /* radix tree of all pages */ spinlock_t tree_lock; /* page_tree lock */ unsigned int i_mmap_writable; /* VM_SHARED ma count */ struct prio_tree_root i_mmap; /* list of all mappings */ struct list_head i_mmap_nonlinear; /* VM_NONLINEAR ma list */ spinlock_t i_mmap_lock; /* i_mmap lock */ atomic_t truncate_count; /* truncate re count */ unsigned long nrpages; /* total number of pages */ pgoff_t writeback_index; /* writeback start offset */ struct address_space_operations *a_ops; /* operations table */ unsigned long flags; /* gfp_mask and error flags */ struct backing_dev_info *backing_dev_info; /* read-ahead information */ spinlock_t private_lock; /* private lock */ struct list_head private_list; /* private list */ struct address_space *assoc_mapping; /* associated buffers */ };
- 其中 host域指向對應的inode對象,host有可能為NULL,這意味着這個address_space不是和一個文件關聯,而是和swap area相關,swap是Linux中將匿名內存(比如進程的堆、棧等,沒有一個文件作為back store)置換到swap area(比如swap分區)從而釋放物理內存的一種機制。page_tree保存了該page cache中所有的page,使用基數樹(radix Tree)來存儲。i_mmap是保存了所有映射到當前page cache(物理的)的虛擬內存區域(VMA)。nrpages是當前address_space中page的數量。
-
address_space操作函數
- address_space中的a_ops域指向操作函數表(struct address_space_operations),每個后備存儲都要實現這個函數表。
-
內核使用函數表中的函數管理page cache,其中最重要的兩個函數是readpage() 和writepage()
- readpage()函數
- readpage()首先會調用find_get_page(mapping, index)在page cache中尋找請求的數據
- mapping是要尋找的page cache對象,即address_space對象
- index是要讀取的數據在文件中的偏移量
- 如果請求的數據不在該page cache中,那么內核就會創建一個新的page加入page cache中,並將要請求的磁盤數據緩存到該page中,同時將page返回給調用者。
- writepage()函數
- 對於文件映射, page每次修改后都會調用SetPageDirty(page)將page標識為dirty。
- 內核首先在指定的address_space尋找目標page
- 如果沒有,就分配一個page並加入到page cache中,然后內核發起一個寫請求將數據從用戶空間拷入內核空間,最后將數據寫入磁盤中。
- readpage()函數
Buffer Cache
-
用於表示內存到磁盤映射的buffer_head結構
-
每個buffer-block映射都有一個buffer_head結構,buffer_head中的b_assoc_map指向了address_space。
-
值得注意的是在Linux2.4中,buffer cache和 page cache之間是獨立的
- 前者使用老版本的buffer_head進行存儲,這導致了一個磁盤block可能在兩個cache中同時存在,造成了內存的浪費。
- 2.6內核中將兩者合並到了一起,使buffer_head只存儲buffer-block的映射信息,不再存儲block的內容, 從而減少了內存浪費。
Flusher Threads
- Page Cache推遲了文件寫入后備存儲的時間, 但是dirty page最終還是要被寫回磁盤的。
- 內核會在以下三種情況下將dirty page 寫回磁盤:
- 用戶進程調用sync() 和 fsync()系統調用
- 空閑內存低於特定的閾值(threshold)
- Dirty數據在內存中駐留的時間超過一個特定的閾值
- 線程群的特點是讓一個線程負責一個存儲設備(比如一個磁盤驅動器),多少個存儲設備就用多少個線程, 從而避免阻塞或者競爭的情況,提高效率。
- 當空閑內存低於閾值時,內核就會調用wakeup_flusher_threads()來喚醒一個或者多個flusher線程,將數據寫回磁盤。
- 為了避免dirty數據在內存中駐留過長時間(避免在系統崩潰時丟失過多數據),內核會定期喚醒一個flusher線程,將駐留時間過長的dirty數據寫回磁盤。