5.6 FIB Alias:当同一个目的地有了多个分身
上一节我们聊了 FIB Tables 和 fib_info 的通用结构。你可能觉得一切看起来都很完美——一条路由,一个 fib_info,记录网关、设备和度量,清晰明了。
但现实网络总喜欢给我们出一些边缘情况的难题。
想象这样一个场景:你要去一个目的地,比如 192.168.1.10。大部分情况下,你只关心能不能到。但在更精细的 QoS(服务质量)控制中,你可能希望根据数据包的 TOS(Type of Service) 字段来走不同的路——比如,把语音流量(TOS 低延迟)走一条昂贵的专线,而把文件下载流量(TOS 低成本)走一条普通的公网。
如果内核为每一条细微差异的路由都克隆一份完整的 fib_info(包含 nexthop、metrics 等所有大块头数据),内存会被浪费得很快。
Linux 内核的解决方案是 FIB Alias(fib_alias)。这是一个典型的「提取公因式」设计:把不变的胖结构体(fib_info)留着,把变化的瘦属性(TOS、优先级、类型)做成小钩子挂上去。
6.1 不仅仅是 TOS
原文开头提到了 TOS,但 Alias 的适用范围其实比那更广。当有几条路由条目指向完全相同的目的地(或者同一子网),并且它们经过相同的网关、使用相同的出接口,唯一的区别仅仅在于:
- TOS 值不同
- 优先级不同
- 路由类型不同(比如
RTN_UNICASTvsRTN_PROHIBIT)
这时候,创建一个新的 fib_info 就显得太奢侈了。我们只需要创建一个轻量级的 fib_alias,指向那个已经存在的 fib_info 即可。
让我们手动制造一组 Alias 看看。下面的命令创建了 3 条去往 192.168.1.10 的路由,网关都是 192.168.2.1,只有 TOS 字段不一样:
ip route add 192.168.1.10 via 192.168.2.1 tos 0x02
ip route add 192.168.1.10 via 192.168.2.1 tos 0x04
ip route add 192.168.1.10 via 192.168.2.1 tos 0x06
在内核眼中,这三条路由的「物理属性」(网关、设备)是完全一样的,没必要存三份。它们真正共享的那份 fib_info,就像是三兄弟共住的一套房;而 fib_alias,就是每个人手里的钥匙扣,上面刻着各自的个性化标识。
6.2 结构体:fib_alias
来看看 fib_alias 的结构定义。它非常小巧,这也正是它存在的意义——省内存:
struct fib_alias {
struct list_head fa_list; // 链表节点,挂载到 fib_node 的别名列表上
struct fib_info *fa_info; // 指向那个共享的 fib_info 对象
u8 fa_tos; // TOS 值,区分流量的关键
u8 fa_type; // 路由类型 (RTN_UNICAST, RTN_PROHIBIT 等)
u8 fa_state; // 状态标志
struct rcu_head rcu; // RCU 机制使用的回调头
};
这里有个历史遗留的小细节值得注意(算是给考古爱好者准备的):在早期的内核版本(2.6.39 之前),fib_alias 里面还有一个 fa_scope 字段。后来开发者们发现,scope 其实是路由的一个固有属性,不应该放在别名里区分,于是把它移到了 fib_info 结构体中。
6.3 图解共享机制
为了把这个关系印在脑子里,我们来看一张图(图 5-3)。
在这个场景中,我们有三个 fib_alias 对象。它们各自拿着不同的 fa_tos(比如 0x02, 0x04, 0x06),代表三条不同的路由策略。但是,请注意它们的 fa_info 指针——全部指向同一个 fib_info 对象。
这就像是三个操作系统线程(Alias)引用同一份物理内存页(fib_info)。为了防止这份「公共财产」被 premature free(提前释放),fib_info 内部维护了一个引用计数器 fib_treeref。
因为有三条路由在用它,图里的 fib_treeref 值显示为 3。如果你删除了其中一条路由(比如删掉了 tos 0x04 的规则),内核只会减少计数并释放对应的 fib_alias,而那个胖大的 fib_info 会继续赖在内存里,直到计数器归零。
6.4 内核内部:fib_table_insert()
光看结构不够劲爆,让我们钻进内核代码里,看看当你敲下那条 ip route add 命令时,内核是怎么处理这种共享的。
这一切都发生在 fib_table_insert() 方法里。这是路由条目添加的总入口。假设我们之前已经加好了第一条 TOS 0x02 的路由,现在正在添加第二条 TOS 0x04 的路由。
int fib_table_insert(struct fib_table *tb, struct fib_config *cfg)
{
struct trie *t = (struct trie *) tb->tb_data;
struct fib_alias *fa, *new_fa;
struct list_head *fa_head = NULL;
struct fib_info *fi;
// ... 后续逻辑展开 ...
}
第一步:试着找一个现成的 fib_info
代码首先会尝试创建(或查找)一个 fib_info。这一步非常关键,它是去重的逻辑核心。
fi = fib_create_info(cfg);
在 fib_create_info() 内部,内核会先分配一个 fib_info,然后马上调用 fib_find_info() 去哈希表里搜一搜:「哎?有没有一模一样的配置(网关、设备等都一致)的 fib_info 已经存在了?」
struct fib_info *fib_create_info(struct fib_config *cfg)
{
struct fib_info *fi = NULL;
struct fib_info *ofi;
// ...
fi = kzalloc(sizeof(*fi) + nhs*sizeof(struct fib_nh), GFP_KERNEL);
if (fi == NULL)
goto failure;
// ... 初始化 fi ...
link_it:
ofi = fib_find_info(fi); // 去哈希表里搜
那个 fib_find_info() 是个侦探。如果它找到了一个完全匹配的 ofi(old fib info),内核会觉得:「既然已经有了,那刚才新 kmalloc 出来的这个 fi 就是多余的垃圾了。」
于是,代码会把新的 fi 标记为死掉(fib_dead = 1),直接释放掉内存,然后把那个找到的 ofi 的引用计数 fib_treeref 加 1,最后把 ofi 返回给调用者。
if (ofi) {
fi->fib_dead = 1; // 标记新分配的那个为废弃
free_fib_info(fi); // 释放掉那个多余的
ofi->fib_treeref++; // 给那个幸存的老兵计数+1
return ofi; // 返回共享的对象
}
// ...
}
在这个例子中,因为第二条路由(TOS 0x04)的网关和设备跟第一条(TOS 0x02)完全一样,fib_find_info() 会成功命中。我们复用了同一个 fib_info。
第二步:检查是否需要新的 Alias
现在手里有了 fi(其实是复用的老 ofi),内核开始处理 TRffiE 插入逻辑。它要去相应的叶子节点挂载 Alias。
这里的关键是:虽然 fib_info 是一样的,但因为 TOS 不同,我们肯定需要一个新的 fib_alias 对象。内核通过 fib_find_alias() 来确认这一点:
l = fib_find_node(t, key);
fa = NULL;
if (l) {
fa_head = get_fa_head(l, plen);
// 在别名列表里找,看有没有一模一样的 TOS 和 Priority
fa = fib_find_alias(fa_head, tos, fi->fib_priority);
}
如果 fa 指针不为空,并且 TOS 和优先级都完全一致,那说明你在加一条完全重复的路由,内核可能会直接更新或者报错(取决于配置)。但在我们的场景里,tos 是 0x04,跟现有的 0x02 不一样,所以 fib_find_alias() 会返回 NULL,告诉内核:「这里没有同分身,你可以加一个新的。」
第三步:创建并挂载 Alias
最后,内核从 slab 分配器里拿一个新的 fib_alias,把它的 fa_info 指针指向那个共享的 fib_info,然后挂到链表上。
new_fa = kmem_cache_alloc(fn_alias_kmem, GFP_KERNEL);
if (new_fa == NULL)
goto out;
new_fa->fa_info = fi; // 指向那个共享的 fib_info
new_fa->fa_tos = tos; // 设置自己的独特属性
// ... 插入到 fa_list 链表中 ...
6.5 这一节的回响
到这里,FIB 的静态存储结构——从 Table 到 TRIE,从 fib_info 到 fib_alias——我们就全摸透了。
fib_table是骨架。fib_info是肌肉(存实体数据)。fib_alias是皮肤(存差异化属性)。
这个设计极其高效。当你配置了一个复杂的 BGP 路由表,里面有几万条路由,其中很多只是优先级不同时,这种共享机制能节省可观的内存。
但现在的路由还是静态的。
路由器不仅要会存路由,还要会「说话」。当它发现走弯路了,它会主动给邻居发消息:「嘿,别走我了,走那边那个网关更快。」这就是 ICMPv4 Redirect。
下一节,我们就来看看当路由发生「次优」情况时,内核是如何通过 ICMP Redirect 消息来动态修正路径的。