5.2 在路由子系统中执行查找
在上一节里,我们搞清楚了 FIB 是什么——它不是一张简单的表,而是内核手里那张决定数据包生死去处的「藏宝图」。图是有了,但还没人去读它。
数据包到了网卡,进了协议栈,内核这时候必须做一个决定:这货是给我的吗?是给别人转交的吗?还是直接扔进垃圾桶?
这个决定过程就是路由查找。
这是个高频操作——每一个数据包,无论你是收进来的还是发出去的,都要走这一遭。在内核 3.6 版本之前,为了抢时间,这个过程分两步走:先去翻「路由缓存」,没翻到再去翻「路由表」(FIB)。现在缓存机制废了,我们直接面对核心:fib_lookup()。
核心调用:fib_lookup()
这个函数是路由查找的大脑。它的任务很简单:拿着你给的线索(目标地址等),去 FIB 里翻,找到了就把结果填进一个结构体,然后挥手示意(返回 0)。
函数原型长这样:
int fib_lookup(struct net *net, const struct flowi4 *flp, struct fib_result *res);
这里有两个关键角色登场:线索提供者 flowi4 和 结果容器 fib_result。
线索:flowi4
flowi4 其实就是一张「查表申请单」。你不能两手空空去问内核路怎么走,你得填个表。
这张表里最重要的栏目包括:
- 目标地址
- 源地址
- 服务类型(TOS)
- ……(还有一些别的)
内核就是拿着这些字段作为「Key」去 FIB 里匹配。对于 IPv6,有个对应的 flowi6,都在 include/net/flow.h 里定义。
结果:fib_result
如果 fib_lookup() 顺利归来,它带回的 fib_result 结构体就是整个查找操作的战果。这个结构体里藏着下一步该干什么的所有线索。
查找流程是怎样的?
内核拿着你的 flowi4 申请单,先去翻 Local 表(看是不是找本机的)。如果没找到,再去翻 Main 表(看是不是要转发的)。只要在其中一张表里命中了,查找就算成功。
但这只是故事的一半。找到了路,还得有「脚」去走。
从 FIB 到路由缓存项:rtable 的诞生
查找成功后,无论收包还是发包,内核都会构建一个 dst_entry 对象(Destination Entry,目的缓存)。你可以把它理解为一张「路条」——上面写着「拿到了这个条子之后,下一步去找谁」。
这个 dst_entry 通常是嵌入在一个更大的结构体 rtable 里的。rtable 才是那个真正能挂在数据包(SKB)身上的路由条目。
先看 dst_entry 的核心:
struct dst_entry {
...
int (*input)(struct sk_buff *);
int (*output)(struct sk_buff *);
...
};
注意这两个函数指针:input 和 output。
这就是我们跑完 fib_lookup() 之后真正要拿到的东西。根据查找结果的不同,这两个钩子会被挂上不同的处理函数。数据包就像个傻瓜,拿着路条,直接调用这两个函数,剩下的路就自动走完了。
再看外面的壳子——rtable:
struct rtable {
struct dst_entry dst;
int rt_genid;
unsigned int rt_flags;
__u16 rt_type;
__u8 rt_is_input;
__u8 rt_uses_gateway;
int rt_iif;
/* Info on neighbour */
__be32 rt_gateway;
/* Miscellaneous cached information */
u32 rt_pmtu;
struct list_head rt_uncached;
};
这里面的每个字段都有讲究,我们拆开来看。
rt_flags:路条上的特别批注
这是标志位集合,用来告诉内核这条路由有点「特殊情况」。常见的有:
- RTCF_BROADCAST:目标是广播地址。
ip_route_input_slow()和__mkroute_output()会设这个。 - RTCF_MULTICAST:目标是组播地址。
ip_route_input_mc()会设这个。 - RTCF_DOREDIRECT:这个很关键。设了这个标志,意味着内核应该给发送方回一个 ICMP Redirect 消息,告诉它「路走错了,有条近道」。
- 触发条件很严苛:输入设备和输出设备必须是同一个,而且 Procfs 里开启
send_redirects。这是__mkroute_input()里设的。
- 触发条件很严苛:输入设备和输出设备必须是同一个,而且 Procfs 里开启
- RTCF_LOCAL:目标是本地。设了这个,包就得往上层协议栈送,而不是转发。
其他关键字段
- rt_is_input:如果是 1,说明这是「进来的路由」;0 就是出去的。
- rt_uses_gateway:
- 如果是 1:下一跳是个网关。
- 如果是 0:直连路由。
- rt_iif:输入接口的索引。这是包是从哪个口进来的。
- rt_gateway:下一跳网关的 IP 地址。
- rt_pmtu:路径 MTU。这是为了防止包在半路被「分尸」,缓存一下路上最小的 MTU 值。
一个小插曲:在内核 3.6 之前,这里还有个 rt_spec_dst 字段,后来因为 fib_compute_spec_dst() 方法的加入,它变得多余被移除了。这主要是在处理 ICMP 回复这种特殊情况时用的——回复的时候得把源地址当目标地址用。
命运的分叉口:input/output 回调
rtable 构建好了之后,dst.input 和 dst.output 这两个钩子会被赋予真正的使命。这就是路由查找的最终目的:决定函数调用链。
- 进来的包:
- 如果目标是本机:
input→ip_local_deliver()。 - 如果需要转发:
input→ip_forward()。
- 如果目标是本机:
- 出去的包:
- 如果是本机发出:
output→ip_output()。
- 如果是本机发出:
- 组播包:
input→ip_mr_input()(特定条件下)。
- 错误处理:
- 如果是
RTN_PROHIBIT(禁止路由):input→ip_error()。
- 如果是
你看,这比你看 ip route 列表要直观得多——在代码里,路由选择就是「选函数」。
深挖 fib_result
上面说了 fib_lookup() 会把结果填进 fib_result。现在我们来拆这个结构体,它里面装的是查找过程中的原始数据,还没被加工成 rtable。
struct fib_result {
unsigned char prefixlen;
unsigned char nh_sel;
unsigned char type;
unsigned char scope;
u32 tclassid;
struct fib_info *fi;
struct fib_table *table;
struct list_head *fa_head;
};
- prefixlen:子网前缀长度(也就是 netmask)。
- 范围 0~32。如果是默认路由(0.0.0.0/0),它就是 0。
- 这是在
check_leaf()方法里确定的。
- nh_sel:下一跳的选择器。
- 如果是单路径路由,就是 0。
- 如果启用了 Multipath Routing(多路径路由),可能有好几个下一跳,这个值就是选中的那个索引号。
- type:这是最重要的字段。它直接决定了包的命运。
RTN_UNICAST:普通的单播,转发或者直连。RTN_LOCAL:发往本机。RTN_BROADCAST:广播。RTN_MULTICAST:组播。RTN_UNREACHABLE:不可达,会触发 ICMP 目标不可达消息。- ……(总共有 12 种类型)。
- scope:路由的作用域(距离)。
- fi (fib_info):指向路由条目核心信息的指针。这里面包含了真正的下一跳信息(
fib_nh)。我们下一节会细聊这个。 - table:指向查找发生的 FIB 表(Local 还是 Main)。
- fa_head:指向
fib_alias列表。- 这是个优化手段。如果好几条路由除了 TOS 或优先级不一样,其他都一样,它们可以共享同一个
fib_info,通过fib_alias链表挂在一起。
- 这是个优化手段。如果好几条路由除了 TOS 或优先级不一样,其他都一样,它们可以共享同一个
小结
这一节我们像剥洋葱一样,剥开了路由查找的内核。
从用户空间的 ip route 命令,到内核里的一行行代码,中间跨越的不仅仅是数据结构,而是「决策」与「执行」的分界线。fib_lookup() 是决策者,它填好 fib_result;而 rtable 及其回调函数是执行者,它们拿到指令,推着数据包往前走。
但这里留了个大悬念:fib_result 里那个指向 fib_info 的指针到底藏着什么?为什么下一跳信息要单独放在一个结构里?下一节,我们深入到 FIB 的更底层,去看看那张被频繁引用的「路由表」到底是怎么组织的。