1. 程式人生 > >《Linux核心設計與實現》讀書筆記(十六)- 頁快取記憶體和頁回寫

《Linux核心設計與實現》讀書筆記(十六)- 頁快取記憶體和頁回寫

好久沒有更新了。。。

主要內容:

  • 快取簡介
  • 頁快取記憶體
  • 頁回寫

1. 快取簡介

在程式設計中,快取是很常見也很有效的一種提高程式效能的機制。

linux核心也不例外,為了提高I/O效能,也引入了快取機制,即將一部分磁碟上的資料快取到記憶體中。

1.1 原理

之所以通過快取能提高I/O效能是基於以下2個重要的原理:

  1. CPU訪問記憶體的速度遠遠大於訪問磁碟的速度(訪問速度差距不是一般的大,差好幾個數量級)
  2. 資料一旦被訪問,就有可能在短期內再次被訪問(臨時區域性原理)

1.2 策略

快取的建立和讀取沒什麼好說的,無非就是檢查快取是否存在要建立或者要讀取的內容。

但是寫快取和快取回收就需要好好考慮了,這裡面涉及到「快取內容」和「磁碟內容」同步的問題。

1.2.1 「寫快取」常見的有3種策略

  • 不快取(nowrite) :: 也就是不快取寫操作,當對快取中的資料進行寫操作時,直接寫入磁碟,同時使此資料的快取失效
  • 寫透快取(write-through) :: 寫資料時同時更新磁碟和快取
  • 回寫(copy-write or write-behind) :: 寫資料時直接寫到快取,由另外的程序(回寫程序)在合適的時候將資料同步到磁碟

3種策略的優缺點如下:

策略

複雜度

效能

不快取 簡單 快取只用於讀,對於寫操作較多的I/O,效能反而會下降
寫透快取 簡單 提升了讀效能,寫效能反而有些下降(除了寫磁碟,還要寫快取)
回寫 複雜 讀寫的效能都有提高(目前核心中採用的方法)

1.2.2 「快取回收」的策略

  • 最近最少使用(LRU) :: 每個快取資料都有個時間戳,儲存最近被訪問的時間。回收快取時首先回收時間戳較舊的資料。
  • 雙鏈策略(LRU/2) :: 基於LRU的改善策略。具體參見下面的補充說明

補充說明(雙鏈策略):

雙鏈策略其實就是 LRU(Least Recently Used) 演算法的改進版。

它通過2個連結串列(活躍連結串列和非活躍連結串列)來模擬LRU的過程,目的是為了提高頁面回收的效能。

頁面回收動作發生時,從非活躍連結串列的尾部開始回收頁面。

雙鏈策略的關鍵就是頁面如何在2個連結串列之間移動的。

雙鏈策略中,每個頁面都有2個標誌位,分別為

PG_active - 標誌頁面是否活躍,也就是表示此頁面是否要移動到活躍連結串列

PG_referenced - 表示頁面是否被程序訪問到

頁面移動的流程如下:

  1. 當頁面第一次被被訪問時,PG_active 置為1,加入到活動連結串列
  2. 當頁面再次被訪問時,PG_referenced 置為1,此時如果頁面在非活動連結串列,則將其移動到活動連結串列,並將PG_active置為1,PG_referenced 置為0
  3. 系統中 daemon 會定時掃描活動連結串列,定時將頁面的 PG_referenced 位置為0
  4. 系統中 daemon 定時檢查頁面的 PG_referenced,如果 PG_referenced=0,那麼將此頁面的 PG_active 置為0,同時將頁面移動到非活動連結串列

2. 頁快取記憶體

故名思義,頁快取記憶體中快取的最小單元就是記憶體頁。

但是此記憶體頁對應的資料不僅僅是檔案系統的資料,可以是任何基於頁的物件,包括各種型別的檔案和記憶體對映。

2.1 簡介

頁快取記憶體快取的是具體的物理頁面,與前面章節中提到的虛擬記憶體空間(vm_area_struct)不同,假設有程序建立了多個 vm_area_struct 都指向同一個檔案,

那麼這個 vm_area_struct 對應的 頁快取記憶體只有一份。

也就是磁碟上的檔案快取到記憶體後,它的虛擬記憶體地址可以有多個,但是實體記憶體地址卻只能有一個。

為了有效提高I/O效能,頁快取記憶體要需要滿足以下條件:

  1. 能夠快速檢索需要的記憶體頁是否存在
  2. 能夠快速定位 髒頁面(也就是被寫過,但還沒有同步到磁碟上的資料)
  3. 頁快取記憶體被併發訪問時,儘量減少併發鎖帶來的效能損失

下面通過分析核心中的相應的結構體,來了解核心是如何提高 I/O效能的。

2.2 實現

實現頁快取記憶體的最重要的結構體要算是 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 計數 */
    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;    /* 截斷計數 */
    unsigned long        nrpages;    /* 總頁數 */
    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; /* 預讀資訊 */
    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))));

補充說明:

  1. inode - 如果 address_space 是由不帶inode的檔案系統中的檔案對映的話,此欄位為 null
  2. page_tree - 這個樹結構很重要,它保證了頁快取記憶體中資料能被快速檢索到,髒頁面能夠快速定位。
  3. i_mmap - 根據 vm_area_struct,能夠快速的找到關聯的快取檔案(即 address_space),前面提到過, address_space 和 vm_area_struct 是 一對多的關係。
  4. 其他欄位主要是提供各種鎖和輔助功能

此外,對於這裡出現的一種新的資料結構 radix 樹,進行簡要的說明。

radix樹通過long型的位操作來查詢各個節點, 儲存效率高,並且可以快速查詢。

linux中 radix樹相關的內容參見: include/linux/radix-tree.hlib/radix-tree.c

下面根據我自己的理解,簡單的說明一下radix樹結構及原理。

2.2.1 首先是 radix樹節點的定義

/* 原始碼參照 lib/radix-tree.c */
struct radix_tree_node {
    unsigned int    height;        /* radix樹的高度 */
    unsigned int    count;      /* 當前節點的子節點數目 */
    struct rcu_head    rcu_head;   /* RCU 回撥函式連結串列 */
    void        *slots[RADIX_TREE_MAP_SIZE]; /* 節點中的slot陣列 */
    unsigned long    tags[RADIX_TREE_MAX_TAGS][RADIX_TREE_TAG_LONGS]; /* slot標籤 */
};

弄清楚 radix_tree_node 中各個欄位的含義,也就差不多知道 radix樹是怎麼一回事了。

  • height   表示的整個 radix樹的高度(即葉子節點到樹根的高度), 不是當前節點到樹根的高度
  • count    這個比較好理解,表示當前節點的子節點個數,葉子節點的 count=0
  • rcu_head RCU發生時觸發的回撥函式連結串列
  • slots    每個slot對應一個子節點(葉子節點)
  • tags     標記子節點是否 dirty 或者 wirteback

2.2.2 每個葉子節點指向檔案內相應偏移所對應的快取頁

比如下圖表示 0x000000 至 0x11111111 的偏移範圍,樹的高度為4 (圖是網上找的,不是自己畫的)

radix-tree

2.2.3 radix tree 的葉子節點都對應一個二進位制的整數,不是字串,所以進行比較的時候非常快

其實葉子節點的值就是地址空間的值(一般是long型)

3. 頁回寫

由於目前linux核心中對於「寫快取」採用的是第3種策略,所以回寫的時機就顯得非常重要,回寫太頻繁影響效能,回寫太少容易造成資料丟失。

3.1 簡介

linux 頁快取記憶體中的回寫是由核心中的一個執行緒(flusher 執行緒)來完成的,flusher 執行緒在以下3種情況發生時,觸發回寫操作。

1. 當空閒記憶體低於一個閥值時

    空閒記憶體不足時,需要釋放一部分快取,由於只有不髒的頁面才能被釋放,所以要把髒頁面都回寫到磁碟,使其變成乾淨的頁面。

2. 當髒頁在記憶體中駐留時間超過一個閥值時

   確保髒頁面不會無限期的駐留在記憶體中,從而減少了資料丟失的風險。

3. 當用戶程序呼叫 sync() 和 fsync() 系統呼叫時

   給使用者提供一種強制回寫的方法,應對回寫要求嚴格的場景。

頁回寫中涉及的一些閥值可以在 /proc/sys/vm 中找到

下表中列出的是與 pdflush(flusher 執行緒的一種實現) 相關的一些閥值

閥值

描述

dirty_background_ratio 佔全部記憶體的百分比,當記憶體中的空閒頁達到這個比例時,pdflush執行緒開始回寫髒頁
dirty_expire_interval 該數值以百分之一秒為單位,它描述超時多久的資料將被週期性執行的pdflush執行緒寫出
dirty_ratio 佔全部記憶體的百分比,當一個程序產生的髒頁達到這個比例時,就開始被寫出
dirty_writeback_interval 該數值以百分之一秒未單位,它描述pdflush執行緒的執行頻率
laptop_mode 一個布林值,用於控制膝上型計算機模式

3.2 實現

flusher執行緒的實現方法隨著核心的發展也在不斷的變化著。下面介紹幾種在核心發展中出現的比較典型的實現方法。

1. 膝上型計算機模式

這種模式的意圖是將硬碟轉動的機械行為最小化,允許硬碟儘可能長時間的停滯,以此延長電池供電時間。

該模式通過 /proc/sys/vm/laptop_mode 檔案來設定。(0 - 關閉該模式  1 - 開啟該模式)

2. bdflush 和 kupdated (2.6版本前 flusher 執行緒的實現方法)

bdflush 核心執行緒在後臺執行,系統中只有一個 bdflush 執行緒,當記憶體消耗到特定閥值以下時,bdflush 執行緒被喚醒

kupdated 週期性的執行,寫回髒頁。

bdflush 存在的問題:

整個系統僅僅只有一個 bdflush 執行緒,當系統回寫任務較重時,bdflush 執行緒可能會阻塞在某個磁碟的I/O上,

導致其他磁碟的I/O回寫操作不能及時執行。

3. pdflush (2.6版本引入)

pdflush 執行緒數目是動態的,取決於系統的I/O負載。它是面向系統中所有磁碟的全域性任務的。

pdflush 存在的問題:

pdflush的數目是動態的,一定程度上緩解了 bdflush 的問題。但是由於 pdflush 是面向所有磁碟的,

所以有可能出現多個 pdflush 執行緒全部阻塞在某個擁塞的磁碟上,同樣導致其他磁碟的I/O回寫不能及時執行。

4. flusher執行緒 (2.6.32版本後引入)

flusher執行緒改善了上面出現的問題:

首先,flusher 執行緒的數目不是唯一的,這就避免了 bdflush 執行緒的問題

其次,flusher 執行緒不是面向所有磁碟的,而是每個 flusher 執行緒對應一個磁碟,這就避免了 pdflush 執行緒的問題