最近在学习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树。
关于进程的地址空间和文件的映射关系请参考博客。