跳到主要内容

5.3 FIB 信息:一张路由条目的「身份证」

上一节我们说到,fib_lookup() 填好 fib_result 就算完成了它的历史使命。结果结构里最重要的那个指针——指向 fib_info——才是真正的大佬。

现在让我们把镜头拉近。如果说 fib_table 是整本路由册子,那 fib_info 就是册子里某一条具体的「路标」。它定义了数据包该怎么走:从哪个门(设备)出去、有多急(优先级)、谁规定的(协议)、能走多远(作用域)。

这是一张身份详尽的「身份证」。


struct fib_info:核心参数大起底

这个结构体在内核里就是路由条目的化身。我们可以把它看作是一个大号的参数集合,里面塞满了一个路由器做决策时需要的所有信息。

先上代码,感受一下它的「丰满」程度:

struct fib_info {
struct hlist_node fib_hash;
struct hlist_node fib_lhash;
struct net *fib_net;
int fib_treeref;
atomic_t fib_clntref;
unsigned int fib_flags;
unsigned char fib_dead;
unsigned char fib_protocol;
unsigned char fib_scope;
unsigned char fib_type;
__be32 fib_prefsrc;
u32 fib_priority;
u32 *fib_metrics;
#define fib_mtu fib_metrics[RTAX_MTU-1]
#define fib_window fib_metrics[RTAX_WINDOW-1]
#define fib_rtt fib_metrics[RTAX_RTT-1]
#define fib_advmss fib_metrics[RTAX_ADVMSS-1]
int fib_nhs;
#ifdef CONFIG_IP_ROUTE_MULTIPATH
int fib_power;
#endif
struct rcu_head rcu;
struct fib_nh fib_nh[0];
#define fib_dev fib_nh[0].nh_dev
};

一眼看过去全是字段,别慌。我们把它们拆成三类来看:生命周期属性特征性能指标


生命周期:生与死的指针

前面两个字段 fib_hashfib_lhash 是哈希表节点,为了让内核能快速找到这张「身份证」。接下来的几个字段则决定了这张卡什么时候该被回收。

  • fib_net:指向 Network Namespace。这东西让容器化成为可能,每个容器都有自己的网络栈,自然也都有自己的 fib_info 池子。
  • fib_treeref:这是一个引用计数,但它计数的是「有多少个 fib_alias 指着我」。记得上一节说的 fib_alias 吗?如果多条路由只是 TOS 或优先级不同,它们可以共享同一个 fib_info。每有一个这样的别名,这个计数就加 1。加的时候在 fib_create_info() 里做,减的时候在 fib_release_info() 里做。
  • fib_clntref:这是另一个引用计数,更像是「客户端引用」。同样是加在创建时,减在 fib_info_put() 里。如果减到 0 了,那就意味着没人用这个路由条目了,free_fib_info() 会被叫来清理现场。
  • fib_dead:这是个死亡标志位。如果你想释放一个 fib_info必须先把这位置 1。如果没置位就敢去调 free_fib_info(),内核会拒绝释放——它认为这东西还活着。这就像是给对象打了个「已注销」的标签,防止误删。

属性特征:谁、哪里、多远

接下来的这些字段定义了路由的本质。

fib_protocol:谁规定的?

fib_protocol 告诉内核这条路由是「谁」加进来的。这很关键,因为来源决定了信任度和处理方式。

如果你在用户态敲命令时不带任何修饰:

ip route add 192.168.1.0/24 dev eth0

内核默认认为这是 RTPROT_BOOT(启动时的配置)。

如果你特意加了修饰:

ip route add 192.168.1.0/24 dev eth0 proto static

这就成了 RTPROT_STATIC(管理员静态配置)。

除了这些,还有几位常客:

  • RTPROT_KERNEL:内核自己加的。比如本地回环路由(127.0.0.0/8),那是内核自启动时配置的,不劳你操心。
  • RTPROT_REDIRECT:这个在 IPv4 里很少见,主要是 IPv6 用的,表示这是由 ICMP Redirect 消息触发的路由。
  • RTPROT_RA:这是给 IPv6 路由器广播用的,别跟 Router Alert Option 搞混了。

当然,像 ZEBRA、XORP 这种高级路由守护进程加的路由,它们也有自己的标识(比如 RTPROT_XORP)。所有这些定义都在 include/uapi/linux/rtnetlink.h 里。


fib_scope:能管多宽?

Scope(作用域)是个关于「距离」的概念。它告诉我们这个目的地到底有多远,或者说能传多远。

你可以用 ip address showip route show 看到它们。主要有这么几种:

  • host (RT_SCOPE_HOST):就在本机。最典型的就是 127.0.0.1,不出网卡。
  • link (RT_SCOPE_LINK):就在这根线上。只有直接连在同一个交换机/同一条网线上的主机能收到。
  • global (RT_SCOPE_UNIVERSE):全球通用。这是绝大多数路由的默认配置,去哪都行。
  • site (RT_SCOPE_SITE):这是 IPv6 的地盘(第 8 章细说)。
  • nowhere (RT_SCOPE_NOWHERE):不存在。别去。

如果你加路由时不指定 scope,内核会按规矩办:

  1. 如果是经过网关的单播路由 → global
  2. 如果是直连的单播或广播 → link
  3. 如果是本地路由 → host

fib_type:路是通的还是堵的?

这是内核 3.7 之后加进 fib_info 的一个键。以前它只存在于 fib_alias 里,为了做区分才搬进来的。

最常见的类型是 RTN_UNICAST(正常的单播路由)。但有个有趣的类型叫 RTN_PROHIBIT

你可以这样加一条路障:

ip route add prohibit 192.168.1.17 from 192.168.2.103

这条命令的意思是:严禁从 103 去 17。如果有人非要从那走,内核不会静默丢包,它会礼貌地回复一个 ICMPv4 "Packet Filtered" (ICMP_PKT_FILTERED) 消息,告诉对方「此路不通」。

这个机制是怎么实现的?我们稍后讲 fib_props 的时候细说。


fib_prefsrc:偏心的源地址

有时候你想强制指定某个源地址,哪怕这不算是那条链路上的原生地址。fib_prefsrc 就是用来存这个「偏心眼」的选择。


fib_priority:谁说了算?

优先级,你可以叫它 Metric,也可以叫它 Preference。数字越小,优先级越高(0 是最高)。

你有三种方式在命令行里表达同一个意思(设优先级为 5):

ip route add 192.168.1.10 via 192.168.2.1 metric 5
ip route add 192.168.1.10 via 192.168.2.1 priority 5
ip route add 192.168.1.10 via 192.168.2.1 preference 5

这三个命令在内核眼里是一模一样的。而且千万别把它跟后面要说的 fib_metrics 搞混——虽然命令行里叫 metric,但那个字段跟 fib_metrics 数组里的复杂指标没半毛钱关系。


性能指标:MTU、RTT 和那一堆数组

fib_metrics 是个数组,存了 15 个(RTAX_MAX)与路径性能相关的参数。

为了让代码好看点(顺便偷懒),内核用了宏定义把数组里常用的几个位置重新取了名字:

  • fib_mtu:路径 MTU。
  • fib_window:TCP 窗口大小。
  • fib_rtt:往返时间。
  • fib_advmss:建议的 MSS。

这些指标初始化时会指向 dst_default_metrics(在 net/core/dst.c)。很多是 TCP 专用的,比如 initcwnd(初始拥塞窗口)。

你可以手动设这些玩意儿。比如想把初始拥塞窗口调大点:

ip route add 192.168.1.0/24 initcwnd 35

或者想锁死 MTU:

# 这里的 lock 很关键
ip route add 192.168.1.0/24 mtu lock 800

为什么要加 lock? 如果不加 lock,内核的 Path MTU Discovery 机制可能会在发现路径上 MTU 变小时,悄悄把你的 MTU 改了。一旦锁住,内核就会调用 dst_metric_locked() 检查,发现是锁定的,就直接返回,绝不动摇。

看看内核代码里的逻辑(net/ipv4/route.c):

static void __ip_rt_update_pmtu(struct rtable *rt, struct flowi4 *fl4, u32 mtu)
{
// ...
/* 只有没上锁的 MTU 才允许更新 */
if (dst_metric_locked(dst, RTAX_MTU))
return;
// ...
}

多路径路由:条条大路通罗马

fib_nhs:下一站有几个?

fib_nhs 记录的是 Number of Hops。在没开启 CONFIG_IP_ROUTE_MULTIPATH 的时候,它只能是 1。

开启多路径路由后,一条路由可以对应多个下一跳。这能带来冗余、负载均衡,甚至安全性提升。

fib_dev & fib_nh[0]:第一站是谁

fib_dev 只是个宏,它指向 fib_nh[0].nh_dev——也就是第一个下一跳对应的网卡设备。

如果配置了多路径,你可以在命令行里这样写:

ip route add default scope global nexthop dev eth0 nexthop dev eth1

这时候 fib_nh 就是个数组,里面挂了两个出口。内核会根据算法(比如哈希或者权重)把包扔到其中一个口上。


fib_props:让「禁止」落地有声

刚才我们提到了 RTN_PROHIBIT 这种类型,它会导致内核回送 ICMP "Packet Filtered"。

这个机制是靠一个叫 fib_props 的数组实现的。

这个数组定义在 net/ipv4/fib_semantics.c,有 12 个元素(RTN_MAX),每个元素对应一种路由类型。

结构体长这样:

struct fib_prop {
int error; /* 错误码 */
u8 scope; /* 作用域 */
};

对于正常的单播路由(RTN_UNICAST),error 是 0(没问题),scopeRT_SCOPE_UNIVERSE(全球通)。

但对于禁止路由(RTN_PROHIBIT),配置是这样的:

const struct fib_prop fib_props[RTN_MAX + 1] = {
// ...
[RTN_PROHIBIT] = {
.error = -EACCES, /* 拒绝访问 */
.scope = RT_SCOPE_UNIVERSE,
},
// ...
};

它是怎么工作的?

当你在 Rx 路径(收包路径)发送数据到 192.168.1.17,且恰好配了 prohibit 规则时,流程是这样的:

  1. fib_lookup() 开始查表。
  2. 在 FIB TRIE 里找到了匹配的叶子节点,调用 check_leaf() 方法。
  3. check_leaf() 拿到 fib_alias 的类型(fa_type),用它去查 fib_props 数组。
  4. 发现 .error-EACCES(非 0)。
  5. 错误码一路回传,最终 fib_lookup() 返回错误。
  6. 上层调用者 ip_error() 接收到这个 -EACCES,根据这个错误码,构造一个 ICMP Destination Unreachable 消息,代码设为 ICMP_PKT_FILTERED,扔回去,然后把包丢弃。

这就是为什么 ip route add prohibit 这种「软防火墙」能产生 ICMP 报文的原因——它是挂在路由查找路径上的。


Caching:结果也是可以存起来的

路由查找是个苦活儿,尤其是在路由表很大的时候。为了省点力气,内核会把查找结果缓存起来。

注意,这里说的缓存不是那个已经被移除的「IPv4 Routing Cache」(那是 3.6 之前的旧历史),而是基于 Next Hop 的缓存

缓存在哪?

缓存的地点就在 fib_nh(下一跳)结构体里。

  • Rx 路径(收包转发):结果缓存在 fib_nhnh_rth_input 字段里。
  • Tx 路径(本机发包):结果缓存在 fib_nhnh_pcpu_rth_output 字段里。

这两个字段本质上都是 rtable 结构体,里面包含了我们要的去向信息。

什么时候不缓存?

如果你用的是多路径路由,或者用了 Realms(流量归类),或者包不是单播的,内核就会谨慎起来,不直接缓存在 fib_nh 里,避免不同的规则串台。

谁负责缓存?

有个专门的函数叫 rt_cache_route()(定义在 net/ipv4/route.c),不管你是收包还是发包,都是它负责把 fib_result 塞进缓存槽位。

Per-CPU 的性能魔法

注意到 nh_pcpu_rth_output 这个名字里的 pcpu 了吗? 这意味着每个 CPU 都有自己的那份缓存

这是为了性能——多个 CPU 并发发包时,不需要去抢同一把锁,大家各玩各的,互不干扰。这也是为什么 Linux 在高吞吐下转发性能依然强劲的原因之一。


小结

这一节我们把 fib_info 这个「路由条目之母」的结构体翻了个底朝天。

从引用计数的生命周期管理,到 fib_protocolfib_scope 描述的路由属性;从 fib_metrics 里封装的 TCP 性能参数,到 fib_props 数组实现的路由拦截机制。

这其中有一个细节值得回味:内核是如何通过查表(fib_props)将路由类型映射到具体行为的。这种「数据驱动」的设计在内核里比比皆是——不是写死 if (type == PROHIBIT),而是去查一个配置数组。

现在我们手里有了完整的 fib_info,也有了基于下一跳的缓存。但还有一个关键角色我们只是匆匆带过——那就是 fib_nh(下一跳)本身。

下一节,我们深入到 fib_nh 结构体内部,去看看当内核决定把包发往「下一站」时,它手里握着的究竟是什么样的地址和设备。以及,那个叫 FIB Nexthop Exception 的机制是如何在不惊动全局路由表的情况下,偷偷修正某一条路径的 MTU 或下一跳的。