跳到主要内容

6.9 最后的备忘单

我们终于走到了这一章的结尾。

这一节没有新的故事,也没有新的概念。它更像是一本探险日记最后的附录——当你真正开始动笔写代码或者调试奇怪的网络问题时,你会回头翻这一页。

这里列出了我们在本章折腾过的核心方法、几个关键的宏,以及那些在 /proc 下窥探内核状态的入口。


核心方法速查

首先是我们接触过的重要内核方法。按照它们在组播和策略路由中的角色,我把它们分了类。

注意: 这一节的代码签名都很重要,如果你在写内核模块或者调试驱动,参数类型错一个都会导致编译失败。

组播路由守护进程的通道

这部分是用户空间的 mroutedpimd 与内核对话的接口。

int ip_mroute_setsockopt(struct sock *sk, int optname, char __user *optval, unsigned int optlen);

这是内核与组播路由守护进程的「电话听筒」。守护进程通过 setsockopt() 调用它,下达各种指令。 支持的命令(optname)包括:

  • MRT_INIT:初始化组播转发。
  • MRT_DONE:停止并清理。
  • MRT_ADD_VIF / MRT_DEL_VIF:添加或删除虚拟接口。
  • MRT_ADD_MFC / MRT_DEL_MFC:添加或删除转发缓存条目。
  • MRT_ASSERT:断言相关。
  • MRT_PIM:如果内核开启了 PIM 支持。
  • MRT_TABLE:如果开启了组播策略路由(多表支持)。
int ip_mroute_getsockopt(struct sock *sk, int optname, char __user *optval, int __user *optlen);

对应的上行通道。守护进程用它来查询状态,比如 MRT_VERSIONMRT_ASSERTMRT_PIM 的当前状态。

组播路由表的生命周期

struct mr_table *ipmr_new_table(struct net *net, u32 id);

创建一个新的组播路由表。如果你在做策略路由,可能会用到这个。参数 id 就是指定表的 ID。

void ipmr_free_table(struct mr_table *mrt);

释放指定组播路由表及其占用的所有资源。别忘了,内核不会自动帮你做所有清理工作。

主机侧:加入与离开

int ip_mc_join_group(struct sock *sk, struct ip_mreqn *imr);

这不是给路由器用的,是给主机用的。 当你想要加入一个组播组时调用这个方法。组播组的地址填在 imr 参数里。成功返回 0。

核心查找与转发逻辑

static struct mfc_cache *ipmr_cache_find(struct mr_table *mrt, __be32 origin, __be32 mcastgrp);

我们在「IPv4 Multicast Rx Path」一节里见过它。 它在 IPv4 组播路由缓存(MFC)里查找条目。键值是源地址(origin)和组播组地址(mcastgrp)。找不到就返回 NULL。

bool ipv4_is_multicast(__be32 addr);

一个简单的判断工具:这个 IP 地址是不是组播地址?(也就是是不是 D 类地址)。

int ip_mr_input(struct sk_buff *skb);

这是组播数据包接收路径的「主函数」(位于 net/ipv4/ipmr.c)。所有的组播包进来,如果不是给本机的,都要经过它来决定怎么转发。

内存管理与缓存构建

struct mfc_cache *ipmr_cache_alloc(void);

分配一个普通的组播转发缓存条目(mfc_cache)。

static struct mfc_cache *ipmr_cache_alloc_unres(void);

专门用于分配「未解析」条目。 回忆一下:当内核收到一个组播包,但缓存里还没有对应的路由时,它会先创建一个未解析条目,并设置过期时间,然后等用户空间守护进程来填坑。

static int ipmr_mfc_add(struct net *net, struct mr_table *mrt, struct mfcctl *mfc, int mrtsock, int parent);

这是 MRT_ADD_MFC 命令的内核实现。它把用户空间传过来的 mfcctl 结构体转换成内核里的缓存条目。

static int ipmr_mfc_delete(struct mr_table *mrt, struct mfcctl *mfc, int parent);

同理,这是 MRT_DEL_MFC 的内核实现。

虚拟接口 (VIF) 管理

static int vif_add(struct net *net, struct mr_table *mrt, struct vifctl *vifc, int mrtsock);

将一个物理网卡或隧道注册为一个虚拟组播接口(VIF)。对应 MRT_ADD_VIF

static int vif_delete(struct mr_table *mrt, int vifi, int notify, struct list_head *head);

删除 VIF。对应 MRT_DEL_VIF

int dev_set_allmulti(struct net_device *dev, int inc);

关键函数。 还记得我们要网卡接收所有组播包吗?就是这个函数实现的。它修改网卡的 allmulti 计数器。inc 为正数时加一,负数时减一。

通知与维护

static int ipmr_cache_report(struct mr_table *mrt, struct sk_buff *pkt, vifi_t vifi, int assert);

当内核遇到搞不定的包(比如未解析的 NOCACHE,或者从错误接口进来的 WRONGVIF),它会调用这个函数。 它会构建一个特殊的 IGMP 消息包,塞进队列,通过 sock_queue_rcv_skb() 发给用户空间的守护进程。

static int ipmr_device_event(struct notifier_block *this, unsigned long event, void *ptr);

这是一个热插拔事件的回调。 如果你拔掉了网卡,内核会触发 NETDEV_UNREGISTER 事件。这个回调函数会收到通知,然后去 vif_table 里把那个网卡对应的 VIF 删掉——防止内核去访问一个已经不存在的设备。

static void mrtsock_destruct(struct sock *sk);

当守护进程调用 setsockopt(MRT_DONE) 关门走人时,这个析构函数会被调用。 它做清理工作:把 mroute_sk 指针清零,把 /proc/sys/net/ipv4/conf/all/mc_forwarding 减 1,并调用 mroute_clean_tables() 把桌子擦干净。

static void ipmr_expire_process(unsigned long arg);

定时器回调。 如果 ipmr_cache_report() 发出了求救信号,但守护进程一直没回应(没添加路由),这个定时器到期后就会把那个未解析的条目删掉——内核不做老好人。

协议分发

int igmp_rcv(struct sk_buff *skb);

IGMP 协议的接收处理函数。虽然这不直接属于路由,但它是维持组播成员关系的基础。

策略路由与多路径

void fib_select_multipath(struct fib_result *res);

在多路径路由(ECMP)场景下使用。 当查路由发现有好几条路可以走时,这个函数会根据权重算法(哈希或轮询)从中挑出一条具体的下一跳。


关键宏

这里有两个我们在代码里见过但没细说的宏,它们是实现细节的关键。

MFC_HASH(a, b)

#define MFC_HASH(a, b) ...

MFC (Multicast Forwarding Cache) 的哈希算法。 参数 a 是组播组地址,b 是源地址。 内核不是线性遍历缓存表的(那样太慢了),而是用这个宏算出一个索引,直接跳到对应的位置去查。

VIF_EXISTS(_mrt, _idx)

#define VIF_EXISTS(_mrt, _idx) ...

边界检查宏。 用来判断索引为 _idx 的虚拟接口是否在路由表 _mrtvif_table 数组中真实存在。 这就像你去酒店找房间,得先确认这个房间号真的存在,而不是直接往里闯。


/proc 窥视口

当你在调试组播问题时,除了抓包,这几个 /proc 文件是你最好的朋友。它们直接反映了内核此刻脑子里的状态。

/proc/net/ip_mr_vif

虚拟接口列表。

读这个文件,你就能看到内核当前注册了哪些虚拟组播接口。 它背后的实现函数是 ipmr_vif_seq_show()

你会看到什么:

  • 每个接口的索引。
  • 它绑定的物理设备。
  • 那些统计计数器(收了多少包,发错了多少包)。

/proc/net/ip_mr_cache

MFC 转发缓存的状态。

这是最能反映路由逻辑是否正确的地方。它背后的实现函数是 ipmr_mfc_seq_show()

你会看到什么: 每一个缓存条目的详细字段:

  • mfc_mcastgrp:组播组地址。
  • mfc_origin:源地址。
  • mfc_parent:入接口索引(包是从哪个接口进来的)。
  • pkt / bytes:转发统计(转发了多少包,多少字节)。
  • wrong_if:错误接口统计(有多少包走错了门)。
  • ttls:转发接口列表及其 TTL 阈值。

策略路由选择器表 (Table 6-1)

最后,这张表是你在配置策略路由(ip rule)时常用的匹配字段,以及它们在内核代码里对应的符号。

这一块最好打印出来贴在显示器边上,因为当你写复杂的 ip rule add 命令时,很容易忘掉某个字段对应的 FRA 常量是什么。

Linux 符号路由命令关键字描述结构体成员
FRA_SRCfrom匹配源地址fib4_rule->src
FRA_DSTto匹配目的地址fib4_rule->dst
FRA_IIFNAMEiif匹配入接口名称fib_rule->iifname
FRA_OIFNAMEoif匹配出接口名称fib_rule->oifname
FRA_FWMARKfwmark匹配防火墙标记fib_rule->mark
FRA_FWMASKfwmask匹配防火墙标记掩码fib_rule->mark_mask
FRA_PRIORITYpriority / preference / order规则的优先级fib_rule->pref
(无对应符号)tos / dsfield服务类型fib4_rule->tos

本章回响

到这里,这一章的旅程就结束了。

我们从一个简单的问题开始——怎么把数据包发给一群人?——结果我们挖开了 Linux 内核最复杂的一块:路由子系统。

这一章表面上是在讲「高级路由」,其实是在讲控制权

  • 组播路由把控制权从简单的「单线传输」交给了复杂的「树状分发」,迫使内核区分「这是给谁的」和「谁应该转发它」。
  • 策略路由把控制权从「按目的地寻址」交给了「按管理员意愿寻址」,让同一个包可以因为它的来源、打标甚至入接口的不同,而走向截然不同的道路。

还记得我们在 IGMP 和组播路由表那里打过的交道吗?那些看起来晦涩的数据结构——mfc_cachevif_tablefib4_rule——它们都是为了在保持内核高性能的同时,容纳这种灵活性而设计的。

当你下次在命令行敲下 ip route add 或者 ip maddr add 时,你应该能想象出这行字背后,内核在哈希表里翻找了什么,在网络设备的引用计数上加了什么。

下一章,我们将跨过传输层,去看看更深的东西——或者,也许是更基础的东西:网络底层的那些基石。

准备好了吗?深吸一口气。


练习题

练习 1:understanding

题目:在 Linux 内核的组播路由实现中,当组播路由守护进程通过 setsockopt() 发送 MRT_INIT 命令来初始化组播路由时,内核的 mr_table 结构体中的哪个成员会被初始化为指向该用户空间套接字?该操作还会导致哪个只读的 procfs 条目被自动设置为启用状态?

答案与解析

答案:mroute_sk;/proc/sys/net/ipv4/conf/all/mc_forwarding

解析:根据源码分析,当内核处理 MRT_INIT 命令时(ip_mroute_setsockopt 方法),它会保留一个指向用户空间套接字的引用,并将其存储在 mr_table 结构体的 mroute_sk 成员中。这是内核与用户空间守护进程(如 pimd 或 mrouted)通信的基础。同时,内核会通过 IPV4_DEVCONF_ALL(net, MC_FORWARDING)++ 将 mc_forwarding 计数器加 1,从而在 /proc 文件系统中反映组播转发已启用。注意该 procfs 条目是只读的,无法直接通过用户空间写入修改。

练习 2:application

题目:假设一个组播数据包到达了被配置为组播路由器的 Linux 主机。如果在组播转发缓存(MFC)中查找条目失败(cache miss),内核会调用 ipmr_cache_unresolved() 方法。请问:1)内核会将该数据包缓存多少个?2)如果未解析队列已满,内核会向用户空间守护进程发送什么消息来请求解析路由?

答案与解析

答案:最多缓存 3 个(SKB);IGMPMSG_NOCACHE

解析:在 ip_mr_cache_unresolved() 的实现中,内核检查 c->mfc_un.unres.unresolved.qlen 是否大于 3。如果未超过 3,数据包会被加入未解析队列;否则,数据包会被丢弃(kfree_skb)并返回 -ENOBUFS。当未解析条目被创建时,内核会调用 ipmr_cache_report() 方法,该方法构建一个 IGMPMSG_NOCACHE 消息并通过 sock_queue_rcv_skb 发送给用户空间的组播路由守护进程,通知其有新的组播源需要建立路由条目。

练习 3:application

题目:在多路径路由的场景下,Linux 内核使用 fib_select_multipath() 函数来决定将数据包发送到哪个下一跳。假设你配置了一条带有两个下一跳的路由,第一个下一跳的权重 为 2,第二个下一跳的权重 为 1。如果此时有 300 个数据包需要通过该路由转发,理论上大概会有多少个数据包被发送到第二个下一跳?这种机制在内核中主要是为了解决什么问题?

答案与解析

答案:约 100 个(负载均衡);网络带宽利用率和链路冗余

解析:多路径路由根据权重(weight)进行哈希加权负载均衡。权重为 2 和 1 的比例为 2:1,因此总权重为 3。理论上,2/3 的流量(约 200 个包)会走第一跳,1/3 的流量(约 100 个包)会走第二跳。内核通过 fib_select_multipath() 结合哈希算法来实现这一分发,旨在充分利用多条链路的带宽,并提供链路冗余备份,防止单点故障。

练习 4:thinking

题目:为什么 IGMPv3 的“源过滤”功能对于构建高效的大型组播网络至关重要?请结合 Linux 内核处理组播路由的机制(特别是 MFC 缓存和内核与用户空间守护进程的交互),分析如果主机只能通过 IGMPv2 加入组播组,可能会对网络带宽和路由器性能产生什么影响?

答案与解析

答案:IGMPv2 缺乏源过滤会导致不必要的流量泛洪和核心路由器负担加重。

解析:IGMPv2 仅允许主机声明“我要加入组 G”,无法指定源 S。这意味着路由器必须将来自任意源发往 G 的流量都转发给该主机。在内核层面,MFC 条目是基于 (S, G) 查找的。如果没有源过滤,组播路由守护进程可能会倾向于建立 (*, G) 类型的条目,导致大量来自无关源的数据包被转发到接收端,浪费带宽。而 IGMPv3 允许主机指定 INCLUDE 或 EXCLUDE 列表,使得路由守护进程可以精确地在内核中建立特定源 (S, G) 的 MFC 条目。这不仅减少了不必要的数据包复制和转发(减轻 ip_mr_forward 的负载),也减少了上游链路的带宽占用。


要点提炼

组播机制的核心在于解决“一对多”通信的效率问题,它通过让路由器智能地复制和分发数据包,避免了为每个接收者单独建立传输通道所带来的带宽灾难。这就要求网络基础设施必须具备管理动态成员身份的能力,主机通过 IGMP 协议与路由器协商“加入”或“离开”组,从而确保组播流量仅被路由到真正感兴趣的接收者所在的网段。

Linux 内核通过 IGMP 协议栈实现组成员管理的逻辑进化,从 v1 的简单查询与报告,发展到 v2 引入主动离开消息以优化退出效率,再到 v3 支持源过滤以实现更精细的流量控制。当内核接收到 IGMP 查询时,会通过 igmp_heard_query 等函数重置定时器并准备发送报告,这保证了只要有主机在线,路由器就能维护正确的成员状态,而这一切都基于严格的生存时间(TTL)限制以防消息泄露到本地网络之外。

组播路由的决策大脑是内核中的 mr_table 结构体,它不仅维护着物理或虚拟接口列表(vif_table),还掌管着核心的组播转发缓存(MFC)。内核本身并不运行复杂的路由协议,而是通过 setsockopt 系统调用与用户空间的守护进程(如 pimd)进行双向交互:守护进程负责计算路由策略并填入 MFC 表,内核则负责根据这些条目进行高速转发,当遇到未知流量时,内核会将数据包暂存在未解析队列中并通知守护进程处理。

数据包的转发决策完全依赖于 mfc_cache 条目,该结构以(源地址 S,组地址 G)为哈希键值,记录了数据包的入接口以及所有合法的出接口及其 TTL 阈值。在 ip_mr_forward 的实际执行过程中,内核会严格检查数据包的实际入接口是否与缓存条目匹配(防止环路和路由泄露),并遍历所有虚拟接口,仅对那些数据包 TTL 高于接口阈值的端口执行克隆和发送操作,从而实现精确的流量分发。

当组播数据包抵达但缓存未命中时,内核不会立即丢弃,而是启动一套暂存与求助机制,即 ipmr_cache_unresolved。它会将未解析的数据包挂起(通常限制为 3 个包以防内存耗尽),并通过特殊的 socket 向用户空间守护进程发送 IGMPMSG_NOCACHE 报警;如果守护进程在 10 秒内通过 MRT_ADD_MFC 填补了路由条目,内核就会释放被扣留的数据包进行转发,否则这些条目将被定时器清理,这种设计巧妙地在内核的高速转发与用户态的复杂决策之间取得了平衡。