《Linux內(nèi)核設(shè)計與實現(xiàn)》讀書筆記(十六)- 頁高速緩存和頁回寫
主要內(nèi)容:
- 緩存簡介
- 頁高速緩存
- 頁回寫
1. 緩存簡介
在編程中,緩存是很常見也很有效的一種提高程序性能的機制。
linux內(nèi)核也不例外,為了提高I/O性能,也引入了緩存機制,即將一部分磁盤上的數(shù)據(jù)緩存到內(nèi)存中。
1.1 原理
之所以通過緩存能提高I/O性能是基于以下2個重要的原理:
- CPU訪問內(nèi)存的速度遠遠大于訪問磁盤的速度(訪問速度差距不是一般的大,差好幾個數(shù)量級)
- 數(shù)據(jù)一旦被訪問,就有可能在短期內(nèi)再次被訪問(臨時局部原理)
1.2 策略
緩存的創(chuàng)建和讀取沒什么好說的,無非就是檢查緩存是否存在要創(chuàng)建或者要讀取的內(nèi)容。
但是寫緩存和緩存回收就需要好好考慮了,這里面涉及到「緩存內(nèi)容」和「磁盤內(nèi)容」同步的問題。
1.2.1 「寫緩存」常見的有3種策略
- 不緩存(nowrite) :: 也就是不緩存寫操作,當對緩存中的數(shù)據(jù)進行寫操作時,直接寫入磁盤,同時使此數(shù)據(jù)的緩存失效
- 寫透緩存(write-through) :: 寫數(shù)據(jù)時同時更新磁盤和緩存
- 回寫(copy-write or write-behind) :: 寫數(shù)據(jù)時直接寫到緩存,由另外的進程(回寫進程)在合適的時候?qū)?shù)據(jù)同步到磁盤
3種策略的優(yōu)缺點如下:
|
策略 |
復(fù)雜度 |
性能 |
| 不緩存 | 簡單 | 緩存只用于讀,對于寫操作較多的I/O,性能反而會下降 |
| 寫透緩存 | 簡單 | 提升了讀性能,寫性能反而有些下降(除了寫磁盤,還要寫緩存) |
| 回寫 | 復(fù)雜 | 讀寫的性能都有提高(目前內(nèi)核中采用的方法) |
1.2.2 「緩存回收」的策略
- 最近最少使用(LRU) :: 每個緩存數(shù)據(jù)都有個時間戳,保存最近被訪問的時間。回收緩存時首先回收時間戳較舊的數(shù)據(jù)。
- 雙鏈策略(LRU/2) :: 基于LRU的改善策略。具體參見下面的補充說明
補充說明(雙鏈策略):
雙鏈策略其實就是 LRU(Least Recently Used) 算法的改進版。
它通過2個鏈表(活躍鏈表和非活躍鏈表)來模擬LRU的過程,目的是為了提高頁面回收的性能。
頁面回收動作發(fā)生時,從非活躍鏈表的尾部開始回收頁面。
雙鏈策略的關(guān)鍵就是頁面如何在2個鏈表之間移動的。
雙鏈策略中,每個頁面都有2個標志位,分別為
PG_active - 標志頁面是否活躍,也就是表示此頁面是否要移動到活躍鏈表
PG_referenced - 表示頁面是否被進程訪問到
頁面移動的流程如下:
- 當頁面第一次被被訪問時,PG_active 置為1,加入到活動鏈表
- 當頁面再次被訪問時,PG_referenced 置為1,此時如果頁面在非活動鏈表,則將其移動到活動鏈表,并將PG_active置為1,PG_referenced 置為0
- 系統(tǒng)中 daemon 會定時掃描活動鏈表,定時將頁面的 PG_referenced 位置為0
- 系統(tǒng)中 daemon 定時檢查頁面的 PG_referenced,如果 PG_referenced=0,那么將此頁面的 PG_active 置為0,同時將頁面移動到非活動鏈表
2. 頁高速緩存
故名思義,頁高速緩存中緩存的最小單元就是內(nèi)存頁。
但是此內(nèi)存頁對應(yīng)的數(shù)據(jù)不僅僅是文件系統(tǒng)的數(shù)據(jù),可以是任何基于頁的對象,包括各種類型的文件和內(nèi)存映射。
2.1 簡介
頁高速緩存緩存的是具體的物理頁面,與前面章節(jié)中提到的虛擬內(nèi)存空間(vm_area_struct)不同,假設(shè)有進程創(chuàng)建了多個 vm_area_struct 都指向同一個文件,
那么這個 vm_area_struct 對應(yīng)的 頁高速緩存只有一份。
也就是磁盤上的文件緩存到內(nèi)存后,它的虛擬內(nèi)存地址可以有多個,但是物理內(nèi)存地址卻只能有一個。
為了有效提高I/O性能,頁高速緩存要需要滿足以下條件:
- 能夠快速檢索需要的內(nèi)存頁是否存在
- 能夠快速定位 臟頁面(也就是被寫過,但還沒有同步到磁盤上的數(shù)據(jù))
- 頁高速緩存被并發(fā)訪問時,盡量減少并發(fā)鎖帶來的性能損失
下面通過分析內(nèi)核中的相應(yīng)的結(jié)構(gòu)體,來了解內(nèi)核是如何提高 I/O性能的。
2.2 實現(xiàn)
實現(xiàn)頁高速緩存的最重要的結(jié)構(gòu)體要算是 address_space ,在 <linux/fs.h> 中
struct address_space { struct inode *host; /* 擁有此 address_space 的inode對象 */ struct radix_tree_root page_tree; /* 包含全部頁面的 radix 樹 */ spinlock_t tree_lock; /* 保護 radix 樹的自旋鎖 */ unsigned int i_mmap_writable;/* VM_SHARED 計數(shù) */ struct prio_tree_root i_mmap; /* 私有映射鏈表的樹 */ struct list_head i_mmap_nonlinear;/* VM_NONLINEAR 鏈表 */ spinlock_t i_mmap_lock; /* 保護 i_map 的自旋鎖 */ unsigned int truncate_count; /* 截斷計數(shù) */ unsigned long nrpages; /* 總頁數(shù) */ pgoff_t writeback_index;/* 回寫的起始偏移 */ const struct address_space_operations *a_ops; /* address_space 的操作表 */ unsigned long flags; /* gfp_mask 掩碼與錯誤標識 */ struct backing_dev_info *backing_dev_info; /* 預(yù)讀信息 */ spinlock_t private_lock; /* 私有 address_space 自旋鎖 */ struct list_head private_list; /* 私有 address_space 鏈表 */ struct address_space *assoc_mapping; /* 緩沖 */ struct mutex unmap_mutex; /* 保護未映射頁的 mutux 鎖 */ } __attribute__((aligned(sizeof(long))));
補充說明:
- inode - 如果 address_space 是由不帶inode的文件系統(tǒng)中的文件映射的話,此字段為 null
- page_tree - 這個樹結(jié)構(gòu)很重要,它保證了頁高速緩存中數(shù)據(jù)能被快速檢索到,臟頁面能夠快速定位。
- i_mmap - 根據(jù) vm_area_struct,能夠快速的找到關(guān)聯(lián)的緩存文件(即 address_space),前面提到過, address_space 和 vm_area_struct 是 一對多的關(guān)系。
- 其他字段主要是提供各種鎖和輔助功能
此外,對于這里出現(xiàn)的一種新的數(shù)據(jù)結(jié)構(gòu) radix 樹,進行簡要的說明。
radix樹通過long型的位操作來查詢各個節(jié)點, 存儲效率高,并且可以快速查詢。
linux中 radix樹相關(guān)的內(nèi)容參見: include/linux/radix-tree.h 和 lib/radix-tree.c
下面根據(jù)我自己的理解,簡單的說明一下radix樹結(jié)構(gòu)及原理。
2.2.1 首先是 radix樹節(jié)點的定義
/* 源碼參照 lib/radix-tree.c */ struct radix_tree_node { unsigned int height; /* radix樹的高度 */ unsigned int count; /* 當前節(jié)點的子節(jié)點數(shù)目 */ struct rcu_head rcu_head; /* RCU 回調(diào)函數(shù)鏈表 */ void *slots[RADIX_TREE_MAP_SIZE]; /* 節(jié)點中的slot數(shù)組 */ unsigned long tags[RADIX_TREE_MAX_TAGS][RADIX_TREE_TAG_LONGS]; /* slot標簽 */ };
弄清楚 radix_tree_node 中各個字段的含義,也就差不多知道 radix樹是怎么一回事了。
- height 表示的整個 radix樹的高度(即葉子節(jié)點到樹根的高度), 不是當前節(jié)點到樹根的高度
- count 這個比較好理解,表示當前節(jié)點的子節(jié)點個數(shù),葉子節(jié)點的 count=0
- rcu_head RCU發(fā)生時觸發(fā)的回調(diào)函數(shù)鏈表
- slots 每個slot對應(yīng)一個子節(jié)點(葉子節(jié)點)
- tags 標記子節(jié)點是否 dirty 或者 wirteback
2.2.2 每個葉子節(jié)點指向文件內(nèi)相應(yīng)偏移所對應(yīng)的緩存頁
比如下圖表示 0x000000 至 0x11111111 的偏移范圍,樹的高度為4 (圖是網(wǎng)上找的,不是自己畫的)
2.2.3 radix tree 的葉子節(jié)點都對應(yīng)一個二進制的整數(shù),不是字符串,所以進行比較的時候非常快
其實葉子節(jié)點的值就是地址空間的值(一般是long型)
3. 頁回寫
由于目前l(fā)inux內(nèi)核中對于「寫緩存」采用的是第3種策略,所以回寫的時機就顯得非常重要,回寫太頻繁影響性能,回寫太少容易造成數(shù)據(jù)丟失。
3.1 簡介
linux 頁高速緩存中的回寫是由內(nèi)核中的一個線程(flusher 線程)來完成的,flusher 線程在以下3種情況發(fā)生時,觸發(fā)回寫操作。
1. 當空閑內(nèi)存低于一個閥值時
空閑內(nèi)存不足時,需要釋放一部分緩存,由于只有不臟的頁面才能被釋放,所以要把臟頁面都回寫到磁盤,使其變成干凈的頁面。
2. 當臟頁在內(nèi)存中駐留時間超過一個閥值時
確保臟頁面不會無限期的駐留在內(nèi)存中,從而減少了數(shù)據(jù)丟失的風(fēng)險。
3. 當用戶進程調(diào)用 sync() 和 fsync() 系統(tǒng)調(diào)用時
給用戶提供一種強制回寫的方法,應(yīng)對回寫要求嚴格的場景。
頁回寫中涉及的一些閥值可以在 /proc/sys/vm 中找到
下表中列出的是與 pdflush(flusher 線程的一種實現(xiàn)) 相關(guān)的一些閥值
|
閥值 |
描述 |
| dirty_background_ratio | 占全部內(nèi)存的百分比,當內(nèi)存中的空閑頁達到這個比例時,pdflush線程開始回寫臟頁 |
| dirty_expire_interval | 該數(shù)值以百分之一秒為單位,它描述超時多久的數(shù)據(jù)將被周期性執(zhí)行的pdflush線程寫出 |
| dirty_ratio | 占全部內(nèi)存的百分比,當一個進程產(chǎn)生的臟頁達到這個比例時,就開始被寫出 |
| dirty_writeback_interval | 該數(shù)值以百分之一秒未單位,它描述pdflush線程的運行頻率 |
| laptop_mode | 一個布爾值,用于控制膝上型計算機模式 |
3.2 實現(xiàn)
flusher線程的實現(xiàn)方法隨著內(nèi)核的發(fā)展也在不斷的變化著。下面介紹幾種在內(nèi)核發(fā)展中出現(xiàn)的比較典型的實現(xiàn)方法。
1. 膝上型計算機模式
這種模式的意圖是將硬盤轉(zhuǎn)動的機械行為最小化,允許硬盤盡可能長時間的停滯,以此延長電池供電時間。
該模式通過 /proc/sys/vm/laptop_mode 文件來設(shè)置。(0 - 關(guān)閉該模式 1 - 開啟該模式)
2. bdflush 和 kupdated (2.6版本前 flusher 線程的實現(xiàn)方法)
bdflush 內(nèi)核線程在后臺運行,系統(tǒng)中只有一個 bdflush 線程,當內(nèi)存消耗到特定閥值以下時,bdflush 線程被喚醒
kupdated 周期性的運行,寫回臟頁。
bdflush 存在的問題:
整個系統(tǒng)僅僅只有一個 bdflush 線程,當系統(tǒng)回寫任務(wù)較重時,bdflush 線程可能會阻塞在某個磁盤的I/O上,
導(dǎo)致其他磁盤的I/O回寫操作不能及時執(zhí)行。
3. pdflush (2.6版本引入)
pdflush 線程數(shù)目是動態(tài)的,取決于系統(tǒng)的I/O負載。它是面向系統(tǒng)中所有磁盤的全局任務(wù)的。
pdflush 存在的問題:
pdflush的數(shù)目是動態(tài)的,一定程度上緩解了 bdflush 的問題。但是由于 pdflush 是面向所有磁盤的,
所以有可能出現(xiàn)多個 pdflush 線程全部阻塞在某個擁塞的磁盤上,同樣導(dǎo)致其他磁盤的I/O回寫不能及時執(zhí)行。
4. flusher線程 (2.6.32版本后引入)
flusher線程改善了上面出現(xiàn)的問題:
首先,flusher 線程的數(shù)目不是唯一的,這就避免了 bdflush 線程的問題
其次,flusher 線程不是面向所有磁盤的,而是每個 flusher 線程對應(yīng)一個磁盤,這就避免了 pdflush 線程的問題


浙公網(wǎng)安備 33010602011771號