【讀書筆記】Linux內核設計與實現--頁高速緩存和頁回寫

頁高速緩存(cache)是Linux內核實現磁盤緩存。主要用來減少對磁盤的 I/O 操作。具體的講是通過把磁盤中的數據緩存到物理內存中,把對磁盤的訪問變爲對物理內存的訪問

磁盤高速緩存之所以在任何現代操作系統中尤爲重要有如下原因:

  1. 訪問磁盤的速度要遠遠低於訪問內存的速度–ms和ns的差距;
  2. 數據一旦被訪問,就很有可能在短期內再次被訪問到(臨時局部原理(temporal locality))。

1.緩存手段

頁高速緩存是由內存中的物理頁組成的,其內容對應磁盤上的物理塊

Q:何爲緩存命中?
A:當內核開始一個讀操作(read系統調用),它首先會檢查需要的數據是否在頁高速緩存中,如果在,則放棄訪問磁盤,而直接從內存中讀取–該行爲稱爲緩存命中(即直接從內存中讀取數據)。

ps:系統不一定要將整個文件都緩存,緩存可以持有某個文件的全部內容,也可以存儲另一些文件的一頁或者幾頁,到底該緩存誰取決於誰被訪問到。

1.1 寫緩存

緩存實現策略 說明
不緩存(nowrite) 高速緩存不去緩存任何寫操作,直接跳過緩存,寫磁盤,同時也使緩存中的數據失效,後續讀操作進行也需重新從磁盤中讀取數據
寫透緩存(write-through cache) 寫操作將自動更新內存緩存,同時也更新磁盤文件。寫操作會立刻穿透緩存到磁盤中。緩存數據時刻和後備存儲保持同步,不需要讓緩存失效
回寫-Linux方式 寫操作直接寫到緩存中,後端存儲不會立刻直接更新,而是將頁高速緩存中被寫入的頁面標記成”髒“,並且被加入到髒頁鏈表中;然後由一個進程(回寫進程)週期性將髒頁鏈表中的頁寫回到次哦,從而讓磁盤中的數據和內存中最終一致。最後清理”髒“標識

ps:
Linux策略方式(回寫)中的"髒頁"理解成磁盤中的數據已經過時了,並非是頁高速緩存中的數據是髒的,可以理解成高速緩存中的數據和磁盤中的數據未同步。
回寫策略通常認爲要好於寫透策略,因爲通過延遲寫磁盤,方便在以後的時間內合併更多的數據和再一次刷新。

1.2 緩存回收–緩存回收策略

緩存回收策略就是決定緩存中什麼內容將被清除的策略。
Linux的緩存回收是通過選擇乾淨頁(不髒)進行簡單替換。
如果緩衝中沒有足夠的乾淨頁面,內核將強制地進行回寫操作,以騰出更多的乾淨可用頁。

ps:
最難的事情在於決定什麼頁應該回收。

理想的回收策略應該是回收那些以後最不可能使用的頁面,當然要知道以後的事情你必須是先知,因此,這樣理想的回收策略被稱爲預測算法。(太理想,無法真正實現)

1.最近最少使用–LRU算法
緩存回收策略通過所訪問的數據特性,盡力追求預測效率
最成功的的算法稱爲最近最少使用算法–LRU算法
LRU回收策略需要跟蹤每個頁面的訪問蹤跡(或者至少按照訪問時間爲序的頁鏈表),以便能回收最老時間戳的頁面(或者回收排序鏈表頭所指的頁面)。
該策略的良好效果來源於緩存的數據越久未被訪問,則越不大可能近期再被訪問,而最近被訪問的最優可能再次訪問

ps:但是,LRU策略並非是放之四海而皆準的法則,對於許多文件被訪問一次,再不被訪問的情景,LRU尤其失敗。將這些頁面放在LRU鏈的頂端顯然不是最優,當然,內核並沒有辦法知道一個文件只會被訪問一次,但是能夠知道過去訪問了多少次。

2.雙鏈策略–修改過的LRU,LUR/2->LUR/n
Linux實現的是一個修改過的LRU,維護兩個鏈表:活躍鏈表非活躍鏈表
處於活躍鏈表上的頁面被認爲是“熱”的且不會被換出,而在非活躍鏈表上的頁面則是可以被換出的。
兩個鏈表都被僞LRU規則維護:頁面從尾部加入,從頭部移除,如同隊列。
雙鏈表策略解決了傳統LRU算法中對僅一次訪問的窘境。

PS:現在可知道頁緩存如何構建(通過讀和寫),如何在寫時被同步(通過回寫)以及舊數據如何被回收來容納新數據(通過雙鏈表)。

2.Linux頁高速緩存

頁高速緩存緩存的是內存頁面
緩存中的頁來自對正規文件、塊設備文件和內存映射文件的讀寫。如此一來,頁高速緩存就包含了最近被訪問過的文件的數據塊。

在執行一個 I/O 操作前(如read),內核會檢查數據是否已經在頁高速緩存中了,如果所需要的數據確實在高速緩存中,那麼內核可以從內存中迅速的返回需要的頁,而不再需要從相對較慢的磁盤上讀取數據。

2.1 address_space對象

在頁高速緩存中的頁可能包含了多個不連續的物理磁盤塊。
Linux頁高速緩存的目標緩存任何基於頁的對象,包含各種類型的文件和各種類型的內存映射。

Linux頁高速緩存使用了一個新對象(address_space結構體)管理緩存項和頁 I/O 操作。
address_space結構體是虛擬地址 vm_area_struct 的物理地址對等體。即:
當一個文件可以被10個vm_area_struct結構體標識(如有5個進程,每個調用mmap()映射它兩次),那麼這個文件只能有一個address_space數據結構–也就是文件可以有多個虛擬地址,但只能在物理內存有一份。
address_space結構定義在文件<linux/fs.h>中,形式如下:

struct address_space {
	struct inode		*host;		/* owner: inode, block_device 擁有節點 */
	struct radix_tree_root	page_tree;	/* radix tree of all pages 包含全部頁面的radix樹 */
	spinlock_t		tree_lock;	/* and lock protecting it 保護page_tree的自旋鎖 */
	unsigned int		i_mmap_writable;/* count VM_SHARED mappings VM_SHARED 計數 */
	struct rb_root		i_mmap;		/* tree of private and shared mappings 私有映射鏈表 */
	struct list_head	i_mmap_nonlinear;/*list VM_NONLINEAR mappings VM_NONLINEAR 鏈表 */
	struct mutex		i_mmap_mutex;	/* protect tree, count, list */
	/* Protected by tree_lock together with the radix tree */
	unsigned long		nrpages;	/* number of total pages 頁總數 */
	pgoff_t			writeback_index;/* writeback starts here 回寫的起始偏移 */
	const struct address_space_operations *a_ops;	/* methods 操作表 */
	unsigned long		flags;		/* error bits/gfp mask gfp_mask 掩碼與錯誤標識 */
	struct backing_dev_info *backing_dev_info; /* device readahead, etc 預讀信息 */
	spinlock_t		private_lock;	/* for use by the address_space 私有address_space鎖 */
	struct list_head	private_list;	/* ditto 私有address_space 鏈表 */
	void			*private_data;	/* ditto */
} __attribute__((aligned(sizeof(long))));

i_mmap 字段是一個優先搜索樹(一種巧妙地將堆與radix樹結合的快速檢索樹)。它的搜索範圍包含了在address_space中所有共享的與私有的映射頁面。
i_mmap字段可幫助內核高效地找到關聯的被緩存文件–因爲一個被緩存的文件只和一個address_space結構體相關聯,但它可以有多個vm_area_struct結構體,即一物理頁到虛擬頁是個一對多的映射;
nrpages 字段描述 address_space 頁總數;
host 域:在address_space結構往往會和某些內核對象關聯。通常與一個索引節點(inode)關聯,host域就會指向該索引節點;如果關聯對象不是一個inode,則host域會被設置爲NULL。

2.2 address_space操作–address_space_operations操作表

a_ops 域指向地址空間對象中的操作函數表。定義在文件<linux/fs.h>中,由address_space_operations結構體來表示:

struct address_space_operations {
	int (*writepage)(struct page *page, struct writeback_control *wbc);
	int (*readpage)(struct file *, struct page *);

	/* Write back some dirty pages from this mapping. */
	int (*writepages)(struct address_space *, struct writeback_control *);

	/* Set a page dirty.  Return true if this dirtied it */
	int (*set_page_dirty)(struct page *page);

	int (*readpages)(struct file *filp, struct address_space *mapping,
			struct list_head *pages, unsigned nr_pages);

	int (*write_begin)(struct file *, struct address_space *mapping,
				loff_t pos, unsigned len, unsigned flags,
				struct page **pagep, void **fsdata);
	int (*write_end)(struct file *, struct address_space *mapping,
				loff_t pos, unsigned len, unsigned copied,
				struct page *page, void *fsdata);

	/* Unfortunately this kludge is needed for FIBMAP. Don't use it */
	sector_t (*bmap)(struct address_space *, sector_t);
	void (*invalidatepage) (struct page *, unsigned long);
	int (*releasepage) (struct page *, gfp_t);
	void (*freepage)(struct page *);
	ssize_t (*direct_IO)(int, struct kiocb *, const struct iovec *iov,
			loff_t offset, unsigned long nr_segs);
	int (*get_xip_mem)(struct address_space *, pgoff_t, int,
						void **, unsigned long *);
	/*
	 * migrate the contents of a page to the specified target. If sync
	 * is false, it must not block.
	 */
	int (*migratepage) (struct address_space *,
			struct page *, struct page *, enum migrate_mode);
	int (*launder_page) (struct page *);
	int (*is_partially_uptodate) (struct page *, read_descriptor_t *,
					unsigned long);
	void (*is_dirty_writeback) (struct page *, bool *, bool *);
	int (*error_remove_page)(struct address_space *, struct page *);

	/* swapfile support */
	int (*swap_activate)(struct swap_info_struct *sis, struct file *file,
				sector_t *span);
	void (*swap_deactivate)(struct file *file);
};

這些方法指針指向那些指定緩存對象實現的頁 I/O 操作。
操作表readpage()–讀頁到緩存和writepage()–更新緩存數據兩個方法最爲重要。

Q:一個頁面的讀操作所包含的步驟?
A:

  1. linux 內核試圖在頁高速緩存中找到需要的數據:find_get_page()方法負責完成這個檢查動作。
  2. 如果搜索的頁並沒在高速緩存中,find_get_page()將會返回一個NULL,並且內核將分配一個新頁面,然後將之前搜索的頁加入到頁高速緩存中;
  3. 需要的數據從磁盤被讀入,再被加入頁高速緩存,然後返回給用戶。

Q:通常寫操作路徑要包含的步驟?
A:
寫操作和讀操作有少許不同,對於文件映射來說,當頁被修改了,VM僅僅需要調用:

SetPageDirty(page);

內核會在晚些時候通過writepage()方法把頁寫出。對特定文件的些操作比較複雜,它的代碼在文件mm/filemap.c中,通常寫操作路徑要包含以下步驟:

page = __grab_cache_page(mapping,index,&cached_page,&lru_pvec);
status = a_ops->prepare_write(file,page,offset,offset+bytes);
page_fault = filemap_copy_from_user(page,offset,buf,bytes);
status = a_ops->commit_write(file,page,offset,offset+bytes);

首先,在頁高速緩存中搜索需要的頁。如果需要的頁不在高速緩存中,那麼內核在高速緩存中新分配一空閒項;下一步,內核創建一個寫請求;接着數據被從用戶空間拷貝到了內核緩衝;最後將數據寫入磁盤。

ps:
因爲所有的頁 I/O 操作都要執行以上步驟,保證了所有的頁 I/O 操作必然通過頁高速緩存進行的。
因此內核總是試圖先通過頁高速緩存來滿足所有的讀請求。如果在頁高速緩存中未搜索到需要的頁,則內核將從磁盤讀入需要的頁,然後將該頁加入到頁高速緩存中;
對於寫操作,頁高速緩存更像是一個存儲平臺,所有要被寫出的頁都要加入頁高速緩存中。

2.3 基樹–保證了頻繁的檢查會迅速和高效

任何頁 I/O 操作前內核都要檢查頁是否已經在頁高速緩衝中情況下,基樹的使用保證了這種頻繁進行的檢查迅速、高效。否則搜索和檢查頁高速緩存的開銷可能抵消頁高速緩存帶來的好處。

基樹保存在page_tree結構體中,它是一個二叉樹,只要指定了文件偏移量就可以在基樹中迅速檢索到希望的頁。

基樹核心代碼的通用形式可以在文件lib/radix-tree.c中找到,使用基樹,需要包含頭文件<linux/radix_tree.h>。

2.4 以前的頁散列表–新版本引入了基於基樹的頁高速緩存來避免相應弊端

被丟棄的頁散列表,需要了解的可參考本書16.2.4章節。

3.緩衝區高速緩存

獨立的磁盤塊通過塊 I/O 緩衝也要被存入頁高速緩存。
一個緩衝是一個物理磁盤塊在內存裏的表示。
緩衝的作用就是映射內存中頁面到磁盤塊,這樣一來頁高速緩存在塊 I/O 操作時也減少了磁盤訪問,因爲它緩存磁盤塊和減少塊 I/O 操作。 該緩存通常被稱爲緩衝區高速緩存。(雖然實現上它沒有作爲獨立緩存,而是作爲頁高速緩存的一部分)

4.flusher線程

由於頁高速緩存的緩存作用,寫操作實際上會被延遲。
當頁高速緩存中數據比後臺存儲的數據更新時,該數據被稱作髒數據
在內存中累積起來的髒頁最終必須被寫回磁盤。

實現將數據寫回磁盤的角色就是flusher線程。

Q:髒頁何時被寫回磁盤?
A:

  1. 當空閒內存低於一個特定的閾值時,內核必須將髒頁寫回磁盤以便釋放內存,因爲只有乾淨(不髒的)內存纔可以被回收。當內存乾淨後,內核就可以從緩存清理數據,然後收縮緩存,最終釋放出更多的內存;
  2. 當髒頁在內存中駐留時間超過一個特定的閾值時,內核必須將超時的髒頁寫回磁盤,以確保髒頁不會無限期地駐留在內存中;
  3. 當用戶進程調用sync()和fsync()系統調用時,內核會按要求執行回寫動作。

Q:數據寫回磁盤的時候,什麼時候停止?
A:

  1. 已經有指定的最小數目的頁被寫出到磁盤;
  2. 空閒內存數已經回升,超過了閾值 dirty_background_ratio。

ps:回寫操作不會再達到這兩個條件前停止,除非刷新者線程寫回了所有的髒頁,沒有剩下的髒頁可再被寫回了。

flusher線程的實現代碼在文件 mm/page-writeback.c 和 mm/backing-dev.c 中,回寫機制的實現代碼在文件 fs/fs-writeback.c中。

系統管理員可以在/proc/sys/vm 中設置回寫相關的參數,也可以通過sysctl系統調用設置。
可參考本書16.4章節或者此博客:Linux 內核參數說明

4.1 膝上型計算機模式

膝上型計算機模式是一種特殊的頁回寫策略,該策略的主要意圖是將硬盤轉動的機械行爲最小化,允許硬盤儘可能長時間的停滯,以此延長電池供電時間。

該模式可通過/proc/sys/vm/laptop_mode文件進行配置。

該策略模式除了當緩存中的頁面太舊時要執行回寫髒頁以外,flusher還會找準磁盤運轉的時機,把所有其他的物理磁盤 I/O 、刷新髒緩存等通過協會到磁盤,以保證不會專門爲了寫磁盤而去主動激活磁盤運行。

因爲關閉磁盤驅動器是節電的重要手段,膝上模式可以延長膝上計算機依靠電池的續航能力。其壞處則是系統崩潰或者其他錯誤會使數據丟失。

4.2 歷史上的bdflush、kupdated和pdflush

在2.6版本前,flusher線程的工作分別由bdflush和kupdated兩個線程共同完成。
當可用內存過低時,bdflush 內核線程在後臺執行髒頁回寫操作。
系統中只有一個bdflush後臺線程(flusher線程的數目是根據磁盤數量變化的)。
bdflush線程基於緩衝,它將髒緩衝寫回磁盤(flusher線程基於頁面,它將整個髒頁寫回磁盤)。

ps:頁面可能包含緩衝,但實際 I/O 操作對象是整頁,而不是塊(緩衝卻是塊)。

在2.6內核中,bdflush 和 kupdated 讓路給 pdflush(page dirty flush)。
pdflush 線程的執行和如今的flusher線程類似。
區別在於pdflush線程數目是動態的,默認是2個到8個,具體多少取決於系統 I/O 的負載。

在線程2.6.32 內核系列中flusher線程取代了pdflush線程,因爲flusher線程針對每個磁盤獨立執行回寫操作。

4.3 避免擁塞的方法:使用多線程

使用bdflush線程最主要的一個缺點就是:bdflush僅僅包含一個線程,因此很有可能在頁回寫任務很重時,造成擁塞。

爲了避免出現這種情況,內核需要多個回寫線程併發執行,這樣單個設備隊列的擁塞就不會成爲系統瓶頸了。

pdflush線程策略中,線程數是動態變化的。每一個線程試圖儘可能地從每個超級塊的髒頁鏈表中回收數據,並回寫到磁盤,但是如果每個pdflush線程在同一個擁塞的隊列上掛起,這樣多個pdflush線程可能並不比一個線程更好,因此,pdflush線程採取了擁塞回避策略:它們會主動嘗試從那些沒有擁塞的隊列回寫頁。從而,pdflsuh線程將其工作調度開來,防止僅僅欺負某一個忙碌設備。

當前flusher線程模型(自2.3.32內核系列以後採用)和具體塊設備關聯,所以每個給定線程從每個給定設備的髒頁鏈表收集數據,並寫回到對應磁盤,這樣回寫更趨於同步了,而且由於每個磁盤對應一個線程,所以線程也不需要採用複雜的擁塞避免策略,因爲一個磁盤就一個線程操作

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