ch02_7
2.7 快速参考手册
写代码的时候,最痛苦的不是不知道原理,而是知道原理,但忘了那个该死的函数叫什么名字,或者忘了它到底要填哪几个参数。
我们在这一章里拆解了 Netlink 的骨架、Generic Netlink 的肌肉,甚至连 RTNL 的血管都看了个遍。现在,当我们回到编辑器里准备敲下第一行代码时,我们需要一本「作弊条」——不是那种只列函数名的空洞文档,而是那种告诉你「这函数其实干了什么脏活」的速查表。
我整理了一份我们在这一章(以及内核源码树中)最常打交道的核心 API。与其去翻那几百行的 include/linux/netlink.h,不如直接看这里。
注意:这不是冰冷的 API 手册。我会把那些容易被忽略的行为细节(比如它到底有没有帮你校验长度,或者它会不会自动发 ACK)直接注在后面。
2.7.1 核心 Netlink 辅助函数
这些是 Netlink 通信的基础设施函数,处理从套接字创建到消息封装的通用逻辑。
struct sock *netlink_kernel_create(struct net *net, int unit, struct netlink_kernel_cfg *cfg)
- 作用:在内核空间创建一个 Netlink 套接字。这是所有内核侧 Netlink 通信的起点。
- 参数:
unit就是协议号(比如NETLINK_GENERIC),cfg是配置结构体(里面填入你的回调函数)。 - 细节:成功返回
sock指针,失败返回 NULL。一旦失败,通常是因为协议号冲突或内存不足。
int netlink_rcv_skb(struct sk_buff *skb, int (*cb)(struct sk_buff *, struct nlmsghdr *))
- 作用:这是接收 Netlink 消息的「标准管家」。它在你的 input 回调里被调用。
- 为什么需要它:它替你处理了所有枯燥的脏活累活——检查消息头长度是否越界(
NLMSG_HDRLEN)、跳过控制消息(比如NLMSG_ERROR)、以及处理NLM_F_ACK标志(如果用户要求应答,它会自动调用netlink_ack()发送错误包)。 - 你的任务:你只需要把真正的业务逻辑塞进传给它的
cb回调函数里。如果cb返回非 0 值,netlink_rcv_skb会认为出错了并发送一个 ERROR 报文回去。
struct sk_buff *nlmsg_new(size_t payload, gfp_t flags)
- 作用:分配一个新的 Netlink 消息缓冲区(
sk_buff)。 - 细节:它内部调用了
alloc_skb()。如果你指定的payload为 0,它也会聪明地至少分配一个对齐后的消息头长度(NLMSG_HDRLEN)。这是防止你在填充消息头之前就把内存写崩了。
void *nlmsg_put(struct sk_buff *skb, u32 portid, u32 seq, int type, int len, int flags)
- 作用:构造 Netlink 消息头,并将其放入
skb的数据区。 - 细节:它调用
__nlmsg_put()。如果空间不够(skb尾部空间不足),它会返回 NULL 并丢弃skb。 - 提示:一定要检查返回值!如果它返回 NULL,说明你分配的
skb太小了,这时候千万别继续写,直接把skb丢了重来。
struct nlmsghdr *nlmsg_hdr(const struct sk_buff *skb)
- 作用:拿到
skb里的 Netlink 消息头指针。 - 细节:本质上就是返回
skb->data,但强转了一下类型。别被宏吓到,它就是帮你少写几行强制类型转换。
struct netlink_sock *nlk_sk(struct sock *sk)
- 作用:从通用的
sock结构体拿到 Netlink 专用的netlink_sock结构体。 - 场景:当你需要访问 Netlink 特定的字段(比如
nl_pid或groups)时,用这个宏。net/netlink/af_netlink.h里定义。
2.7.2 RTNetlink 专用接口
路由 Netlink 是最重用的 Netlink 用户。这里是一组我们频繁接触的 API,主要用于操作网络接口、路由表和 IP 地址。
int rtnl_register(int protocol, int msgtype, rtnl_doit_func doit, rtnl_dumpit_func dumpit, rtnl_calcit_func calcit)
- 作用:把一个特定的 RTNetlink 消息类型(比如
RTM_NEWLINK)和你的处理函数绑定起来。 - 参数:
doit:处理单个对象的操作(创建/删除/修改)。dumpit:处理列表转储(ip link show这种)。calcit:计算转储所需的 buffer 大小(可选)。
- 细节:如果你既没提供
doit也没提供dumpit,内核会直接拒绝这个请求。
static int rtnetlink_rcv_msg(struct sk_buff *skb, struct nlmsghdr *nlh)
- 作用:RTNetlink 消息的分发器。它解析消息类型,查找之前注册的
doit或dumpit回调,然后调用它们。 - 细节:你在写驱动时通常不直接调它,它是 RTNetlink socket 的
rcv回调,帮你把消息分发到正确的处理函数。
static int rtnl_fill_ifinfo(struct sk_buff *skb, struct net_device *dev, int type, u32 pid, u32 seq, u32 change, unsigned int flags, u32 ext_filter_mask)
- 作用:把网络接口(
net_device)的信息填充进 Netlink 消息。 - 细节:它干了两件事:先放了一个
nlmsghdr,紧接着放了一个ifinfomsg结构体。如果你想发「网卡上线」或「IP 变动」的通知,这个函数是你的主力。
void rtnl_notify(struct sk_buff *skb, struct net *net, u32 pid, u32 group, struct nlmsghdr *nlh, gfp_t flags)
- 作用:发送一条 RTNetlink 消息给用户空间。
- 场景:当内核里的网络状态发生变化(比如拔网线、改 IP),内核用这个函数通知订阅了该
group的用户空间进程(比如 NetworkManager)。
2.7.3 Generic Netlink 注册与管理
Generic Netlink 的 API 比标准 Netlink 要复杂一点,因为它多了一层「家族管理」的逻辑。
int genl_register_family(struct genl_family *family)
- 作用:注册一个新的 Generic Netlink 家族。
- 细节:它会验证家族的有效性,并分配一个唯一的家族 ID。如果你把
family->id设为GENL_ID_GENERATE,内核会自动帮你生成一个 ID。 - 注意:名字不能重复。同一个名字注册两次会直接报错。
int genl_register_family_with_ops(struct genl_family *family, struct genl_ops *ops, size_t n_ops)
- 作用:原子操作版——同时注册家族和它的一组操作(
ops)。 - 为什么推荐:这是最常用的 API。它等于调用了
genl_register_family(),然后循环调用genl_register_ops()。如果中间出错,它会帮你把注册了一半的家族清理掉,不会留下垃圾。 - 约束:每个
ops必须至少包含doit或dumpit中的一个,否则直接返回-EINVAL。
int genl_register_ops(struct genl_family *family, struct genl_ops *ops)
- 作用:给已注册的家族添加一个具体的命令操作。
- 约束:同一个命令 ID(
cmd)不能注册两次。
void genl_unregister_mc_group(struct genl_family *family, struct genl_multicast_group *grp)
- 作用:注销多播组。
- 细节:注销家族时会自动清理所有组,所以你不需要在驱动退出时手动逐个注销。但如果你想动态增删组,这个函数就派上用场了。
void *genlmsg_put(struct sk_buff *skb, u32 portid, u32 seq, struct genl_family *family, int flags, u8 cmd)
- 作用:给
skb加上 Generic Netlink 的头。 - 细节:它实际上调用了
nlmsg_put()加 Netlink 头,然后紧接着填了一个genlmsghdr(里面包含你的cmd)。 - 返回:返回指向 Generic Netlink 头部之后 payload 区域的指针。你通常会把返回值作为接下来填充属性(
nla_put)的起始位置。
2.7.4 内存与锁
struct sk_buff *netlink_alloc_skb(struct sock *ssk, unsigned int size, u32 dst_portid, gfp_t gfp_mask)
- 作用:分配一个专门用于 Netlink 的
sk_buff。 - 场景:主要用于
NETLINK_MMAP(内存映射 I/O)这种高级优化场景,这在普通驱动开发中很少见。如果你没用 mmap,直接用nlmsg_new即可。
void genl_lock(void) / void genl_unlock(void)
- 作用:操作 Generic Netlink 的全局互斥锁(
genl_mutex)。 - 场景:保护家族注册/注销过程。在大多数情况下,内核内部的
genl_register_*系列函数已经帮你处理了锁,除非你在写极其底层的并发逻辑,否则别乱动它。
本章回响
我们花了一整章的篇幅来拆解 Netlink,这看起来有些篇幅过长——毕竟它表面上只是一套「内核与用户空间聊天的机制」。
但事情到这里有一个微妙的转折:现代 Linux 的内核与用户空间边界,正在变得越来越「薄」。过去,我们通过 ioctl 发送僵化的命令;现在,通过 Netlink(尤其是 Generic Netlink),内核和用户空间更像是两个平等的进程,通过一套精心设计的消息协议在协作。你不仅能下发命令,还能收到事件通知;不仅能查询单个对象,还能转储整个数据库。
这种「双向、异步、可扩展」的通信模式,是理解现代 Linux 网络栈的关键。如果没有 Netlink,ip 命令就不可能存在,iw 也无法控制复杂的无线驱动,甚至容器技术在进行网络隔离时也会举步维艰。
还记得我们在本章开头提到的那个问题吗——为什么不用 ioctl 了?现在答案很清楚了:因为我们需要的是一套动态的、面向未来的接口,而不仅仅是一堆静态的命令代码。
下一章,我们将暂别控制面,去探索数据面的另一个极端:当网络发生故障时,谁负责报信?答案是 ICMP 协议。我们将看到内核如何生成 ICMP 消息,以及这些错误报文是如何穿透协议栈,最终送到应用程序的手里的。
练习题
练习 1:understanding
题目:在开发用户空间网络工具时,你会选择使用基于 IOCTL 的 net-tools 工具包(如 ifconfig)还是基于 Netlink 套接字的 iproute2 工具包(如 ip)?请列举 Netlink 套接字相比 IOCTL 的两个主要技术优势。
答案与解析
答案:应选择 iproute2 (Netlink)。 优势 1:Netlink 支持内核主动向用户空间发送异步消息,而 IOCTL 只能是用户空间发起的同步请求。 优势 2:Netlink 基于 Socket 套接字机制,支持多播,且不需要进行复杂的 IOCTL 命令号定义。
解析:本章明确指出 Netlink 是为了替代 IOCTL 这种笨拙的通信方式而创建的。IOCTL 的主要缺陷在于它是同步的,内核无法主动发起通信(例如无法主动通知用户空间网络接口已断开),而 Netlink 解决了这个问题。此外,Netlink 利用了标准的 Socket API,使得处理双向通信和多播变得更加简单和灵活。
练习 2:application
题目:假设你需要编写一个用户空间守护进程来监控 Linux 系统中的 IPv4 路由表变化(例如,当管理员执行 ip route add 时收到通知)。基于本章对 rtnetlink 和 Netlink 消息处理的描述,该守护进程需要绑定哪个特定的 Netlink 协议族,并加入哪个多播组才能接收到这些异步事件?
答案与解析
答案:协议族:NETLINK_ROUTE (或 AF_NETLINK 配合协议类型 NETLINK_ROUTE) 多播组:RTNLGRP_IPV4_ROUTE
解析:根据本章内容,路由和链路相关的消息属于 rtnetlink 协议族(NETLINK_ROUTE)。为了接收特定的异步事件,用户空间套接字不仅需要创建 NETLINK_ROUTE 类型的套接字,还需要通过 bind() 加入对应的多播组。文中提到当插入新的路由条目时,内核会调用 rtmsg_fib() 通过 rtnl_notify() 通知所有注册到 RTNLGRP_IPV4_ROUTE 组的监听者。
练习 3:thinking
题目:在设计一个自定义的 Netlink 协议时,标准 Netlink 协议的数量被限制为 32 个(MAX_LINKS)。为了解决这个限制并支持更多动态的内核子系统通信,Linux 引入了 Generic Netlink (genl)。请简述 Generic Netlink 是如何通过多路复用技术来突破这个数量限制的,并列举一个使用它的实际子系统案例。
答案与解析
答案:原理:Generic Netlink 自身只占用一个标准的 Netlink 协议号(NETLINK_GENERIC),它充当多路复用器。它引入了 genl_family(家族)的概念,允许不同的子系统(如 nl80211)注册为不同的家族。通过家族 ID 来区分同一 Netlink 套接字上的不同子系统消息,从而理论上支持成百上千种不同的协议。
案例:nl80211(用于无线子系统配置,被 iw 工具使用)。
解析:这是一个考察对 Netlink 架构深度理解的问题。标准 Netlink 类似于一个扁平的频道,频道数量有限(32个)。Generic Netlink 将其中一个频道专门拿出来做“总管”(Multiplexer),在这个频道内部通过类似于“子频道”的方式支持任意数量的通信类型。这是本章中关于 Generic Netlink 协议的核心设计思想,也是为何无线驱动、任务统计等现代子系统倾向于使用它的原因。
要点提炼
Netlink 套接字是现代 Linux 内核与用户空间通信的核心机制,它彻底取代了老旧的 ioctl 方式。通过支持双向异步通信和多播机制,Netlink 不仅允许用户空间主动查询内核状态(如路由表、网卡信息),更关键的是能让内核在事件发生时(如网卡上下线、路由变更)主动“推送”通知给订阅的用户进程,这种事件驱动模型是 iproute2 等现代网络管理工具高效运行的基础。
用户空间开发 Netlink 应用通常基于 libnl 或 libmnl 库,以避免直接处理繁琐的系统调用和缓冲区操作。通信的基础是地址结构体 sockaddr_nl,其中 nl_pid 字段充当“端口号”的概念:发往内核时设为 0,而在多线程程序中若使用自动绑定需小心冲突,因为同一进程内的多个 socket 若不手动指定不同的 nl_pid,会导致接收错乱。
内核处理 Netlink 消息的核心在于消息分发与回调注册机制。通过 netlink_kernel_create 创建 socket 时指定的 input 回调函数(如 rtnetlink 的 rtnetlink_rcv)充当总入口,而具体的消息类型(如 RTM_NEWLINK)则通过 rtnl_register() 将处理函数注册到内核的多维查找表中。当消息到达时,内核依据协议号和消息类型分发到对应的 doit(用于单条操作)或 dumpit(用于批量转储)函数执行。
Netlink 协议采用**“头部 + TLV 属性”**的二进制格式以保证极高的扩展性。消息头 nlmsghdr 包含长度、类型和序列号,紧随其后的载荷采用 TLV(Type-Length-Value)编码,支持嵌套以构建复杂数据结构。内核通过预定义的 nla_policy 策略数组对传入属性进行严格的类型和长度校验,只有通过验证的请求才会被执行,从而保证了内核的安全性。
针对 Netlink 协议号资源(仅 32 个)紧缺的问题,内核引入了 Generic Netlink 作为通用的多路复用解决方案。它仅占用一个协议号(NETLINK_GENERIC),通过名为 nlctrl 的“总服务台”动态分配和管理无限的子家族(如 nl80211),实现了基于字符串命名的运行时扩展。这种机制允许开发者无需修改内核核心代码,即可通过注册自定义的 genl_family 和 genl_ops 来实现特定的内核模块通信接口。