fib_unmerge:把local路由从main表里切割出来

fib_unmerge 是内核路由子系统中一个精巧的按需优化设计:

  • 默认状态下,local 表只是 main 表的别名,所有路由合并在一张表里,fib_lookup 只需查一次表,性能最优
  • 添加策略路由时fib_unmerge 将 local 路由从 main 表中切割出来,放入独立的 local 表,确保策略路由能正确匹配
  • 切割四步走:创建新 local 表 → 复制 local 路由 → 替换旧表 → 清理 main 表中的残留条目
  • 幂等设计:只在首次添加策略路由时执行分离,后续再添加时 fib_trie_unmerge 检测到已分离,直接返回

本文将从调用点入手,逐步分析 fib_unmerge 的实现细节。

背景

前面文章提到,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_lookupfib_rules_lookup,会按策略路由顺序依次查找 local/main/default 三张表。如果 local 路由还在 main 表里,那 local 表就是空的,查找 local 表就没有意义了。

所以,只有在用户添加了自定义策略路由规则后,local 路由才需要从 main 表里分离出来,放到独立的 local 表里

函数 fib_unmerge 就是做这个事情的。

调用点

fib_unmerge 的调用点有两个:

  • fib4_rule_configure:添加策略路由
  • fib4_rule_delete:删除策略路由

这两个函数是 IPv4 策略路由模版 fib4_rules_ops_template 里的 configuredelete 函数定义。

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 /* split local/main if they are not already split */
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 表分离开。

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;

/* attempt to fetch the local table */
old = fib_get_table(net, RT_TABLE_LOCAL);
if (!old)
return 0;

/* create new local table with routes from old */
new = fib_trie_unmerge(old);
if (!new)
return -ENOMEM;

/* if they are the same, no unmerging needed */
if (new == old)
return 0;

/* replace old local table with new one */
fib_replace_table(net, old, new);
fib_free_table(old);

/* flush local entries that were merged into main */
main_table = fib_get_table(net, RT_TABLE_MAIN);
if (main_table)
fib_table_flush_external(main_table);

return 0;
}

fib_unmerge 的逻辑可以分为四步:

第一步:获取旧的 local 表

1
old = fib_get_table(net, RT_TABLE_LOCAL);

获取当前的 local 路由表。如果不存在,说明不需要分离,直接返回。

第二步:创建新的 local 表

1
new = fib_trie_unmerge(old);

fib_trie_unmerge 从旧的(合并在 main 表里的)local 表中,把 local 路由复制到一张新的独立 local 表里。

如果返回的 new 和 old 相同,说明已经分离过了(不是第一次添加策略路由),直接返回。

第三步:替换旧表

1
2
fib_replace_table(net, old, new);
fib_free_table(old);

用新创建的 local 表替换掉旧的(别名到 main 的)local 表,并释放旧表。

第四步:清理 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 表中的外部引用。

流程总结

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路由)

与 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 表。此时不需要分离。

这两条路径保持了一致的路由查找语义,同时在没有自定义规则时通过合并表来优化性能。

相关文章:
这个设计体现了内核”按需分离”的思想——在不需要策略路由时保持简单高效,在需要时才承担额外的开销。

相关文章: