页面反向映射之文件页面
一、文件映射
和匿名页面相比,文件映射算是比较幸福的一种映射方式了,它可以依靠每个文件都有的address_space结构来引伸出自己需要的信息,例如,所有映射了这个页面的page可以指向文件对应的inode的address_space结构。在vma侧,它们可以在address_space中建立一个自己使用的链表头结构,看起来问题会更简单一些,so far so good。还是家家都有一本难念的经,文件映射也有自己的问题,文件映射的问题在于文件映射时mmap参数的offset是实实在在生效并且会影响到vma结构的。虽然映射的文件相同,但是不同的vma截取(看到的)文件的开始地址和长度不同。给定一个区间[start,end],如何快速的找到所有映射了该文件的vma中哪些包含了该区间的vma。
最简单的办法就是将所有的vma无序的放在一起,查找是一遍所有遍历;或者按照开始地址排序后放在一起,对于查找开始地址比较可以过滤掉一部分不必要的查找,由于没有任何结束地址信息,对于所有满足起始位置的节点都要遍历一次。
对于这种大于一维的查找,有比较实用的算法就是priority search tree,在网络上搜了一下,资料不多,英文的也不多,也不知道是不是名字搜索的不对,看了看,表示不是很理解,但是貌似很厉害的样子。从内核中这个实现的名字来看,它是一个priority radix tree,在基础上加入了基数的概念。这个基数在内核的idx实现中也有应用,它大致的意思就是按照一定的基数,根据基数位上的数值来进行判断和分支。它不是对数字囫囵吞枣,而是抽丝剥茧,庖丁解牛的思路。
具体这种结构的速度和效率有多好,起始也不是很清楚。只是对着代码看了很久,相当于对作者意思的反汇编,注释下各部分的意思,至于效率分析,可能还要更多时间。
二、实现的大致思路
首先树的结构从区间结束地址为键值组成一个最大堆,根节点对应区间的结束值为当前数中所有区间中最大的,这一部分键值在代码中使用h_index或者heap_index来表示,。该树每个节点的h_index大于两个字节点的h_index值。
一个堆只是要求父节点大于两个子节点的值,而两个子节点之间的关系没有任何要求,新插入一个小于根的节点时,需要使用radix来决定该节点是放在左侧还是右侧,其中的radix表示vma的开始地址。这里就隐含一个关系,同一个节点的h_index(vma结束地址)一定大于r_index(vma开始地址)。
为了使用基数,在prio_tree的根节点中保存了该树中所有节点中radix最大值的最高有效bit的位置,有了这个值,对于新添加的节点,就可以一次从节点radix的最高有效bit开始依次判断该节点从根节点开始在每个路口是向左还是向右(0向左,1向右)
三、部分函数注释
1、插入新节点
struct prio_tree_node *prio_tree_insert(struct prio_tree_root *root,
struct prio_tree_node *node)
{
struct prio_tree_node *cur, *res = node;
unsigned long radix_index, heap_index;
unsigned long r_index, h_index, index, mask;
int size_flag = 0;
get_index(root, node, &radix_index, &heap_index);
if (prio_tree_empty(root) ||
heap_index > prio_tree_maxindex(root->index_bits))//新节点h_index参数最高有效bit大于当前最高有效bit数
return prio_tree_expand(root, node, heap_index);需要进行树的扩展,增加index_bits值。
cur = root->prio_tree_node;
mask = 1UL << (root->index_bits - 1);//取到该树所有节点中h_index最大值的最高非零bit值,用来从高到低依次取出待插入值的bit值,用以决定在树的各层左侧还是右侧路由。
while (mask) {
get_index(root, cur, &r_index, &h_index);
if (r_index == radix_index && h_index == heap_index)
return cur;//开始和结束均相等,返回
if (h_index < heap_index ||
(h_index == heap_index && r_index > radix_index)) {//新插入节点的h_index更大,例如当前根节点为0b1000,新节点为0b1111,此时新节点替换根节点,接下来if内语句将原始根节点摘除,并根据h_index子上而下确定它的新位置。
struct prio_tree_node *tmp = node;
node = prio_tree_replace(root, cur, node);
cur = tmp;
/* swap indices */
index = r_index;
r_index = radix_index;
radix_index = index;
index = h_index;
h_index = heap_index;
heap_index = index;
}
if (size_flag)//如果size_flag为1,表示mask已经为零,存在一个路径,它的到达的节点和带插入节点的radix相同,即存在区间开始位置相同的区间,此时扩展树结构,以区间长度作为index,同样依次通过最高bit决定左右路由。
index = heap_index - radix_index;
else
index = radix_index;
if (index & mask) {//radix对应bit非零,向右,
if (prio_tree_right_empty(cur)) {//右节点为空,直接插入
INIT_PRIO_TREE_NODE(node);
cur->right = node;
node->parent = cur;
return res;
} else//非空,根据下一bit继续决定左右。
cur = cur->right;
} else {
if (prio_tree_left_empty(cur)) {
INIT_PRIO_TREE_NODE(node);
cur->left = node;
node->parent = cur;
return res;
} else
cur = cur->left;
}
mask >>= 1;//该bit对应位置已经被占用,继续下一bit
if (!mask) {mask为零,已经存在相同radix节点,设置size_flag,指示下次以区间长度为radix继续路由。
mask = 1UL << (BITS_PER_LONG - 1);
size_flag = 1;
}
}
/* Should not reach here */
BUG();
return NULL;
}
2、树扩展
在增加root->index_bits的同时调整树结构,将当前prio_tree全部转移到node的左节点中。由于node超过了当前index_bits,所以它的最高有效位一定大于当前prio_tree中所有节点的最高有效位。
static struct prio_tree_node *prio_tree_expand(struct prio_tree_root *root,
struct prio_tree_node *node, unsigned long max_heap_index)
{
struct prio_tree_node *first = NULL, *prev, *last = NULL;
if (max_heap_index > prio_tree_maxindex(root->index_bits))
root->index_bits++;//执行该函数就是因为root_index_bits不能表示node节点的所有非零bit,所以+1是必须的。
while (max_heap_index > prio_tree_maxindex(root->index_bits)) {
root->index_bits++;//每次至少加一个有效bit
if (prio_tree_empty(root))//如果根节点为空,所以heap变化比较剧烈,无论如何都要继续,保证root->index_bits能够覆盖新heap_index的所有非零bit。
continue;
//下面操作为了构造出一颗以first为根节点的树,这棵树每个节点只有左孩子,每次新加入的节点都放在该树最底层(叶子左节点)节点的左节点中(整个first树结构就是“八”子的左半部分)。first树每次添加的节点为当前prio_tree的根节点,该节点是prio_tree中heap最大的节点, prio_tree_remove函数删除一个节点,并将该节点的某个子节点上移,直到重新满足heap条件。下面的单步操作直观上说就是将当前的prio_tree根节点转移到first树的最左下节点,并调整prio_tree保持heap特征。
if (first == NULL) {
first = root->prio_tree_node;
prio_tree_remove(root, root->prio_tree_node);
INIT_PRIO_TREE_NODE(first);
last = first;
} else {
prev = last;
last = root->prio_tree_node;
prio_tree_remove(root, root->prio_tree_node);
INIT_PRIO_TREE_NODE(last);
prev->left = last;
last->parent = prev;
}
}
INIT_PRIO_TREE_NODE(node);
if (first) {//first树作为新节点的直接左子节点。
node->left = first;
first->parent = node;
} else
last = node;
if (!prio_tree_empty(root)) {//如果prio_tree还有剩余,真个剩余部分作为first树的最左下子节点。
last->left = root->prio_tree_node;
last->left->parent = last;
}
root->prio_tree_node = node;//新节点作为prio_tree的根节点。
return node;
}
3、节点删除
删除节点node,删除后调整树结构,保持该树的heap属性(节点heap值大于所有所有孩子节点heap值)
void prio_tree_remove(struct prio_tree_root *root, struct prio_tree_node *node)
{
struct prio_tree_node *cur;
unsigned long r_index, h_index_right, h_index_left;
cur = node;
while (!prio_tree_left_empty(cur) || !prio_tree_right_empty(cur)) {//逐层查找子节点中heap值较大的那个节点,直到找到一个叶子节点。
if (!prio_tree_left_empty(cur))
get_index(root, cur->left, &r_index, &h_index_left);
else {
cur = cur->right;
continue;
}
if (!prio_tree_right_empty(cur))
get_index(root, cur->right, &r_index, &h_index_right);
else {
cur = cur->left;
continue;
}
/* both h_index_left and h_index_right cannot be 0 */
if (h_index_left >= h_index_right)
cur = cur->left;
else
cur = cur->right;
}
if (prio_tree_root(cur)) {
BUG_ON(root->prio_tree_node != cur);
__INIT_PRIO_TREE_ROOT(root, root->raw);
return;
}
//先将cur节点从父节点中删除
if (cur->parent->right == cur)
cur->parent->right = cur->parent;
else
cur->parent->left = cur->parent;
//从cur开始到root之间,该路径上所有node节点依次上移,以填充根节点被删除之后留下的空位。
while (cur != node)
cur = prio_tree_replace(root, cur->parent, cur);
}
4、左右节点迭代
其中比较绕的地方在于其中的mask移位操作,这个同样是由于这个树允许开始地址相同造成的,在index_bits用完之后还有一些节点需要使用区间长度作为键值再次路由,单单使用mask无法表示这些信息,添加了size_level和value字段。如果size_level为1,表示已经遇到了起始地址相同的节点。在从子节点返回父节点时,这些参数状态也可以作为参考。prio_tree_left和prio_tree_right均是返回一个可能满足区间重叠的节点,返回为NULL,表示绝无可能。
static struct prio_tree_node *prio_tree_right(struct prio_tree_iter *iter,
unsigned long *r_index, unsigned long *h_index)
{
unsigned long value;
if (prio_tree_right_empty(iter->cur))
return NULL;
if (iter->size_level)//当size_level非零时,出现区间起始地址相同节点,value值即为该区间起始值,不因区间长度而变化。
value = iter->value;
else
value = iter->value | iter->mask;//或人mask,value记录当前节点所有子节点radix的最小值。
if (iter->h_index < value)//如果当前节点所有子节点radix最小值(区间开始)大于区间h_index(目标区间结束),所有字节点一定不满足条件,直接返回。
return NULL;
get_index(iter->root, iter->cur->right, r_index, h_index);
if (iter->r_index <= *h_index) {
iter->cur = iter->cur->right;
iter->mask >>= 1;
iter->value = value;
if (iter->mask) {
if (iter->size_level)//记录相同radix下区间长度生成树的层数。
iter->size_level++;
} else {
if (iter->size_level) {//同起始地址树深度为BITS_PER_LONG层,不能再多了,接下来必须为叶子节点(两个BUG_ON),
BUG_ON(!prio_tree_left_empty(iter->cur));
BUG_ON(!prio_tree_right_empty(iter->cur));
iter->size_level++;
iter->mask = ULONG_MAX;
} else {
iter->size_level = 1;
iter->mask = 1UL << (BITS_PER_LONG - 1);
}
}
return iter->cur;
}
return NULL;
}
5、遍历
prio_tree_first函数查找所有满足重合区间中起始地址最小的。
prio_tree_next也是根据这个原则来遍历,但是多了一个回溯的过程,即查找下一个大于当前节点起始地址的区间。
struct prio_tree_node *prio_tree_next(struct prio_tree_iter *iter)
{
unsigned long r_index, h_index;
if (iter->cur == NULL)
return prio_tree_first(iter);
repeat:
while (prio_tree_left(iter, &r_index, &h_index))
if (overlap(iter, r_index, h_index))
return iter->cur;
while (!prio_tree_right(iter, &r_index, &h_index)) {//如果right节点不满足,向上回溯
while (!prio_tree_root(iter->cur) &&
iter->cur->parent->right == iter->cur)//找到第一个
prio_tree_parent(iter);
if (prio_tree_root(iter->cur))
return NULL;
prio_tree_parent(iter);
}
if (overlap(iter, r_index, h_index))
return iter->cur;
goto repeat;
}
6、first实现的依据
prio_tree_first函数中对于第一个节点的查找没有使用回溯。这里使用了这种树结构的隐含属性:任意一个左节点及其子节点的radix值一定大于同层的左节点及其所有子节点,这一点可以通过在bits增加时执行的左节点调整方式可以证明。 radix是区间的左值,如果满足left,说明node->heap > target->radix,即当前节点的右值大于目标区间的左值,如果该节点的子节点不满足,说明子节点node->radxi > target->heap,即当前节点的左值大于目标区间右值,由于所有同层右节点及子节点均大于左节点及子节点的radix值,如果左节点不满足,右节点肯定不满足。
while (1) {
if (overlap(iter, r_index, h_index))
return iter->cur;
if (prio_tree_left(iter, &r_index, &h_index))
continue;
if (prio_tree_right(iter, &r_index, &h_index))
continue;
break;
}
四、实现基础
对于这里的vma区间匹配有一个特殊的性质:对于【radix,heap】表示的二元组,heap > radix,节点在数中的层数按照heap由大到小的顺序组成对,最终根节点的heap值是所有的heap、radix的最大值,也即2^(heap_bits -1)能够覆盖到radix和heap的最大值。
对于一个右节点,假设从根节点到该节点的层数为h,在插入该节点时,它radix的第(max-h)个bit位的值一定为1,新插入的节点的值一定大于从根节点走到此处是累加的mask值。并且在节点所在层数不变,即h值不变的情况下,新插入的到达该分支的节点的【max-h,max】之间所有bit与该节点相同。由于max单调递增,所以新添加在该分支的所有子节点的radix一定大于等于该值,并且大于从根节点到达这里累加的mask值。
特殊情况在于当新添加节点的heap值大于根节点的heap值时需要对树结构进行调整,在expand代码里会从当前树提取新节点(调整堆结构),从根节点到被提升的也子节点之间一条路径上的所有节点逐层上提,这时可能出现这些上提节点h值减小的问题,此时可能会存在右节点的radix值小于子节点radix值的情况。
但是在expand的代码中也可以看到,新抽取出来的节点被逐层单个放在了左节点,这个新生成树的长度会抵消掉节点上升的层数,右节点所有子节点的值大于从根节点走到这里累积的mask值的属性依然可以保持,因为新分裂的左节点的层数会补偿节点被上提的层数。
在prio_tree_right函数中,value记录到达该右节点累加的基础值,也就是该右节点所有子节点的最小值minrchild
if (iter->size_level)
value = iter->value;
else
value = iter->value | iter->mask;
if (iter->h_index < value)//这个地方利用了前面所说的右节点所有字节点一定大于累加的mask值的属性。
return NULL;
如果目标匹配区间的右区间小于所有节点左区间的最小值,整个右节点的所有子节点就没有必要判断了,也就是如果prio_tree_right函数返回为NULL,则整个节点的所有子节点没有必要再遍历。需要注意的是,左节点并没有这种对应关系,所以左节点判断只依赖堆的关系,即父节点的值大于子节点的heap值,如果目标区间的左区间大于节点的右区间(heap值,因此大于所有子节点的heap值),所有子节点没有必要再遍历。
反过来说,如果left返回为true,只是表示
static inline int overlap(struct prio_tree_iter *iter,
unsigned long r_index, unsigned long h_index)
{
return iter->h_index >= r_index && iter->r_index <= h_index;
}
的第二个条件满足,即当前节点区间的右值(heap值)大于等于目标区间的左值,如果overlap不满足,说明当前节点的左值大于目标区间的右值(heap值),只要减少节点的左值即可,对于左节点来说,子节点的左值之间没有任何相对关系,所以如果left的overlap判断不满足,需要继续尝试子左节点(和left返回值不同,如果left返回值为NULL,不需要再继续子节点)。对于右节点,子节点的左值均大于当前值,如果overlap返回false,可以放弃整个右子树,因为子树中左值不可能再减少了。如果右节点overlap返回错误,可以放弃整个右子树。这个思路在prio_tree_next函数中体现为下面的形式
while (prio_tree_left(iter, &r_index, &h_index))
if (overlap(iter, r_index, h_index))
return iter->cur;
从当前节点向上回溯,找到第一个父节点在回溯链上、并且可能与目标区间可能重合的右节点。
while (!prio_tree_right(iter, &r_index, &h_index)) {
while (!prio_tree_root(iter->cur) &&
iter->cur->parent->right == iter->cur)
prio_tree_parent(iter);
//执行到这里,cur为一个左节点
if (prio_tree_root(iter->cur))
return NULL;
prio_tree_parent(iter);//左节点的父节点
}
if (overlap(iter, r_index, h_index))
return iter->cur;
五、部分优点
按照结束顺序从高到低排列,越靠下的节点heap值也就是区间结束值越小,当该值小于目标区间的开始值之后就可以不再更底层的节点;当一个right或者left函数返回值为NULL时,没有必要在尝试它的子节点。
左右节点按照开始地址从小到大排列,如果一个节点的起始地址大于结束地址,没有必要继续尝试其右子节点。