linux頁緩存


2017-04-25

本節就聊聊頁緩存這個東西……

 


 

一、概述

頁緩存是一個相對獨立的概念,其根本目的是為了加速對后端設備的IO效率,比如文件的讀寫。頁緩存顧名思義是以頁為單位的,目前我能想到的在兩個地方頁緩存的作用比較明顯。1、在文件的讀寫中。2、在普通進程的匿名映射區操作中。在文件的讀寫中,進程對一個文件發起讀請求,如果沒喲對應的物理內存頁,則內核處理程序首先在頁緩存中查找,如果找到直接返回。如果沒找到,那么再分配空間,把內容從文件讀入內存,插入頁緩存,並返回,這里讀操作雖然是一個頁面發生的未命中,但是很大程度上會讀取多個連續頁面到內存中,只不過返回調用指定頁面,因為實際情況中,很少僅僅需要讀取一個頁面,這樣后續如果在需要訪問還是需要進行磁盤IO,磁盤尋道時間往往比讀取時間還要長,為了提高IO效率,就有了上述的預讀機制。。2、在普通進程的匿名映射中,比如進程申請的比較大的堆空間(大於128kb的都是利用MMAP映射的),這些空間的內容如果在一段時間內沒被釋放 且在一段時間內沒被訪問,則有可能被系統內存管理器交換到外存以騰出更多的物理內存空間。下次在進程訪問該內容,根據頁表查找發現物理頁面不在內存,則需要調用缺頁異常處理程序。異常處理程序判斷該頁時被換出到外存,那么首先也是在頁緩存中找,如果找到就返回;否則,從交換分區或者交換文件中把對應的文件讀入內存。這里所說的頁緩存和1中描述的頁緩存是一類緩存,但是並不是一個,1中的頁緩存是文件系統相關的,在磁盤上對應着具體的文件;在頁面回收的時候需要回寫到磁盤即同步一下;而2中的頁緩存其實是一種交換緩存,在頁面回收的時候只能是換出到交換分區或者交換文件,不存在同步的操作。

二、管理結構描述

還記得之前在分析文件系統的時候,發現inode結構中有個address_space字段,每個文件關鍵一個address_space, 該結構管理文件映射的所有物理頁面。在其中有個radix_tree_root字段,表示基數樹的根即radix tree.頁緩存正是通過這種數據結構管理的。

struct radix_tree_root {
    unsigned int        height;
    gfp_t            gfp_mask;
    struct radix_tree_node    __rcu *rnode;
};

 

結構比較簡單,height表示樹的高度;gfp_mask記錄了從哪個內存域分配內存;rnode表示樹的節點,不過樹的根和節點是不同的數據結構,節點是radix_tree_node

struct radix_tree_node {
    unsigned int    height;        /* Height from the bottom */
    unsigned int    count;
    union {
        struct radix_tree_node *parent;    /* Used when ascending tree */
        struct rcu_head    rcu_head;    /* Used when freeing node */
    };
    void __rcu    *slots[RADIX_TREE_MAP_SIZE];
    unsigned long    tags[RADIX_TREE_MAX_TAGS][RADIX_TREE_TAG_LONGS];
};

 

height表示該節點到樹根的高度,count表示節點中已經使用的數組項的數目;slots是一個指向一個數組的指針,數組中每一項都可以是葉子節點或者中間節點。葉子節點一般就是page結構,而中間節點仍然是node節點。tags是一個二維數組,表示該節點的可能關聯的頁的屬性,主要有下面幾個

#define PAGECACHE_TAG_DIRTY 0  頁當前是臟的
#define PAGECACHE_TAG_WRITEBACK 1  頁當前正在回寫
#define PAGECACHE_TAG_TOWRITE 2   

每個node關聯的slots數組有RADIX_TREE_MAP_SIZE個表項,tags二維數組是一個RADIX_TREE_MAX_TAGS行、RADIX_TREE_TAG_LONGS列的數組。

#ifdef __KERNEL__
#define RADIX_TREE_MAP_SHIFT (CONFIG_BASE_SMALL ? 4 : 6)
#else
#define RADIX_TREE_MAP_SHIFT 3 /* For more stressful testing */
#endif

#define RADIX_TREE_MAP_SIZE (1UL << RADIX_TREE_MAP_SHIFT)

#define RADIX_TREE_MAX_TAGS 3

#define RADIX_TREE_TAG_LONGS ((RADIX_TREE_MAP_SIZE + BITS_PER_LONG - 1) / BITS_PER_LONG)

根據上述信息,RADIX_TREE_MAP_SHIFT多數指的4或者6,即RADIX_TREE_MAP_SIZE基本上是16或者64。不過我們這里為了方便介紹,取RADIX_TREE_MAP_SHIFT為3,即RADIX_TREE_MAP_SIZE為8.

這里雖然也是樹狀結構,但是其相對於二叉樹或者三叉樹就隨意多了,可以認為僅僅是廣義上的樹。下面以向緩存中添加一個頁為例,介紹下整個處理流程。

/*
* Per-cpu pool of preloaded nodes
*/
struct radix_tree_preload {
int nr;
struct radix_tree_node *nodes[RADIX_TREE_PRELOAD_SIZE];
};

起始函數為static inline int add_to_page_cache(struct page *page,struct address_space *mapping, pgoff_t offset, gfp_t gfp_mask),該函數鎖定了頁面之后就調用了add_to_page_cache_locked函數。

int add_to_page_cache_locked(struct page *page, struct address_space *mapping,
        pgoff_t offset, gfp_t gfp_mask)
{
    int error;

    VM_BUG_ON(!PageLocked(page));
    VM_BUG_ON(PageSwapBacked(page));

    error = mem_cgroup_cache_charge(page, current->mm,
                    gfp_mask & GFP_RECLAIM_MASK);
    if (error)
        goto out;

    error = radix_tree_preload(gfp_mask & ~__GFP_HIGHMEM);
    if (error == 0) {
        page_cache_get(page);
        page->mapping = mapping;
        page->index = offset;

        spin_lock_irq(&mapping->tree_lock);
        error = radix_tree_insert(&mapping->page_tree, offset, page);
        if (likely(!error)) {
            mapping->nrpages++;
            __inc_zone_page_state(page, NR_FILE_PAGES);
            spin_unlock_irq(&mapping->tree_lock);
            trace_mm_filemap_add_to_page_cache(page);
        } else {
            page->mapping = NULL;
            /* Leave page->index set: truncation relies upon it */
            spin_unlock_irq(&mapping->tree_lock);
            mem_cgroup_uncharge_cache_page(page);
            page_cache_release(page);
        }
        radix_tree_preload_end();
    } else
        mem_cgroup_uncharge_cache_page(page);
out:
    return error;
}

 

該函數中首先進行的是cgroup統計,這點不是本文重點,忽略。然后調用了radix_tree_preload函數,該函數檢查CPU的radix_tree_node 緩存池是否充足,如果不充足就填充,正常情況是返回0的。進入if判斷,首先對page結構做些設置,然后獲取了mapping->tree_lock自旋鎖。保證操作的一致性。調用radix_tree_insert函數把page插入到mapping->page_tree指向的radix樹中。如果插入成功,返回0。插入成功后增加mapping page數目,釋放鎖。否則插入失敗就對page進行釋放。釋放其實就是減少引用,等下次內存管理器掃描空閑內存時對其進行回收。發現最后還調用了下page_cache_release函數,該函數僅僅實現了啟用搶占的功能,在radix_tree_preload函數中禁用了內核搶占。

現在我們自己說說radix_tree_insert函數

int radix_tree_insert(struct radix_tree_root *root,
            unsigned long index, void *item)
{
    struct radix_tree_node *node = NULL, *slot;
    unsigned int height, shift;
    int offset;
    int error;

    BUG_ON(radix_tree_is_indirect_ptr(item));

    /* Make sure the tree is high enough.  */
    if (index > radix_tree_maxindex(root->height)) {
        /*嘗試擴展樹,以滿足條件*/
        error = radix_tree_extend(root, index);
        if (error)
            return error;
    }
    /*radix_tree_node最低一位表示是間接指針還是直接指針*/
    slot = indirect_to_ptr(root->rnode);

    height = root->height;
    //RADIX_TREE_MAP_SHIFT 3
    shift = (height-1) * RADIX_TREE_MAP_SHIFT;

    offset = 0;            /* uninitialised var warning */
    while (height > 0) {
        if (slot == NULL) {//需要擴充樹
        /* Have to add a child node.  */
            if (!(slot = radix_tree_node_alloc(root)))
                return -ENOMEM;
            slot->height = height;
            slot->parent = node;
            if (node) {
                rcu_assign_pointer(node->slots[offset], slot);
                node->count++;
            } else
                rcu_assign_pointer(root->rnode, ptr_to_indirect(slot));
        }

        /* Go a level down */
        offset = (index >> shift) & RADIX_TREE_MAP_MASK;
        node = slot;
        slot = node->slots[offset];
        shift -= RADIX_TREE_MAP_SHIFT;
        height--;
    }

    if (slot != NULL)
        return -EEXIST;

    if (node) {
        node->count++;//增加計數
        rcu_assign_pointer(node->slots[offset], item);//設置指向
        BUG_ON(tag_get(node, 0, offset));
        BUG_ON(tag_get(node, 1, offset));
    } else {
        rcu_assign_pointer(root->rnode, item);
        BUG_ON(root_tag_get(root, 0));
        BUG_ON(root_tag_get(root, 1));
    }

    return 0;
}

 

函數包含三個參數,radix樹的根,插入的位置索引,插入項目的指針,以插入page來講,這里就是指向page結構的指針。radix_tree_node地址的最后一位表示該node連接的是node還是item。如果為1表明連接的還是node。潛意識里不管是node還是item,其地址都至少應該是2字節對齊的。函數首先對item進行了驗證,看是否是二字節對齊的。然后判斷index是否超過了當前樹容納的最大索引。最大值可以由樹高確定,如果index超過了最大額度,則調用radix_tree_extend函數嘗試擴展樹,擴展失敗就返回錯誤,關於這點最后會詳細介紹。通過這里的判斷,就獲取樹根指向的首個radix_tree_node。樹高height,位置偏移shift。進入while循環,條件是height大於0.循環中首先判斷slot==NULL,意味着根據height還沒遍歷到末尾,已經沒有中間節點了,所以這就需要重新分配孩子節點,直到height減小到0;相反,如果一切順利,就一層一層往下尋找,直到height=0;此時node為最終找到的radix_tree_node,offset為在node數組中的下標,接下來就可以設置指向了。下面以三層的radix tree為例,走下這個流程,如圖所示

咱們不考慮根據height定位node遇到中間節點為NULL的情況。圖中初始狀態,slot為root->rnode;height=3;shift=6;循環里面執行如下圖

出了循環,node應該就是最終定位的radix_tree_node節點,根據偏移項offset,可定位具體的項目。注意這里如圖中所說,此時slot是L1層的節點指針,如果slot不為NULL,則表示index對應的slot已經有了映射頁面,不需要再次添加物理頁面了,否則把item設置到node->slots[offset],並增加node的使用項目計數。前思后想還是沒想到node為NULL的情況,如果有知道的朋友可以說明下,謝謝!

下面看下驗證index是否在樹中的函數,系統用一個數組height_to_maxindex記錄height對應的maxindex,看下參考代碼

static __init void radix_tree_init_maxindex(void)
{
    unsigned int i;

    for (i = 0; i < ARRAY_SIZE(height_to_maxindex); i++)
        height_to_maxindex[i] = __maxindex(i);
}

static __init unsigned long __maxindex(unsigned int height)
{
    unsigned int width = height * RADIX_TREE_MAP_SHIFT;
    int shift = RADIX_TREE_INDEX_BITS - width;

    if (shift < 0)
        return ~0UL;
    if (shift >= BITS_PER_LONG)
        return 0UL;
    return ~0UL >> shift;
}
#define RADIX_TREE_INDEX_BITS  (8 /* CHAR_BIT */ * sizeof(unsigned long))

 

后者是初始化該數組的代碼,關鍵在於前兩行,還是按照前面的約定,RADIX_TREE_MAP_SHIFT看做3,后者根體系結構相關,31位下unsigned long是4個字節,RADIX_TREE_INDEX_BITS  為32。最后通過把全1右移得到結果,很是巧妙。還是按照三層來算

 

 

 

 

下面看擴展radix樹的函數radix_tree_extend

static int radix_tree_extend(struct radix_tree_root *root, unsigned long index)
{
    struct radix_tree_node *node;
    struct radix_tree_node *slot;
    unsigned int height;
    int tag;

    /* Figure out what the height should be.  */
    height = root->height + 1;
    while (index > radix_tree_maxindex(height))
        height++;

    if (root->rnode == NULL) {
        root->height = height;
        goto out;
    }

    do {
        unsigned int newheight;
        if (!(node = radix_tree_node_alloc(root)))
            return -ENOMEM;

        /* Propagate the aggregated tag info into the new root */
        for (tag = 0; tag < RADIX_TREE_MAX_TAGS; tag++) {
            if (root_tag_get(root, tag))
                tag_set(node, tag, 0);
        }

        /* Increase the height.  */
        newheight = root->height+1;
        node->height = newheight;//從底部開始計算的高度
        node->count = 1;
        node->parent = NULL;
        slot = root->rnode;
        if (newheight > 1) {
            slot = indirect_to_ptr(slot);
            slot->parent = node;
        }
        node->slots[0] = slot;
        node = ptr_to_indirect(node);
        rcu_assign_pointer(root->rnode, node);
        root->height = newheight;
    } while (height > root->height);
out:
    return 0;
}

 該函數實現邏輯挺簡單,首先嘗試height+1,如果不能還不能滿足條件,持續對height++,直到滿足條件。接下來就是一個循環,以height>root->height為條件。所以這里實際上是填充指定height到root->height路徑上空缺的節點。具體的填充過程沒什么好講的,只是注意這里的插入實際上類似於鏈表的頭插法,即每次都從根節點插入。且節點的height是從下往上計算的。還有一點就是填充雖然達到了指定的高度,但是並不是填充了所有路徑的節點,即並沒有形成滿樹,僅僅是單條路徑。因此在根據index插入item的時候,很有可能遇見路徑上的節點為NULL,那么就需要重新填充、。

radix樹的組織方式基本就是這樣,其他的操作很類似就不在重復介紹。

 

 

 參考:

linux 3.10.1源碼

http://blog.csdn.net/joker0910/article/details/8250085

    

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM