路由表的结构与算法分析--trie插入

在linux的trie树中插入一条路由是很复杂的,远比查找要复杂的多,因为每插入一条路由就要看看是否要动态调整trie树,不是还好,如果要动态调 整,那活儿就大了,正是因为这一点,trie路由表往往在大型的机器上用到,那些机器不吝啬动态调整trie树的时候的空间损失,如果是性能不那么靠铺的 机器,还是老老实实用哈希路由表吧,只要不是疯狂插入很多路由表导致很多哈希碰撞,性能是很棒的,我在pc上装过trie,一旦路由条目大了,内存疯狂耗 尽,要知道,路由表用的可是内核内存阿,不被换出的。bsd一向巨猛,unix嘛,linux一向不管那么多,代码凌乱而有序,要懂得欣赏,仔细品味才有收获。
bsd以前的路由表是基于二叉radix树的,而现在是固定多分枝trie树,具体就是32位分为4段,然后类似于页目录页表那样多叉树搜索,如果是二叉 树的,那么只要访问路由表就要锁住全树,这么说好像多分枝树就不用锁了一样,错了,同样要锁,不同之处在于二叉树如果只锁一个节点那么对下面同样影响很大,想象一下如果锁住了根的左孩子,那么满树的情况下一半的节点都要受影响,但是多分枝就不同了,比如n分枝,如果锁住根的第m孩子(m<n-></n->如果一位一位的比较,那么就是二叉radix树,,因为一位非0即1,所以树就有2个孩子,如果32位32位比较,那么就是2的32次方叉树,也就是根节 点有2的32次方个孩子,所有的ip地址每个为一个孩子,世界上没有哪个路由表是这么实现的,还是二叉树的好一些,但是考虑到前面说的缺点,bsd改进了 radix树,使之成为固定分支的trie树,实质和二叉radix树是一样的。linux来了,世界不吭气了,你不是说叉数多了不好,少了又有这样那样 的缺点,bsd来了个折中,不多不少,2的8次方,在数字上来个折中,linux则更进一步,在动态效果上来了个折中,也就是说,linux的trie完 全动态,游走于二叉和2的32次方叉之间,看你还怎么说,即使你说m叉正好,那么盯着linux几分钟,更新一下路由表,它确实到了m叉,如果你说n叉很 糟糕,但linux这时确实是n叉树,那么继续盯几分钟,插入几条路由或删除几条,哈哈,不是n叉了吧,这一切是怎么实现的呢?这正是我这篇文章要说的动态trie树的插入。来吧,看代码:

static struct list_head * fib_insert_node(struct trie *t, int *err, u32 key, int plen)

{

int pos, newpos;

struct tnode *tp = NULL, *tn = NULL;

struct node *n;

struct leaf *l;

int missbit;

struct list_head *fa_head = NULL;

struct leaf_info *li;

t_key cindex;

pos = 0;

n = t->trie;

while (n != NULL && NODE_TYPE(n) == T_TNODE) {//循环查找我们要插入的节点

tn = (struct tnode *) n;

check_tnode(tn);

//一种寻找,找到我们要插入的位置,意义在于,我们如果要插入一个节点,那么必须先做一次查找,如果没有再插入,这和标准二叉树的插入时一样的。

if (tkey_sub_equals(tn->key, pos, tn->pos-pos, key)) {

tp = tn;

pos = tn->pos + tn->bits;

n = tnode_get_child(tn, tkey_extract_bits(key, tn->pos, tn->bits));

BUG_ON(n && NODE_PARENT(n) != tn);

} else

break;

}

BUG_ON(tp && IS_LEAF(tp));

if (n != NULL & amp;& IS_LEAF(n) && tkey_equals(key, n->key)) {//如果找到了就直接 插入该叶子的链表,注意,不管是插入还是查找,都需要一个查找操作,而且如果命中的话都是在叶子。这也是这个插入规则决定的,下面会看到相应的代码。

struct leaf *l = (struct leaf *) n;

li = leaf_info_new(plen);

if (!li) {

*err = -ENOMEM;

goto err;

}

fa_head = &li->falh;

insert_leaf_info(&l->list, li);

goto done;

}

t->size++;//没有一样的,那么就申请一个叶子,因此插入时最起码要插入一个叶子,查找的时候也必然在叶子节点命中。

l = leaf_new();

if (!l) {

*err = -ENOMEM;

goto err;

}

l->key = key;//设置该叶子的键值,注意,虽然它是叶子,但是也是一个tnode

li = leaf_info_new(plen);//根据掩码长度申请一个叶子信息结构。

if (!li) {

tnode_free((struct tnode *) l);

*err = -ENOMEM;

goto err;

}

fa_head = &li->falh;//初始化相应的链表

insert_leaf_info(&l->list, li);

if (t->trie && n == NULL) {//如果查到的最后命中的不是一个叶子,可能被删除了,那么就把这个叶子插入进去。

NODE_SET_PARENT(l, tp);

cindex = tkey_extract_bits(key, tp->pos, tp->bits);

put_child(t, (struct tnode *)tp, cindex, (struct node *)l);

} else { //n是一个tnode,这说明新插入的key肯定有和这个n不同的了,那么就要考虑多一点了

if (tp)

pos = tp->pos+tp->bits;

else

pos = 0;//第一个节点

if (n) {

newpos = tkey_mismatch(key, pos, n-& gt;key);//寻找n->key和key不同的左边第一个位置相对于0的偏移,因为pos之前肯定是相同的,否则就不可能到达n了。

tn = tnode_new(n->key, newpos, 1);

} else {//tp的第一个孩子

newpos = 0;

tn = tnode_new(key, newpos, 1);

}//有上述可知,新创建的tnode的bits都是1,这个在tnode_new中有说明,那么这不成了二叉树了吗?bits为1说明只有2个孩子,但是今后要resize,resize的时候bits就会慢慢扩大了

if (!tn) {

free_leaf_info(li);

tnode_free((struct tnode *) l);

*err = -ENOMEM;

goto err;

}

NODE_SET_PARENT(tn, tp);//将新创建的tp的父亲设置为tp

//将新创建的tn作为tp的孩子,而老的n作为tn的孩子,当然插入的节点就是刚才创建的那个叶子,同样也作为tn的孩子插入。

missbit = tkey_extract_bits(key, newpos, 1);//确定插入的位置。

put_child(t, tn, missbit, (struct node *)l);

put_child(t, tn, 1-missbit, n);

if (tp) {//tn作为tp的孩子插入树中

cindex = tkey_extract_bits(key, tp->pos, tp->bits);

put_child(t, (struct tnode *)tp, cindex, (struct node *)tn);

} else {

rcu_assign_pointer(t->trie, (struct node *)tn)

tp = tn;

}

}

if (tp && tp->pos + tp->bits > 32)

printk(KERN_WARNING "fib_trie tp=%p pos=%d, bits=%d, key=%0x plen=%d/n",

tp, tp->pos, tp->bits, key, plen);

//重头戏到了,一切插入完毕,trie_rebalance开始重新平衡这棵树,主要是1.不要太高,这样查询慢;2.不要太矮,浪费空间。

rcu_assign_pointer(t->trie, trie_rebalance(t, tp));

done:

t->revision++;

err:

return fa_head;

}

上 面的插入操作也是挺简单的,但是如果事情到此为此那么这个算法就称不上什么美妙了,最关键的就是这个重新平衡的操作,说它是动态调整的,也是因为他会自动平衡,但是不是每次插入都自动平衡一次,那样的办法也太老土了,办法就是满足一定条件才自动平衡,比如,树太高了,太矮了,等等,那么为何每次插入都要在 这里调用这个平衡函数呢?完全没有必要,但是linux强调“每次就做好一件事”,如果把检查平衡条件也放进来那这个插入操作作的事情就太多了,所以这里仅仅作为一个查点,到底用不用真正调用平衡操作就看trie_rebalance内部什么逻辑了。下面就看一下trie_rebalance:

static struct node *trie_rebalance(struct trie *t, struct tnode *tn)

{//这个函数很简单,就是从当前节点一直平衡到最上层的根节点,最后返回已经平衡的根节点。

int wasfull;

t_key cindex, key;

struct tnode *tp = NULL;

key = tn->key;

while (tn != NULL && NODE_PARENT(tn) != NULL) {

tp = NODE_PARENT(tn);//取得tn的父节点,为了询问这个父亲它是父亲的第几个孩子

cindex = tkey_extract_bits(key, tp->pos, tp->bits);//它的父亲告诉它是第cindex个孩子

wasfull = tnode_full(tp, tnode_get_child(tp, cindex));//它是一个满孩子吗?满孩子的意思是说在它和它父亲之间不能再有新父亲从而它的父亲变成它的祖父了,也就是禁止。

tn = (struct tnode *) resize (t, (struct tnode *)tn);//重新平衡这个子树

tnode_put_child_reorg((struct tnode *)tp, cindex,(struct node*)tn, wasfull);//重新将已经resize过的tn作为tp的孩子插入树中。

if (!NODE_PARENT(tn))

break;

tn = NODE_PARENT(tn);

}

if (IS_TNODE(tn))//最后一把平衡一下根节点。

tn = (struct tnode*) resize(t, (struct tnode *)tn);

return (struct node*) tn;

}

这 个函数逻辑十分清晰,就是自下而上依次平衡遇到的祖先,一直到根节点,这也是十分合理的,注意的地方就是第10行取出了wasfull,然后平衡这棵子 树,最后在12行的地方又将它加入,以wasfull为一个参数,这样可能减少tp的满孩子计数,因为在resize函数里面可能会递增tn的pos,这 样如果原来tp->pos+tp->bits和tn->pos相等从而wasfull为1,那么tn的pos递增后(这个递增要等到inflate tp的时候才会进行,因此这里没有希望递增)或者resize返回null后,以上那个条件不满 足,从而tp的满孩子计数会递减,这样有助于减少祖先的重新平衡压力。下面接着看resize:

static struct node *resize(struct trie *t, struct tnode *tn)

{

int i;

int err = 0;

struct tnode *old_tn;

int inflate_threshold_use;

int halve_threshold_use;

int max_resize;

if (!tn)

return NULL;

pr_debug("In tnode_resize %p inflate_threshold=%d threshold=%d/n",

tn, inflate_threshold, halve_threshold);

if (tn->empty_children == tnode_child_length(tn)) {//如果一个孩子也没有,那么留着它也没有什么用,因为一切都是为了查找,而查找必须在叶子命中,这个tnode都没有孩子了,因此把它删除。

tnode_free(tn);

return NULL;

}

if (tn->empty_children == tnode_child_length(tn) - 1)//如果只有一个孩子,那么就返回这个孩子,相当于把这个孩子上提了一级,这个叫地址压缩。

for (i = 0; i

struct node *n;

n = tn->child[i];

if (!n)

continue;

NODE_SET_PARENT(n, NULL);

tnode_free(tn);

return n;

}

check_tnode(tn);

if (!tn->parent)

inflate_threshold_use = inflate_threshold_root;

else

inflate_threshold_use = inflate_threshold;

err = 0;

max_resize = 10;//以下的循环保证了树不能太高,但是只限制最多resize10次

while ((tn->full_children > 0 && max_resize-- &&

50 * (tn->full_children + tnode_child_length(tn) - tn->empty_children) >= inflate_threshold_use * tnode_child_length(tn))) {

old_tn = tn;

tn = inflate(t, tn);//扩展tn的容量,容量增加2倍

if (IS_ERR(tn)) {

tn = old_tn;

break;

}

}

if (max_resize

if (!tn->parent)

printk(KERN_WARNING "Fix inflate_threshold_root. Now=%d size=%d bits/n",

inflate_threshold_root, tn->bits);

else

printk(KERN_WARNING "Fix inflate_threshold. Now=%d size=%d bits/n",

inflate_threshold, tn->bits);

}

check_tnode(tn);

if (!tn->parent)

halve_threshold_use = halve_threshold_root;

else

halve_threshold_use = halve_threshold;

err = 0;

max_resize = 10;//同上,只不过这个循环来压缩空间,使得树不能太矮。

while (tn->bits > 1 && max_resize-- &&

100 * (tnode_child_length(tn) - tn->empty_children)

halve_threshold_use * tnode_child_length(tn)) {

old_tn = tn;

tn = halve(t, tn);

if (IS_ERR(tn)) {

tn = old_tn;

break;

}

}

if (max_resize

if (!tn->parent)

printk(KERN_WARNING "Fix halve_threshold_root. Now=%d size=%d bits/n", halve_threshold_root, tn->bits);

else

printk(KERN_WARNING "Fix halve_threshold. Now=%d size=%d bits/n", halve_threshold, tn->bits);

}

if (tn->empty_children == tnode_child_length(tn) - 1)// 如果进行过上述操作以后tn只剩下一个孩子,那么就把这个孩子压缩到上一级它的祖父那里,将祖父作为父亲,。注意这时tn不可能没有孩子,如果两个循 环都没有进行,那么tn没有孩子的情况在第15行就应该返回,如果进行了两个循环中的一个,那么最起码也要加入left,right,tn的孩子,tn的孙子中的一个。

for (i = 0; i

struct node *n;

n = tn->child[i];

if (!n)

continue;

NODE_SET_PARENT(n, NULL);

tnode_free(tn);

return n;

}

return (struct node *) tn;

}

这个函数本质上没有什么,只是在不同的条件下采取不同的动作,最重要的就是inflate和halve两个函数,我这里只分析inflat,halve道理相同,不再浪费时间。

static struct tnode *inflate(struct trie *t, struct tnode *tn)

{

struct tnode *inode;

struct tnode *oldtnode = tn;

int olen = tnode_child_length(tn);

int i;

//这里将原来的孩子空间扩大了一倍,为何要扩大,使因为满孩子太多了,这样随着插入,树会变得越来越高,严重影响了性能。这样做的结果就是将原来tn的 孙子可能就变成了tn的孩子,并且tn的所有孩子进行了重洗牌,满孩子全部被删除,满孩子的孙子们被新加入的叫做left和right的接管。

tn = tnode_new(oldtnode->key, oldtnode->pos, oldtnode->bits + 1);

if (!tn)

return ERR_PTR(-ENOMEM);

for (i = 0; i

struct tnode *inode = (struct tnode *) tnode_get_child(oldtnode, i);//得到tn原来的孩子,这个时候这个孩子还没有什么太大的作用,只是为了重新设置新孩子的键值。

if (inode &&

IS_TNODE(inode) &&

inode->pos == oldtnode->pos + oldtnode->bits &&

inode->bits > 1) {

struct tnode *left, *right;

t_key m = TKEY_GET_MASK(inode-& gt;pos, 1);//得到原孩子pos下一位的便宜,在此偏移置1,其它位置置0,因为下面要创建的left和right的pos比原来的pos大了1位,所以这个大的1位不是1就是0,根据到底是1还是0来由区别的加入left和right。

left = tnode_new(inode->key&(~m), inode->pos + 1,

inode->bits - 1);

if (!left)

goto nomem;

//具体的设置和上面的left相同。

right = tnode_new(inode->key|m, inode->pos + 1,

inode->bits - 1);

if (!right) {

tnode_free(left);

goto nomem;

}//最后将这两个新的节点作为孩子加入到tn,注意这里加入的是tn,而不是oldnode,如果加入oldnode就晚了,原来的节点将被永久的覆盖掉。

put_child(t, tn, 2*i, (struct node *) left);

put_child(t, tn, 2*i+1, (struct node *) right);

}

}

for (i = 0; i

struct node *node = tnode_get_child(oldtnode, i);

struct tnode *left, *right;

int size, j;

if (node == NULL)//如果没有这个孩子,忽略继续。

continue;

if (IS_LEAF(node) || ((struct tnode *) node)->pos >

tn->pos + tn->bits - 1) {// 如果这个孩子只是个叶子,那么就将这个叶子作为tn的孩子加入trie树;如果这个节点不是原来oldnde的满孩子,那么也是直接接管,这样,接下来的情况很大程度上就是处理oldnode的满孩子的问题了,怎么处理呢,这就用到了left和right,且看下文...

if (tkey_extract_bits(node->key, oldtnode->pos + oldtnode->bits,

1) == 0)// 到底加到哪里要看oldnode->pos+oldnode->bits的下一位是几。毕竟此tn不是彼oldnode,此tn的bits增 加了一个1。

put_child(t, tn, 2*i, node);

else

put_child(t, tn, 2*i+1, node);

continue;

}

inode = (struct tnode *) node;

if (inode->bits == 1) {//如果这个孩子的bits为1,那么肯定最多两个孩子,将它的两个孩子接管,然后释放这个孩子,很可怜!养父被无情的抛弃!

put_child(t, tn, 2*i, inode->child[0]);

put_child(t, tn, 2*i+1, inode->child[1]);

tnode_free(inode);

continue;

}

left = (struct tnode *) tnode_get_child(tn, 2*i);

put_child(t, tn, 2*i, NULL);//加入占位

BUG_ON(!left);

right = (struct tnode *) tnode_get_child(tn, 2*i+1);

put_child(t, tn, 2*i+1, NULL);//加入占位

BUG_ON(!right);

size = tnode_child_length(left);//以下的循环开始接管满孩子的孩子们,它们满了犯了死罪,但是它们的孩子们是无辜的,于是left和right就接管它们。

for (j = 0; j

put_child(t, left, j, inode->child[j]);

put_child(t, right, j, inode->child[j + size]);

}//以下将新的left和right加入到tn,作为tn的孩子,可是left和right也要经受满孩子检查和孩子数量的检查,这样才不至于平衡了父亲孩子却失衡了,于是递规调用resize函数接受和oldnode一样的一切。

put_child(t, tn, 2*i, resize(t, left));// 可能原来oldnde的孩子inode的孩子全部都加到right上去了,这时,risize left的结果就是null,这样就递减了tn的满孩子数。

put_child(t, tn, 2*i+1, resize(t, right));

tnode_free(inode);

}

tnode_free(oldtnode);

return tn;

nomem://在任何操作失败的情况之下,都会回滚,类似于事务,要么做完,要么什么也不做。

{

int size = tnode_child_length(tn);

int j;

for (j = 0; j

if (tn->child[j])

tnode_free((struct tnode *)tn->child[j]);

tnode_free(tn);

return ERR_PTR(-ENOMEM);

}

}

以上就是trie插入操作的全部内容了,比起查询复杂多了,最复杂的就是那个平衡操作,事实证明非常地消耗内存,我一直在思考linux到底为何不用一种 更加确定的方式来实现trie树类似于bsd,仅仅是为了在两个极端之间游走的哲学意义上的原因吗?我看它就像个小孩子来回跑,代码设计的如果精妙却丢失 了一种辉煌,类似于unix的那种辉煌,当你读unix的时候,会不禁感觉到一种大气在里面,而linux却没有这种大气,更多的是令人叹为观止的技巧, 让人惊叹的是这么多的小小技巧结合起来看似没有关系的代码竟然能协同的那么好,最终的结果就是可以胜任很多unix可以胜任的工作,这是一个秘密,同时我 相信,哪怕用linux的人越来越多,其越来越被攻击者重视,那么它还是无懈可击的,这也许也是一个秘密吧。

你可能感兴趣的:(trie)