Linux之netlink(2)Libnl3使用介绍(1)
Author:Onceday Date:2025年4月26日
漫漫长路,才刚刚开始…
全系列文章可查看专栏: Linux内核知识_Once-Day的博客-CSDN博客
本文翻译自libnl3官方文档:Netlink Library (libnl)
参考文档:
- thom311/libnl: Netlink Library Suite
- libnl - Netlink Protocol Library Suite
- Routing Family Netlink Library (libnl-route)
- Netlink Library (libnl)
- Documentation Overview - libnl Suite
文章目录
- Linux之netlink(2)Libnl3使用介绍(1)
- 1. libnl介绍
- 2. Netlink协议介绍
- 2.1 Netlink地址
- 2.2 消息格式
- 2.3 消息类型
- 2.4 序列号
- 3. Netlink套接字
- 3.1 Socket实例
- 3.2 序列号
- 3.3 多播组
- 3.4 回调函数配置
- 3.5 套接字属性
1. libnl介绍
libnl3是Linux平台上一个功能丰富的网络编程库。它为用户空间程序提供了一组全面的API,用于与Linux内核的网络组件进行交互。libnl3让开发者能够方便地配置和管理各种网络功能,如链路、接口、路由、地址、邻居、策略路由、流量控制、网络状态监控等。
libnl 套件是一组库的集合,它为基于 Netlink 协议的 Linux 内核接口提供了应用程序编程接口(API)。
Netlink 是一种主要在内核与用户空间进程之间使用的进程间通信(IPC)机制。它被设计成是 ioctl(输入输出控制)更灵活的替代方案,主要用于提供与网络相关的内核配置和监控接口。
这些接口被划分到几个小型库中,这样就不会强制应用程序链接到一个庞大臃肿的单一库上。
- libnl,核心库,实现了使用 Netlink 协议所需的基本功能,比如套接字处理、消息构建与解析,以及数据的发送和接收。这个库保持小巧且简约。该套件中的其他库都依赖于这个库。
- libnl-route,提供了 NETLINK_ROUTE 系列配置接口的 API,这些接口涉及网络接口、路由、地址、邻居节点以及流量控制等方面。
- libnl-genl,提供了通用 Netlink 协议(Netlink 协议的扩展版本)的 API。
- libnl-nf,提供了基于 Netlink 的 netfilter(网络过滤器)配置和监控接口(连接跟踪、日志记录、队列)的 API。
主要的头文件是 <netlink/netlink.h>
。根据程序所使用的子系统和组件的不同,可能还需要在源文件中包含其他的头文件。
#include <netlink/netlink.h>#include <netlink/cache.h>#include <netlink/route/link.h>
如果该库在编译时启用了调试语句,那么当环境变量 NLDBG 的值设置为大于 0 时,它将会把调试信息打印到标准错误输出(stderr)中。
NLDBG=2 ./myprogram
下面是各个NLDBG值对应的调试级别:
调试级别 | 描述 |
---|---|
0 | 调试功能已禁用(默认设置) |
1 | 警告、重要事件及通知信息 |
2 | 或多或少较为重要的调试消息 |
3 | 会产生大量调试消息的重复性事件相关信息 |
4 | 甚至更不太重要的消息 |
查看与其他套接字交换的 Netlink 消息流通常是很有用的。设置环境变量 NLCB=debug 将启用调试消息处理程序,该程序进而会以人类可读的格式将交换的 Netlink 消息打印到标准错误输出(stderr)中。
$ NLCB=debug ./myprogram-- Debug: Sent Message:-------------------------- BEGIN NETLINK MESSAGE ---------------------------[HEADER] 16 octets.nlmsg_len = 20.nlmsg_type = 18 <route/link::get>.nlmsg_flags = 773 <REQUEST,ACK,ROOT,MATCH>.nlmsg_seq = 1301410712.nlmsg_pid = 20014[PAYLOAD] 16 octets
.....
2. Netlink协议介绍
2.1 Netlink地址
Netlink 协议是一种基于套接字的进程间通信(IPC)机制,用于用户空间进程与内核之间,或者用户空间进程彼此之间的通信。Netlink 协议基于 BSD 套接字,并使用 AF_NETLINK 地址族。每一种 Netlink 协议都有其自身的协议编号(例如,NETLINK_ROUTE、NETLINK_NETFILTER 等)。它的编址模式基于一个 32 位的端口号,以前称为进程标识符(PID),这个端口号唯一标识每个通信对等端。
Netlink 地址(端口)由一个 32 位整数组成。端口 0(零)是为内核保留的,它指的是每个 Netlink 协议族在内核端的套接字。其他端口号通常指的是用户空间所拥有的套接字,不过这并非强制规定。
起初,常见的做法是使用进程标识符(PID)作为本地端口号。但随着支持多线程的 Netlink 应用程序以及需要多个套接字的应用程序的出现,这种做法变得不太实用了。因此,libnl 库会基于进程标识符生成唯一的端口号,并为其添加一个偏移量,从而允许使用多个套接字。出于向后兼容的原因,初始套接字的端口号仍然会等于进程标识符。
上图展示了三个应用程序以及内核端所暴露的两个内核端套接字。它呈现了常见的 Netlink 使用场景:
- 用户空间到内核。
- 用户空间到用户空间。
- 监听内核的多播通知。
(1)用户空间到内核:Netlink 最常见的使用形式是用户空间应用程序向内核发送请求,并处理回复,回复内容要么是错误消息,要么是成功通知。
(2)用户空间到用户空间:Netlink 也可以用作一种进程间通信机制,以便用户空间应用程序之间直接进行通信。通信并不局限于两个对等端,任意数量的对等端都可以相互通信,并且多播功能允许通过一条消息发送给多个对等端。
为了让各个套接字能够相互可见,必须为相同的 Netlink 协议族创建这两个套接字。
(3)用户空间监听内核通知:这种形式的 Netlink 通信通常出现在用户空间守护进程中,这些守护进程需要针对某些内核事件采取行动。此类守护进程通常会维护一个订阅了某个多播组的 Netlink 套接字,内核使用该多播组向感兴趣的用户空间各方通知特定事件。
由于在任何时候更换用户空间组件时都无需内核察觉,并且具有灵活性,所以相较于直接编址,多播的使用更为可取。
2.2 消息格式
Netlink 协议通常基于消息,由 Netlink 消息头部(struct nlmsghdr
)以及附加到它上面的有效载荷组成。有效载荷可以由任意数据构成,但通常包含一个固定大小的特定于协议的头部,其后跟着一系列属性。
Netlink 消息头部(struct nlmsghdr
结构体):
-
总长度(32 位):消息的总长度,以字节为单位,包括 Netlink 消息头部。
-
消息类型(16 位):消息类型指定了该消息所携带的有效载荷的类型。Netlink 协议定义了几种标准的消息类型。每个协议族可能会定义额外的消息类型。
-
消息标志(16 位):消息标志可用于修改消息类型的行为。
-
序列号(32 位):序列号是可选的,可用于引用先前的消息,例如,一条错误消息可以引用导致该错误的原始请求。
-
端口号(32 位):端口号指定了该消息应发送到的对等端。如果未指定端口号,该消息将被发送到同一协议族中第一个匹配的内核端套接字。
2.3 消息类型
Netlink 协议区分请求、通知和回复。请求是设置了NLM_F_REQUEST
标志的消息,其目的是向接收方请求执行某个操作。请求通常由用户空间进程发送到内核。虽然并非严格强制要求,但每个发送的请求都应该携带一个递增的序列号。
根据请求的性质,接收方可能会用另一条 Netlink 消息来回复该请求。回复的序列号必须与它所关联的请求的序列号相匹配。
通知属于非正规性质的消息,不需要回复,因此序列号通常设置为 0。
消息的类型主要通过消息头部中设置的 16 位消息类型来识别。定义了以下标准消息类型:
-
NLMSG_NOOP,无操作,该消息必须被丢弃。
-
NLMSG_ERROR,错误消息或确认消息(ACK)。
-
NLMSG_DONE,多部分消息序列的结束。
-
NLMSG_OVERRUN,溢出通知(错误)。
每个 Netlink 协议都可以自由定义自己的消息类型。请注意,小于 NLMSG_MIN_TYPE(0x10)的消息类型值是保留的,不能使用。
通常的做法是使用自定义的消息类型来实现远程过程调用(RPC)模式。假设正在实现的 Netlink 协议的目标是允许对特定网络设备进行配置,因此希望提供对各种配置选项的读写访问权限。实现这一目标的典型 “Netlink 方式” 是定义两种消息类型:MSG_SETCFG(设置配置消息)和 MSG_GETCFG(获取配置消息):
#define MSG_SETCFG 0x11
#define MSG_GETCFG 0x12
发送一条MSG_GETCFG
请求消息通常会触发一条消息类型为MSG_SETCFG
的回复,回复中包含当前的配置信息。用面向对象的术语来说,这可以描述为 “内核在用户空间中设置配置的本地副本”。
可以通过发送一条 MSG_SETCFG 消息来更改配置,对该消息的回复要么是一条确认消息(ACK),要么是一条错误消息。
作为可选操作,内核可以发送配置更改的通知,使用户空间能够监听这些更改,而无需频繁轮询。通知通常会重用现有的消息类型,并且依赖于应用程序使用单独的套接字来区分请求和通知,但你也可以指定一个单独的消息类型。
尽管从理论上讲,一条 Netlink 消息的大小最大可达 4GiB,但套接字缓冲区很可能不够大,无法容纳如此大小的消息。因此,通常的做法是将消息大小限制为一页的大小(PAGE_SIZE),并使用多部分机制将大块数据分割成几条消息。一条多部分消息(Multipart Messages)设置了标志NLM_F_MULTI
,接收方需要持续接收和解析消息,直到接收到特殊的消息类型NLMSG_DONE
为止。
与分片的 IP 数据包不同,多部分消息无需重新组装,尽管如果协议希望以这种方式工作,重新组装也是完全可行的。多部分消息常常用于发送对象列表或对象树,每条多部分消息仅仅携带多个对象,从而允许独立解析每条消息。
错误消息可以作为对请求的响应而发送。错误消息必须使用标准消息类型 NLMSG_ERROR。其有效载荷由一个错误代码和原始请求的 Netlink 消息头部组成。
错误消息应将序列号设置为导致该错误的请求的序列号。
确认消息(ACK),发送方可以通过在请求中设置 NLM_F_ACK 标志,来请求接收方为每条已处理的请求发回一条确认消息。这通常用于让发送方在请求被接收方处理之前同步后续的处理操作。
确认消息也使用消息类型NLMSG_ERROR
和有效载荷格式,但错误代码设置为 0。
消息标志,定义了以下标准标志:
#define NLM_F_REQUEST 1
#define NLM_F_MULTI 2
#define NLM_F_ACK 4
#define NLM_F_ECHO 8
- NLM_F_REQUEST - 消息是一个请求。
- NLM_F_MULTI - 多部分消息。
- NLM_F_ACK - 请求确认消息。
- NLM_F_ECHO - 请求回显该请求。
标志 NLM_F_ECHO 与 NLM_F_ACK 标志类似。它可以与 NLM_F_REQUEST 标志结合使用,使得作为请求结果而发送的通知,无论发送方是否已订阅相应的多播组,都会被发送给发送方。
还定义了一些仅适用于 GET 请求的额外通用消息标志:
#define NLM_F_ROOT 0x100
#define NLM_F_MATCH 0x200
#define NLM_F_ATOMIC 0x400
#define NLM_F_DUMP (NLM_F_ROOT|NLM_F_MATCH)
- NLM_F_ROOT - 基于树的根节点返回数据。
- NLM_F_MATCH - 返回所有匹配的条目。
- NLM_F_ATOMIC - 已过时,曾经用于请求一个原子操作。
- NLM_F_DUMP - 返回所有对象的列表(NLM_F_ROOT|NLM_F_MATCH)。
这些标志的使用完全是可选的,许多 Netlink 协议仅使用 NLM_F_DUMP 标志,该标志通常请求接收方以多部分消息序列的形式,发送与消息类型相关的所有对象的列表。
还有另一组与 NEW 或 SET 请求相关的标志。这些标志与 GET 请求的标志相互排斥:
#define NLM_F_REPLACE 0x100
#define NLM_F_EXCL 0x200
#define NLM_F_CREATE 0x400
#define NLM_F_APPEND 0x800
- NLM_F_REPLACE - 如果对象已存在,则替换该现有对象。
- NLM_F_EXCL - 如果对象已经存在,则不更新该对象。
- NLM_F_CREATE - 如果对象尚不存在,则创建该对象。
- NLM_F_APPEND - 在列表末尾添加对象。
这些标志的行为在不同的 Netlink 协议之间可能会略有不同。
2.4 序列号
Netlink 允许使用序列号来帮助将回复与请求相关联。需要注意的是,与 TCP 等协议不同,Netlink 对序列号没有严格的强制要求。序列号的唯一目的是帮助发送方将回复与相应的请求关联起来。序列号是在每个套接字的基础上进行管理的。
3. Netlink套接字
3.1 Socket实例
为了使用 Netlink 协议,需要一个 Netlink 套接字。每个套接字都定义了一个独立的消息发送和接收上下文。一个应用程序可以使用多个套接字,例如,一个套接字用于发送请求并接收回复,另一个套接字订阅多播组以接收通知。
Netlink 套接字以及所有相关属性(包括实际的文件描述符)都由struct nl_sock
结构体表示。
#include <netlink/socket.h>struct nl_sock *nl_socket_alloc(void)
void nl_socket_free(struct nl_sock *sk)
应用程序必须为其希望使用的每个 Netlink 套接字分配一个struct nl_sock
结构体的实例。
3.2 序列号
该库会自动为应用程序处理序列号。在套接字结构中存储有一个序列号计数器,当需要发送预期会产生回复(如错误消息或任何其他需要与原始消息相关联的消息类型)的消息时,会自动使用并递增该计数器。
或者,也可以通过函数nl_socket_use_seq()
直接使用该计数器。它将返回计数器的当前值,然后将其递增 1。
#include <netlink/socket.h>unsigned int nl_socket_use_seq(struct nl_sock *sk);
不过,大多数应用程序并不希望自己处理序列号。当使用nl_send_auto()
函数时,序列号会自动填入,并且在收到回复时会再次进行匹配。
如果所实现的 Netlink 协议不使用请求 / 回复模型,例如当套接字用于接收通知消息时,这种(自动处理序列号的)行为可以并且必须被禁用。
#include <netlink/socket.h>void nl_socket_disable_seq_check(struct nl_sock *sk);
3.3 多播组
每个套接字可以订阅其所连接的 Netlink 协议的任意数量的多播组。然后,该套接字将接收发送到任何一个多播组的每条消息的副本。多播组通常用于实现事件通知。
在内核版本 2.6.14 之前,组订阅是使用位掩码来执行的,这将每个协议族的组数量限制为 32 个。即使不建议在新代码中使用,仍然可以通过函数nl_join_groups()
访问这个过时的接口。
#include <netlink/socket.h>void nl_join_groups(struct nl_sock *sk, int bitmask);
从内核版本 2.6.14 开始引入了一种新方法,该方法支持订阅几乎无限数量的多播组。
#include <netlink/socket.h>int nl_socket_add_memberships(struct nl_sock *sk, int group, ...);
int nl_socket_drop_memberships(struct nl_sock *sk, int group, ...);
3.4 回调函数配置
每个套接字都被分配一个回调配置,该配置控制套接字的行为。例如,这对于每个套接字拥有一个单独的消息接收函数是必要的。不过,在套接字之间共享回调配置也是完全可行的。
可以使用以下函数来访问和设置套接字的回调配置:
#include <netlink/socket.h>struct nl_cb *nl_socket_get_cb(const struct nl_sock *sk);
void nl_socket_set_cb(struct nl_sock *sk, struct nl_cb *cb);
此外,还存在一种快捷方式,可以直接修改分配给套接字的回调配置:
#include <netlink/socket.h>int nl_socket_modify_cb(struct nl_sock *sk, enum nl_cb_type type, enum nl_cb_kind kind,nl_recvmsg_msg_cb_t func, void *arg);
3.5 套接字属性
本地端口号唯一标识该套接字,并用于对其进行寻址。在分配套接字时,会自动生成一个唯一的本地端口号。它将由进程 ID(22 位)和一个随机数(10 位)组成,因此每个进程最多允许有 1024 个套接字。
#include <netlink/socket.h>uint32_t nl_socket_get_local_port(const struct nl_sock *sk);
void nl_socket_set_local_port(struct nl_sock *sk, uint32_t port);
注意:可以覆盖本地端口号,但你必须确保所提供的值是唯一的,并且任何其他应用程序中的其他套接字都没有使用相同的值。
可以为套接字分配一个对等端口,这将导致通过该套接字发送的所有单播消息都被发送到该对等方。如果未指定对等方,则消息将发送到内核,内核将尝试自动将该套接字绑定到同一 Netlink 协议族的内核端套接字。通常的做法是不将套接字绑定到对等端口,因为每个 Netlink 协议族通常只存在一个内核端套接字。
#include <netlink/socket.h>uint32_t nl_socket_get_peer_port(const struct nl_sock *sk);
void nl_socket_set_peer_port(struct nl_sock *sk, uint32_t port);
Netlink 使用 BSD 套接字接口,因此每个套接字背后都有一个文件描述符,你可以直接使用它。
#include <netlink/socket.h>int nl_socket_get_fd(const struct nl_sock *sk);
如果一个套接字仅用于接收通知,通常最好将该套接字设置为非阻塞模式,并定期轮询以获取新的通知。
#include <netlink/socket.h>int nl_socket_set_nonblocking(const struct nl_sock *sk);
套接字缓冲区用于在发送方和接收方之间对 Netlink 消息进行排队。这些缓冲区的大小指定了你能够写入 Netlink 套接字的最大大小,也就是说,它将间接定义最大消息大小。默认大小是 32KiB。
#include <netlink/socket.h>int nl_socket_set_buffer_size(struct nl_sock *sk, int rx, int tx);
以下函数允许在套接字上启用 / 禁用自动确认模式。自动确认模式默认是启用的。
#include <netlink/socket.h>void nl_socket_enable_auto_ack(struct nl_sock *sk);
void nl_socket_disable_auto_ack(struct nl_sock *sk);
启用/禁用消息窥探(Message Peeking)。如果启用,消息窥探会使nl_recv函数尝试使用 MSG_PEEK 来检索接收到的下一条消息的大小,并分配一个该大小的缓冲区。消息窥探默认是启用的,但可以使用以下函数将其禁用:
#include <netlink/socket.h>void nl_socket_enable_msg_peek(struct nl_sock *sk);
void nl_socket_disable_msg_peek(struct nl_sock *sk);
启用 / 禁用接收数据包信息。如果启用,从内核接收到的每条 Netlink 消息都将在控制消息中包含一个额外的struct nl_pktinfo
结构体。可以使用以下函数来启用 / 禁用接收数据包信息。
#include <netlink/socket.h>int nl_socket_recv_pktinfo(struct nl_sock *sk, int state);
注意,Netlink Pktinfo的处理功能还没有实现。