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 机制就开始转动了。
- 接收:内核的
rtnetlinksocket 把这条消息收进来,交给rtnetlink_rcv()方法。这是整个路由子系统的总入口。 - 分发:
rtnetlink_rcv()看到消息类型是RTM_NEWROUTE,就会去找谁来处理这件事。 - 执行:最终,执行权被交到了
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 消息。
- 消息飞进内核,再次命中
rtnetlink_rcv()。 - 这次它被分发到
inet_rtm_delroute()回调函数(同样在fib_frontend.c里)。 inet_rtm_delroute()调用fib_table_delete()从 FIB 中抹去这条记录。- 同样,这里会调用
rtmsg_fib(),只不过这次传入的是RTM_DELROUTE。 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 的全套把戏:
- 它是一个基于 Socket 的标准 IPC 通道,取代了老旧的 IOCTL。
- 它有严格的消息格式(头部 + TLV 属性),能承载复杂的网络配置。
- 最重要的是,它是多播的。内核不只是被动接收命令,它还会主动向订阅者广播网络拓扑的变更。
这套机制构成了现代 Linux 网络管理的基石。无论是 ip 命令,还是底层的 rtnetlink,本质上都在玩这一套规则。
但是,有一个问题。
如果你是内核开发者,你想给自己写的某个非标准内核模块加一个 Netlink 接口,你会怎么做?rtnetlink 是给网络子系统用的,你不能把你的随机硬件控制消息硬塞进 RTM_NEWROUTE 这种类型里。
32 个标准 Netlink 协议族的位置其实是很紧缺的。为了解决这个「地址空间危机」,内核后来引入了扩展机制。
下一节,我们要谈谈 Generic Netlink——这是为了解决「插座不够用了」这个问题而设计的终极多路复用方案。