最近在學習linux中內存管理相關的章節,其中頁緩存相關的結構體中遇到了成員數據結構radix_tree_root和radix_tree_node,由於以前沒有遇見過這兩種數據結構,因此在此處針對這兩種數據結構在linux內核的內存管理中的應用做一個簡要的記錄。
一、概述
linux radix樹最廣泛的用途是用於linux的 內存管理,結構address_space通過radix樹跟蹤綁定到其上的所有映射到內存中的頁。address_space結構中的radix樹允許內存管理代碼快速查找標識爲dirty和writeback的頁,而避免遍歷整顆linux中的radix樹。linux的基數樹結構是將指針與long類型的整數鍵值相映射的機制,可以提到查找的效率,是典型的以空間換取時間的做法。大致結構如圖1。
圖 1 基數樹結構
上圖顯示了一個3級節點的radix樹,每個數據條目可用3個6位的鍵值進行索引,鍵值從左到右分別代表第1-3層的節點位置。沒有孩子的節點在圖中不出現。因此,radix樹爲稀疏樹提供了有效的存儲,代替固定尺寸數組提供了鍵值到指針的快速查找。
以index=0x5BFB68爲例,化爲二進制,每6位爲一組:10110(22,第一層編號),111111(63,第2層編號),101101(45,第三層編號),101000(40,第四層編號)。
二、基本數據結構
struct radix_tree_root {
unsigned int height;
gfp_t gfp_mask;
struct radix_tree_node *rnode; /*間接指針,指向節點而非數據條目,通過設置root->rnode的低位表示是否是間接指針*/
};
struct radix_tree_node {
unsigned int height; /*從葉子節點向上計算的樹高度*/
unsigned int count; /*非葉子節點包含一個count域,表示出現在該節點的孩子節點的數量*/
struct rcu_head rcu_head;
void* slot[RADIX_TREE_MAP_SIZE]; //64個指針,指示該幾點的子節點最多有64個,該值是可以進行設置的,參考下面的全局變量的設置*/
unsigned long tags[RADIX_TREE_MAX_TAGS][RADIX_TREE_TAG_LONGS];
};
下面對上面兩個結構中出現的各個成員變量進行說明。
radix_tree_root.height: 該radix樹中從bottom開始的樹的高度;
radix_tree_root.gfp_mask: 用於插入節點分配內存時使用的標識符(傳遞個調用kmalloc的函數分配內存);
radix_tree_root.rnode: 用戶指向葉子節點而不是具體的數據條目;
radix_tree_node.height: 該節點從bottom開始的高度;
radix_tree_node.count: 非葉子節點中表示該節點具有的節點的數目;
radix_tree_node.slot[n]: 爲指向某一個具體的結構或者數據的指針,之所以定義成void*類型,是爲了適應不同類型的指針,比如該數組中的指針可以指向具體的struct page結構,也可以指向子節點;
radix_tree_node.tags: 標識該節點的每個子節點中的標誌位,是通過位圖的方式進行表示的。該域是一個2X2的數組,其中每個成員都是32位。在該節點結構中每個slot都用2位標識,用於記錄該節點下面的子節點的響應標識位有沒有被置位。行數對應於有多少個標識,比如,如果有兩個標識,PAGE_DIRTY和PAGE_WRITEBACK,那麼就需要使用兩行;如果有三個標識,就使用三行。列數對應於有多少個子節點,例如,如果有64個子節點,那麼每一列代表其中的一個子節點。因此,該2x2數組中的每個值代表了每個slot中的每個標識是否被設置(當然需要將long類型的整數對應成二進制位才行,那麼64個子節點恰好是需要64位,恰好是兩個long int 類型,每位代表一個子節點;每行代表一個標識)。該標識對於基數樹的查找非常有幫助。如果tag[0]=0(PAGE_DIRTY爲全爲0),那麼標識該節點對應的子節點中沒有相應的節點有存在髒頁,則在尋找髒頁的過程中可以繞過該節點所對應的所有子節點,而不用遍歷整棵樹,提高了查找的效率;tag[1]=0(PAGE_WRITEBACK標誌全爲0)。
三、全局定義
#define RADIX_TREE_MAP_SHIFT 6 /*值爲6表示每個節點有2^6=64個slot; 值爲4表示有2^4=16個slot*/
#define RADIX_TREE_MAP_SIZE (1UL << RADIX_TREE_MAP_SIZE) /*一個葉子節點可以映射的物理頁的數目*/
#define RADIX_TREE_MAP_MASK (RADIX_TREE_MAP_SIZE -1)
#define RADIX_TREE_TAG_LONGS \
((RADIX_TRED_MAP_SIZE + BITS_PER_LONG - 1) / BITS_PER_LONG) /*(64 + 32 -1 )/ 32 =2,TAG_LONGS類型多少位的long類型表示,對應於tags的列*/
#define RADIX_TREE_INDEX_BITS (8 /*CHAR_BITS*/ * sizeof(unsigned long)) // 32 ???這個域是做什麼的,哪個大神給解釋一下?
#define RADIX_TREE_MAX_PATH (DIV_ROUND_UP(RADIX_TREE_INDEX_BITS, RADIX_TREE_MAP_SHIFT)) //其中,DIV_ROUND_UP的宏定義爲:#define DIV_ROUND_UP(n,d) (((n) + (d) - 1) / (d)),也就是向上取整的含義。
#define RADIX_TREE_MAX_TAGS 2 /* 定義slot數佔用的long類型長度的個數,也就是對應於tags中的行數*/
static unsigned long height_to_maxindex[RADIX_TREE_MAX_PATH + 1] //全局數組,在32位機器上,這個數組大小是7,標識每一層的最多有多少個slot
height = 0 : maxindex = 0,第一層只有一個radix_tree_node;
height = 1: maxindex = 2^6-1,第二層最多有63個
height = 2: maxindex = 2 ^ 12 - 1
height = 3: maxindex = 2 ^ 18 -1
height = 4: maxindex = 2 ^ 24 -1
height = 5: maxindex = 2 ^ 30 -1
height = 6: maxindex = 2 ^ 32 -1, 16T
除了每個struct radix_tree_node中有指向相應的page結構體的指針,每個實例化的struct page結構體中也有指向address_space結構體的指針。
所有對文件進行的讀寫,最終轉化爲對address_space結構體的子節點中所指向的物理頁的讀寫。如果是寫的話,那麼在寫完文件偏移量中對應的某一個物理頁之後,address_space結構體所指向的節點對應的tags響應的標誌位將被置爲1,表示該節點的葉子節點所指向的頁被更新過,則通過flusher後臺線程將髒頁寫回到硬盤的時候可以以tags中相應的標誌位依據。
對文件進行讀的時候,如果該文件已經在物理內存中有對應的頁緩存---也就是對應的address_space結構體,那麼直接讀取頁緩存中的數據,不必再次加載文件。這樣,也可以做到一個進程對文件的修改能夠及時反映到另一個進程中或者同樣一個進程中的不同位置,節約了物理內存。
這裏需要說明的是,所有的物理頁都存在於物理內存中,address_space通過radix_tree_root將所有相關的物理頁組織到一起,方便查找。address_space只是提供了一種管理的手段,並不是提供存儲的手段。address_space與文件的inode結構是一一對應關係。
address_space結構和和inode結構體之間的對應關係如圖2,圖中也標註了頁緩存所管理的radix樹。
關於進程的地址空間和文件的映射關係請參考博客。