Maple Tree
Maple Tree是2022年,由Liam引入,为了解决vma处理时的锁问题。
虽然Maple Tree脱胎于B树,但是细节上还是有很大的差异的。而且这代码我真的是费了老大劲才终于看懂一点。赶紧记录一下,以免忘记。
maple_node如何表达数据
Maple Tree中的一个节点用maple_node表示,理解了其中时如何表达数据的,才能理解相关的操作是如何执行的。
每个节点包含了多个slot/pivot,其中pivot按照递增的顺序排列。 当我们在一个坐标上依次标注pivot后,就展现出一段段由pivot分割的区域,而这些区域对应的值,则存储在相应的slot中。
如果我们以slot为中心,每一个slot[n]代表的区域就是[P(n-1) + 1, Pn]。当然其中由两个特例,头和尾的slot由整个节点的min/max限制。
便于理解,我们看一下父子关系下的情况。
区域slot 1代表的区域,分割成了更小粒度的区域,由子节点表达。此时子节点的min/max就是slot 1的区域界限P0+1和P1了。
函数详解
mas_wr_walk()
这个函数是用来在写入数据前查找对应节点的,这个的作用和红黑树和B树在插入前做的动作一样。所以遍历过程就不再赘述,而是讲一下遍历中保存的几个在最后修改值过程中要用到的值。
我们先来直观看一下遍历完,几个重要值之间的关系
依次解释一下:
遍历过程中mas->node指向的是当前的节点,遍历完时就指向需要修改的叶子节点
mas->min/max表达的是当前节点的界限
mas->index/last形成了一个区域[index, last],这是本次修改涉及的区域
offset/offset_end是[index, last]这个区域所在的起始和结束的下标
[r_min, r_max]是offset下标对应区域范围
end_piv是offset_end下标对应区域右侧界限
根据上面的这些含义,我们可以得出的隐含含义是:
区域[index, last] 包含在 区域[r_min, end_piv]
有了这些背景知识,我们就可以看看具体的修改过程,以及各种不同的case了。
PS: 补充一点, end_piv是在mas_wr_end_piv()中计算的,和mas_wr_walk()搭配使用才有效果。
mas_wr_node_store()
遍历完找到需要修改的叶子节点,我们最终的目的是将[index, last]写入到节点中。这个过程有好几种case,我们打开看一下。
首先按照offset和offset_end的关系,分两个大类。也就是新的区域是在原来的一个区域里,还是跨了多个区域。 然后按照[index, last]和[r_min, end_piv]的关系,分四小类。决定是改写还是要插入新的。
因为有新增插入slot的情况,此时新增一个变量new_end以表示完成插入后节点预期长度。
offset == offset_end
这时,说明[index, last]这个区域就在offset下标所指定的区域内,并不影响到其他区域。 并且, end_piv实际上等于r_max。
此时我们假设初始情况如下:
index == r_min && last == r_max
这种情况说明只要直接改写原有slot的值,并没有改变区域划分。比较直接。
此时节点长度并没有变化。
index != r_min && last != r_max
这种情况需要在原区域的前后都要切一块出来。
所以这种情况下,需要增加两个slot来标记。
index == r_min && last != r_max
index != r_min && last == r_max
offset < offset_end
这时,说明[index, last]在原节点上跨了多个区域。并且在小case的比较上,不是对比r_max了,而是end_piv。
此时我们假设初始情况如下:
index == r_min && last == end_piv
index != r_min && last != end_piv
index == r_min && last != end_piv
index != r_min && last == end_piv
mas_wr_modify()
mas_wr_modify()是mas_wr_node_store()的父函数,虽然在mas_wr_node_store()中可以处理(几乎)所有的情况,但是在mas_wr_modify()里还是做了点优化。
原因是mas_wr_node_store()的操作都是基于复制节点来实现的。而在某些情况下,并不需要复制节点,而只要在原节点上进行修改依然可以做到rcu safe。
在了解了各种修改的情况后,我们再来看看这些可以优化的case。
PS: 强调一点,进入mas_wr_modify时,mas->node一定是叶子节点。所以不用拷贝gap。
offset == end: mas_wr_append()
变化的区间正好是当前节点最后一个区域的情况。而且隐含的条件是offset == offset_end,因为offset_end不可能大于end。
因为只需要添加到节点最后,所以单独拿出来做了一个优化。不过对rcu情况不适用。
new_end == end: mas_wr_slot_store()
这中情况说明节点内的数据长度并没有变化,所以我们不需要复制一个新的节点来做变更,只要在本地做修改就性了。
而且这里有个隐含条件是offset < offset_end,因为在offset == offset_end的case中,没有长度不变的情况。PS:唯一不便的情况已经被特殊处理了。
常用API
maple tree提供了一些API,然后其他子系统,比如mm,会在这个基础上再封装一层。
mas_prev|next()
这一类一共有四个函数
mas_prev(mas, min)
mas_next(mas, max)
mas_prev_range(mas, min)
mas_next_range(mas, max)
他们两两行为是对称的,一个是往前,一个是往后。
在叶子节点一层,在mas->index位置上往前|后一格。
区别在与
mas_prev|next(): 是要一直找到前|后面一格非空的slot。
mas_prev|next_range(): 则只是往前|后一格,如果是空也没有关系。
除此之外,还要关注mas中index/last的变化。
遍历时是按照mas->index的值去找的,和mas->last没有关系。查找的最大范围是通过参数max来指定的。
不论找不找得到,mas->index/last都会设置到某个真实的range的范围上
如果不能再往前或者往后了,status会被设置成ma_overflow或者ma_underflow
最后mas->index/last指定的范围一定和min/max指定的范围有交叉
mas_find()
这个函数的行为和mas_next()非常相似,除了在第一次执行的时候。
如果第一次执行时,mas->index对应有值则返回这个值。而mas_next()则需要时返回下一个。 如果第一次执行时,mas->index对应值为NULL,返回结果和mas_next()一样。
测试代码
在内核代码里有专门的测试程序,在这里记录一下。一旦有改动,需要通过测试程序才能提交。
还有对应的内核模块测试代码。
不过需要打开选项CONFIG_TEST_MAPLE_TREE。
编译完,安装内核模块也是测试。不过比用户态的程序少了点case。
参考资料
Last updated