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