fib_unmerge 是内核路由子系统中一个精巧的按需优化设计:
默认状态下 ,local 表只是 main 表的别名,所有路由合并在一张表里,fib_lookup 只需查一次表,性能最优
添加策略路由时 ,fib_unmerge 将 local 路由从 main 表中切割出来,放入独立的 local 表,确保策略路由能正确匹配
切割四步走 :创建新 local 表 → 复制 local 路由 → 替换旧表 → 清理 main 表中的残留条目
幂等设计 :只在首次添加策略路由时执行分离,后续再添加时 fib_trie_unmerge 检测到已分离,直接返回
本文将从调用点入手,逐步分析 fib_unmerge 的实现细节。
0.0.1. 背景 前面文章提到,local 路由默认情况下是插入到 main 表里的。
在 ping本机网口的IP地址,tcpdump在lo口才能抓到对应报文 中我们分析了 fib_lookup 函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 374 static inline int fib_lookup (struct net *net, struct flowi4 *flp, 375 struct fib_result *res, unsigned int flags) 376 { ...381 if (net->ipv4.fib_has_custom_rules)382 return __fib_lookup(net, flp, res, flags); ...388 tb = rcu_dereference_rtnl(net->ipv4.fib_main);389 if (tb)390 err = fib_table_lookup(tb, flp, res, flags); ...395 tb = rcu_dereference_rtnl(net->ipv4.fib_default);396 if (tb)397 err = fib_table_lookup(tb, flp, res, flags);
没有自定义策略路由规则时(fib_has_custom_rules 为 false),fib_lookup 只查找 main 和 default 两张表,并没有查找 local 表。
这是因为在初始化时,local 表被创建为 main 表的别名:
1 local_table = fib_trie_table(RT_TABLE_LOCAL, main_table);
所有 local 路由实际上都被插入到了 main 表里。这样做的好处是减少一次路由表查找,提升性能。
那问题来了:当用户添加了自定义的策略路由规则后,路由查找走的是 __fib_lookup → fib_rules_lookup,会按策略路由顺序依次查找 local/main/default 三张表。如果 local 路由还在 main 表里,那 local 表就是空的,查找 local 表就没有意义了。
所以,只有在用户添加了自定义策略路由规则后,local 路由才需要从 main 表里分离出来,放到独立的 local 表里 。
函数 fib_unmerge 就是做这个事情的。
0.0.2. 调用点 fib_unmerge 的调用点有两个:
fib4_rule_configure:添加策略路由
fib4_rule_delete:删除策略路由
这两个函数是 IPv4 策略路由模版 fib4_rules_ops_template 里的 configure 和 delete 函数定义。
1 2 3 4 5 6 7 8 470 static const struct fib_rules_ops __net_initconst fib4_rules_ops_template = {471 .family = AF_INET,472 .rule_size = sizeof (struct fib4_rule), ...476 .match = fib4_rule_match,477 .configure = fib4_rule_configure,478 .delete = fib4_rule_delete, ...
以函数 fib4_rule_configure 为例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 269 static int fib4_rule_configure (struct fib_rule *rule, struct sk_buff *skb, 270 struct fib_rule_hdr *frh,271 struct nlattr **tb,272 struct netlink_ext_ack *extack) 273 {274 struct net *net = sock_net(skb->sk);275 int err = -EINVAL;276 struct fib4_rule *rule4 = (struct fib4_rule *) rule;277 278 if (frh->tos & ~IPTOS_TOS_MASK) {279 NL_SET_ERR_MSG(extack, "Invalid tos" );280 goto errout;281 }282 ...288 if (rule->table == RT_TABLE_UNSPEC && rule->action == FR_ACT_TO_TBL) {289 struct fib_table *table ;290 291 table = fib_empty_table(net);292 if (!table) {293 err = -ENOBUFS;294 goto errout;295 }296 297 rule->table = table->tb_id;298 }299 300 301 err = fib_unmerge(net);302 if (err)303 goto errout;304 305 rule4->src_len = frh->src_len;306 rule4->srcmask = inet_make_mask(rule4->src_len);307 rule4->dst_len = frh->dst_len;308 rule4->dstmask = inet_make_mask(rule4->dst_len); ...
关键在第 300-303 行:fib_unmerge(net) —— 在配置新的策略路由规则时,先把 local 和 main 表分离开。
0.0.3. fib_unmerge 实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 int fib_unmerge (struct net *net) { struct fib_table *old , *new , *main_table ; old = fib_get_table(net, RT_TABLE_LOCAL); if (!old) return 0 ; new = fib_trie_unmerge(old); if (!new) return -ENOMEM; if (new == old) return 0 ; fib_replace_table(net, old, new); fib_free_table(old); main_table = fib_get_table(net, RT_TABLE_MAIN); if (main_table) fib_table_flush_external(main_table); return 0 ; }
fib_unmerge 的逻辑可以分为四步:
0.0.3.1. 第一步:获取旧的 local 表 1 old = fib_get_table(net, RT_TABLE_LOCAL);
获取当前的 local 路由表。如果不存在,说明不需要分离,直接返回。
0.0.3.2. 第二步:创建新的 local 表 1 new = fib_trie_unmerge(old);
fib_trie_unmerge 从旧的(合并在 main 表里的)local 表中,把 local 路由复制到一张新的独立 local 表里。
如果返回的 new 和 old 相同,说明已经分离过了(不是第一次添加策略路由),直接返回。
0.0.3.3. 第三步:替换旧表 1 2 fib_replace_table(net, old, new); fib_free_table(old);
用新创建的 local 表替换掉旧的(别名到 main 的)local 表,并释放旧表。
0.0.3.4. 第四步:清理 main 表 1 2 3 main_table = fib_get_table(net, RT_TABLE_MAIN);if (main_table) fib_table_flush_external(main_table);
由于之前 local 路由都在 main 表里,现在已经复制到了独立的 local 表,需要把 main 表里残留的 local 路由条目清理掉。fib_table_flush_external 负责刷新 main 表中的外部引用。
0.0.4. 流程总结 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 初始状态: local 表 ──别名──> main表(local 路由都在main表里) 用户执行 ip rule add ... 添加策略路由: fib_newrule └── ops- > configure 即 fib4_rule_configure └── fib_unmerge(net) ├── 1. old = fib_get_table(LOCAL ) ├── 2. new = fib_trie_unmerge(old ) / / 复制local 路由到新表 ├── 3. fib_replace_table(old , new ) / / 替换local 表 ├── fib_free_table(old ) / / 释放旧表 └── 4. fib_table_flush_external(main) / / 清理main表中残留 分离后状态: local 表(独立,包含所有local 路由) main表(仅包含unicast路由)
0.0.5. 与 fib_has_custom_rules 的关系 回顾 fib_lookup 的入口:
1 2 if (net->ipv4.fib_has_custom_rules) return __fib_lookup(net, flp, res, flags);
当 fib_has_custom_rules 为 true 时,走 __fib_lookup 路径,会遍历策略路由链表,依次查找 local/main/default 三张表。此时 local 表已经通过 fib_unmerge 分离出来,包含了独立的 local 路由。
当 fib_has_custom_rules 为 false 时,直接查找 main 表(其中包含了 local 路由)和 default 表。此时不需要分离。
这两条路径保持了一致的路由查找语义,同时在没有自定义规则时通过合并表来优化性能。
相关文章: 这个设计体现了内核”按需分离”的思想——在不需要策略路由时保持简单高效,在需要时才承担额外的开销。
相关文章: