Linux之netlink(2)libnl使用介绍(1)

Linux之netlink(2)Libnl3使用介绍(1)

Author:Onceday Date:2025年4月26日

漫漫长路,才刚刚开始...

全系列文章可查看专栏: Linux内核知识_Once-Day的博客-CSDN博客

本文翻译自libnl3官方文档:Netlink Library (libnl)

参考文档:

文章目录

      • Linux之netlink(2)Libnl3使用介绍(1)
        • [1. libnl介绍](#1. libnl介绍)
        • [2. Netlink协议介绍](#2. Netlink协议介绍)
          • [2.1 Netlink地址](#2.1 Netlink地址)
          • [2.2 消息格式](#2.2 消息格式)
          • [2.3 消息类型](#2.3 消息类型)
          • [2.4 序列号](#2.4 序列号)
        • [3. Netlink套接字](#3. Netlink套接字)
          • [3.1 Socket实例](#3.1 Socket实例)
          • [3.2 序列号](#3.2 序列号)
          • [3.3 多播组](#3.3 多播组)
          • [3.4 回调函数配置](#3.4 回调函数配置)
          • [3.5 套接字属性](#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>。根据程序所使用的子系统和组件的不同,可能还需要在源文件中包含其他的头文件。

c 复制代码
#include <netlink/netlink.h>

#include <netlink/cache.h>

#include <netlink/route/link.h>

如果该库在编译时启用了调试语句,那么当环境变量 NLDBG 的值设置为大于 0 时,它将会把调试信息打印到标准错误输出(stderr)中。

bash 复制代码
NLDBG=2 ./myprogram

下面是各个NLDBG值对应的调试级别:

调试级别 描述
0 调试功能已禁用(默认设置)
1 警告、重要事件及通知信息
2 或多或少较为重要的调试消息
3 会产生大量调试消息的重复性事件相关信息
4 甚至更不太重要的消息

查看与其他套接字交换的 Netlink 消息流通常是很有用的。设置环境变量 NLCB=debug 将启用调试消息处理程序,该程序进而会以人类可读的格式将交换的 Netlink 消息打印到标准错误输出(stderr)中。

bash 复制代码
$ 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(获取配置消息):

c 复制代码
#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。

消息标志,定义了以下标准标志:

c 复制代码
#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 请求的额外通用消息标志:

c 复制代码
#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 请求的标志相互排斥:

c 复制代码
#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结构体表示。

c 复制代码
#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。

c 复制代码
#include <netlink/socket.h>

unsigned int nl_socket_use_seq(struct nl_sock *sk);

不过,大多数应用程序并不希望自己处理序列号。当使用nl_send_auto()函数时,序列号会自动填入,并且在收到回复时会再次进行匹配。

如果所实现的 Netlink 协议不使用请求 / 回复模型,例如当套接字用于接收通知消息时,这种(自动处理序列号的)行为可以并且必须被禁用。

c 复制代码
#include <netlink/socket.h>

void nl_socket_disable_seq_check(struct nl_sock *sk);
3.3 多播组

每个套接字可以订阅其所连接的 Netlink 协议的任意数量的多播组。然后,该套接字将接收发送到任何一个多播组的每条消息的副本。多播组通常用于实现事件通知。

在内核版本 2.6.14 之前,组订阅是使用位掩码来执行的,这将每个协议族的组数量限制为 32 个。即使不建议在新代码中使用,仍然可以通过函数nl_join_groups()访问这个过时的接口。

c 复制代码
#include <netlink/socket.h>

void nl_join_groups(struct nl_sock *sk, int bitmask);

从内核版本 2.6.14 开始引入了一种新方法,该方法支持订阅几乎无限数量的多播组。

c 复制代码
#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 回调函数配置

每个套接字都被分配一个回调配置,该配置控制套接字的行为。例如,这对于每个套接字拥有一个单独的消息接收函数是必要的。不过,在套接字之间共享回调配置也是完全可行的。

可以使用以下函数来访问和设置套接字的回调配置:

c 复制代码
#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);

此外,还存在一种快捷方式,可以直接修改分配给套接字的回调配置:

c 复制代码
#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 个套接字。

c 复制代码
#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 协议族通常只存在一个内核端套接字。

c 复制代码
#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 套接字接口,因此每个套接字背后都有一个文件描述符,你可以直接使用它。

c 复制代码
#include <netlink/socket.h>

int nl_socket_get_fd(const struct nl_sock *sk);

如果一个套接字仅用于接收通知,通常最好将该套接字设置为非阻塞模式,并定期轮询以获取新的通知。

c 复制代码
#include <netlink/socket.h>

int nl_socket_set_nonblocking(const struct nl_sock *sk);

套接字缓冲区用于在发送方和接收方之间对 Netlink 消息进行排队。这些缓冲区的大小指定了你能够写入 Netlink 套接字的最大大小,也就是说,它将间接定义最大消息大小。默认大小是 32KiB。

yacas 复制代码
#include <netlink/socket.h>

int nl_socket_set_buffer_size(struct nl_sock *sk, int rx, int tx);

以下函数允许在套接字上启用 / 禁用自动确认模式。自动确认模式默认是启用的。

c 复制代码
#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 来检索接收到的下一条消息的大小,并分配一个该大小的缓冲区。消息窥探默认是启用的,但可以使用以下函数将其禁用:

c 复制代码
#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结构体。可以使用以下函数来启用 / 禁用接收数据包信息。

c 复制代码
#include <netlink/socket.h>

int nl_socket_recv_pktinfo(struct nl_sock *sk, int state);

注意,Netlink Pktinfo的处理功能还没有实现。

相关推荐
花小璇学linux9 分钟前
imx6ull-裸机学习实验16——I2C 实验
linux·imx6ull·arm裸机开发
勤匠38 分钟前
spring shell 基础使用
java·linux·spring
珹洺1 小时前
Linux操作系统从入门到实战(七)详细讲解编辑器Vim
linux·编辑器·vim
赵健zj2 小时前
鸿蒙Next开发,配置Navigation的Route
android·linux·ubuntu
Ruimin05192 小时前
LSV负载均衡
linux·运维·服务器·负载均衡·lvs
好奇的菜鸟2 小时前
Linux 系统下的 Sangfor VDI 客户端安装与登录完全攻略 (CentOS、Ubuntu、麒麟全线通用)
linux·ubuntu·centos
AuroraDPY3 小时前
Linux 环境变量
linux·运维·服务器
Ronin3053 小时前
【Linux系统】进程切换 | 进程调度——O(1)调度队列
linux·运维·服务器·ubuntu
万象.3 小时前
Linux多进程
linux