跳到主要内容

2.5 增删路由表项:在 FIB 里跳舞

上一节我们拆解了 Netlink 消息的头部和那些让人眼花缭乱的标志位。现在,是时候把这套理论扔进实战环境里了。

我们要看的是一个非常具体且高频的操作:向内核的路由表(FIB)里塞一条路由,或者删掉它。

这是一个极佳的观察样本,因为它完整地展示了用户空间的一条命令,是如何通过 Netlink 这根线,一路拽着内核的 FIB 子系统满地跑的。而且,这里还藏着一个 Netlink 的杀手级特性——事件广播。


幕后推手:从 ip 命令到内核回调

让我们先看增加路由。

你在命令行敲下这一行:

ip route add 192.168.2.11 via 192.168.2.20

这行命令并没有直接去修改内存。它做的事情非常纯粹:在用户空间构建了一个 RTM_NEWROUTE 类型的 Netlink 消息,然后通过一个 AF_NETLINK 套接字,把它扔进了内核。

消息一旦越过边界,内核的 rtnetlink 机制就开始转动了。

  1. 接收:内核的 rtnetlink socket 把这条消息收进来,交给 rtnetlink_rcv() 方法。这是整个路由子系统的总入口。
  2. 分发rtnetlink_rcv() 看到消息类型是 RTM_NEWROUTE,就会去找谁来处理这件事。
  3. 执行:最终,执行权被交到了 net/ipv4/fib_frontend.c 里的 inet_rtm_newroute() 函数手上。这是真正干脏活累活的地方。

inet_rtm_newroute() 并不只是简单地在数组里插一行——它得去操作 FIB(Forwarding Information Base,转发信息库)。这东西是内核路由决策的「圣经」,任何一点错误都会导致网络包要么送不出去,要么送去地狱。它会调用 fib_table_insert() 把这条路由真正写入硬件无关的路由表数据库里。

广播这件事:别藏着掖着

如果事情到这里就结束,那 Netlink 只能算是个「高级 IOCTL」。

但 Netlink 不一样。fib_table_insert() 在把路由写进数据库之后,还要做另一件事——喊一嗓子

它会调用 rtmsg_fib() 方法,并传入 RTM_NEWROUTE 参数。rtmsg_fib() 的任务不是操作路由,而是构建一个新的 Netlink 通知消息,然后调用 rtnl_notify()

这一声「喊」,是发给 RTNLGRP_IPV4_ROUTE 这个多播组的。

这就像是在一个嘈杂的房间里,有人喊了一句:「嘿,大家注意,刚才加了一条 192.168.2.11 的路由!」。所有「订阅」了这个频道的人都能听到。

  • iproute2 套件里的工具在听。
  • 用户空间的高级路由守护进程(像 xorp 或者 bird)也在听。
  • 甚至内核里的其他模块,如果也注册了这个 group,同样能听到。

这种「一边干活一边广播」的设计,是现代 Linux 网络管理的基础。它意味着用户空间的守护进程不需要轮询内核去查表变更,内核会主动推送。


删除路由:同样的剧本,不同的结局

删除操作几乎是一模一样的流程,只是换了几个名词。

你执行:

ip route del 192.168.2.11

这次,ip 命令生成的是 RTM_DELROUTE 消息。

  1. 消息飞进内核,再次命中 rtnetlink_rcv()
  2. 这次它被分发到 inet_rtm_delroute() 回调函数(同样在 fib_frontend.c 里)。
  3. inet_rtm_delroute() 调用 fib_table_delete() 从 FIB 中抹去这条记录。
  4. 同样,这里会调用 rtmsg_fib(),只不过这次传入的是 RTM_DELROUTE
  5. rtnl_notify() 再次出发,向 RTNLGRP_IPV4_ROUTE 广播:「嘿,大家注意,那条路由刚才删掉了。」

你会发现,内核在处理「增」和「删」的时候,逻辑是对称的,侧重点都是:既要完成实际的数据结构操作,也要确保订阅者收到通知。


实战验证:监听内核的心跳

光说不练是假把式。让我们来亲眼看看这个广播机制。

我们需要两个终端。这就好比你要验证房间里有没有人喊话,最好的办法就是自己带个收音机进去。

终端 1:开启监听

ip monitor route

这个命令启动了一个后台守护进程。它做了什么?它打开了一个 Netlink socket,并且执行了 setsockopt() 来加入 RTNLGRP_IPV4_ROUTE 多播组。现在,它正竖着耳朵等消息呢。

终端 2:制造事件

现在我们在第二个终端里,随便加一条路由:

ip route add 192.168.1.10 via 192.168.2.200

这一瞬间,ip 命令发请求,内核处理请求,然后内核广播通知。

回到终端 1,你应该会立即看到一行输出:

192.168.1.10 via 192.168.2.200 dev em1

看到了吗?这不是你敲的命令,这是内核广播回来的通知。你的监听进程收到了内核的 rtnl_notify(),并把它打印了出来。

接下来,在终端 2 删掉它:

ip route del 192.168.1.10

终端 1 会立刻显示:

Deleted 192.168.1.10 via 192.168.2.200 dev em1

注意那个 Deleted 前缀——这是 ip monitor 的友善提示,它识别出了广播消息类型是 RTM_DELROUTE

更多的频道:链路与 VLAN

路由不是唯一能被广播的东西。rtnetlink 定义了各种各样的多播组。

你可以试试监听「链路」层的事件。在终端 1 执行:

ip monitor link

这会让 Netlink socket 加入 RTNLGRP_LINK 组。只要网卡接口状态发生任何变化——UP/DOWN、新增 VLAN、添加 Bridge——你都能抓到。

现在去终端 2 试着加一个 VLAN 接口:

vconfig add eth1 200

终端 1 会瞬间刷出一大堆信息:

4: eth1.200@eth1: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN
link/ether 00:e0:4c:53:44:58 brd ff:ff:ff:ff:ff:ff

再加一个网桥试试:

brctl addbr mybr

终端 1 继续跟进:

5: mybr: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN
link/ether a2:7c:be:62:b5:b6 brd ff:ff:ff:ff:ff:ff

这就是为什么像 NetworkManager 这种工具能反应那么快——它们根本不需要去轮询 /sys 下的文件,它们只是静静地坐在 Netlink socket 旁边,等着内核告诉它们发生了什么。


本章小结

走到这里,你已经看透了 Netlink Sockets 的全套把戏:

  1. 它是一个基于 Socket 的标准 IPC 通道,取代了老旧的 IOCTL。
  2. 它有严格的消息格式(头部 + TLV 属性),能承载复杂的网络配置。
  3. 最重要的是,它是多播的。内核不只是被动接收命令,它还会主动向订阅者广播网络拓扑的变更。

这套机制构成了现代 Linux 网络管理的基石。无论是 ip 命令,还是底层的 rtnetlink,本质上都在玩这一套规则。

但是,有一个问题。

如果你是内核开发者,你想给自己写的某个非标准内核模块加一个 Netlink 接口,你会怎么做?rtnetlink 是给网络子系统用的,你不能把你的随机硬件控制消息硬塞进 RTM_NEWROUTE 这种类型里。

32 个标准 Netlink 协议族的位置其实是很紧缺的。为了解决这个「地址空间危机」,内核后来引入了扩展机制。

下一节,我们要谈谈 Generic Netlink——这是为了解决「插座不够用了」这个问题而设计的终极多路复用方案。