跳到主要内容

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 *);
...
};

注意这两个函数指针:inputoutput。 这就是我们跑完 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() 里设的。
  • 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.inputdst.output 这两个钩子会被赋予真正的使命。这就是路由查找的最终目的:决定函数调用链。

  • 进来的包
    • 如果目标是本机inputip_local_deliver()
    • 如果需要转发inputip_forward()
  • 出去的包
    • 如果是本机发出outputip_output()
  • 组播包
    • inputip_mr_input()(特定条件下)。
  • 错误处理
    • 如果是 RTN_PROHIBIT(禁止路由):inputip_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 链表挂在一起。

小结

这一节我们像剥洋葱一样,剥开了路由查找的内核。

从用户空间的 ip route 命令,到内核里的一行行代码,中间跨越的不仅仅是数据结构,而是「决策」与「执行」的分界线。fib_lookup() 是决策者,它填好 fib_result;而 rtable 及其回调函数是执行者,它们拿到指令,推着数据包往前走。

但这里留了个大悬念:fib_result 里那个指向 fib_info 的指针到底藏着什么?为什么下一跳信息要单独放在一个结构里?下一节,我们深入到 FIB 的更底层,去看看那张被频繁引用的「路由表」到底是怎么组织的。