libnl
库提供了一套应用于Linux系统基于Netlink协议通信的API接口。从本质上看,Netlink其实是一种典型的IPC机制,只不过此IPC主要是介于用户空间与内核空间之间的通信,而非传统意义上用户空间进程之间的通信。Netlink是设计出来替换ioctl
机制,就目前的使用情况,这一点显然还没有完全达到。
构成
libnl
在设计上被分割为若干个小型库(small libraries
),应用程序在链接时可以择库链接,无需将所有库链接到应用程序中。
-
libnl
:libnl的核心库,构建Netlink通信的基础,提供套接字操作、消息构建/解析、收发报文等操作 -
libnl-route
:提供NETLINK_ROUTE
家族的API接口库,包括网络设备、路由功能、IP地址、邻居功能等操作 -
libnl-genl
:通用Netlink操作 -
libnl-nf
:NetFilter以及接口监控相关的Netlink操作
使用方法
头文件
libnl提供的主要头文件为<netlink/netlink.h>
,此外还提供了一些其他头文件,可以视应用程序的需要酌情增删。
#include <netlink/netlink.h>
#include <netlink/cache.h>
#include <netlink/route/link.h>
#include <netlink/route/addr.h>
链接
可以在gcc中直接链接libnl库
$ gcc myprogram.c -o myprogram $(pkgconfig --cflags --libs libnl-3.0)
$ #或采取直接链接方式
& gcc -Wall -Wextra -pedantic listif.c -o lsif -lnl-3 -lnl-route-3 -isystem /usr/include/libnl3
Netlink协议
Netlink 协议是基于套接字的进程间通信(IPC)机制,它可用于用户空间进程和内核之间或者用户空间进程之间的通信。Netlink协议基于BSD套接字并使用AF_NETLINK地址簇。每一个Netlink协议都有自己的协议号(比如:NETLINK_ROUTE、NETLINK_NETFILTER等)。它的寻址方案是基于 32 位的端口号(之前被称为 PID),此端口号用于唯一地标识每一个对等通信节点。
寻址
Netlink 地址(端口)由一个 32 位的整数组成。端口(port)零保留给内核使用,表示每个Netlink协议簇中内核部分的套接字,其他的端口则通常指的是用户空间的套接字。
注意: 最初通常使用进程标识符(PID)作为本地端口号,但这种方式随着线程化Netlink应用程序的引入而失效,因为这类进程需要多个套接字。为解决此项冲突,libnl以进程标识符为基数再加上一个偏移量来生成唯一的端口号,此方式可以让一个进程使用多个套接字。出于向后兼容方面的考虑,第一个套接字还是以进程标识符作为端口号。
上图中用户空间有三个应用程序,对应有5个套接字;而内核空间则创建了2个套接字。同时,此图也展示了Netlink的常见应用场景:
- 用户空间的进程和内核的通信
- 用户空间内进程之间的通信
- 侦听内核的多播通知
应用场景
用户空间进程和内核的通信
Netlink最常见的应用场景就是用户空间应用程序发送请求给内核,比如设置/读取接口的IP地址,然后接受并处理内核返回的信息,这个回复信息要么是请求出错的信息,要么就是请求成功的通知信息。
用户空间进程之间的通信
Netlink也可以直接作为用户空间应用程序之间的进程间通信机制,但这种方式并不常见,通常会代之以unix域套接字。这种通信并不限制在两个对等通信节点之间,任意一个节点都可以和其他的对等点进行通信。此外因为Netlink支持多播,一条消息可以由多个节点同时接收到。
注意:为了让套接字对通信双方可见,这两个套接字必须在同一个Netlink协议簇下创建。
侦听内核的多播通知信息
此类Netlink通信是一种非常常见的应用方式,此方式可以让用户空间那些需要处理特定内核事件的的守护进程侦听内核反馈的消息/事件。这些应用程序进程通常会订阅内核使用的某个多播组,内核则在某些事件发生的时候通过它来通知订阅该组播组的进程。
相对于直接寻址来说,多播(组播)寻址是一个更好的方式,因为它有更高的灵活性,可以在无需通知内核的情况下随时与用户空间应用组件交换信息。
消息格式
Netlink协议通常是基于消息的,消息则通常是由Netlink消息头部(struct nlmsghdr)加上有效载荷组成。虽然有效载荷可以由任何数据组成,但是它通常的格式是一 个固定大小的协议相关头部后面紧跟一系列的属性。
- 总长度(32 位):消息包括 netlink 消息头部在内的总字节数
- 消息类型(16 位):消息类型指明了消息的有效载荷的类型。netlink 协议定义了多个标准的消息类型。每个协 议簇都可能定义了额外的消息类型。
- 消息标志(16 位):消息标志可以用来更改消息类型的行为。
- 序列号(32 位):序列号的使用是可选的,它可以用来引用前一条消息。比如一条错误消息中可以引用导致错 误的那条请求消息。
- 端口号:端口号指明了这条消息需要发往哪个对等节点。如果没有指定端口号,那么这条消息会被投 递给同一个协议簇中第一个匹配的内核端套接字。
消息类型
Netlink 在请求消息(requests)、通知消息(notifications)和应答消息(replies)的 处理上是有区别的。请求消息设有 NLM_F_REQUEST 标志位,它用来向接收方请求某种响应 。一般来说请求消息都是从用户空间发送到内核的。虽然不是强制规定,但每次发送的请求 消息序列号都应该是上一个序列号加一。
由于请求自身的特性,接收方在收到请求消息之后可能会发送另一条 netlink 消息来响应 这个请求。应答消息的序列号必须和它响应的那条请求消息的序列号一致。
通知消息则没有那么严谨,它不需要应答,所以序列号通常是被设置成 0 的。
消息的类型主要是由消息头部中 16 位的消息类型字段确定的。Netlink 定义了如下标准消息类型:
- NLMSG_NOOP - 无需任何操作,消息必须被丢弃
- NLMSG_ERROR - 错误消息或者是 ACK,
- NLMSG_DONE - 分段序列的结束
- NLMSG_OVERRUN - 通知消息越界错误
每个 netlink 协议都可以自由的定义自己的消息类型。需要注意的是小于 NLMSG_MIN_TYPE(0x10) 的类型是保留的,所以不能使用。通常我们会定义自己的消息类型来实现 RPC 模式。假设你的 netlink 协议的目的是允许你 配置一个网络设备的某些部分,所以你想要提供各种配置选项的读/写访问。完成这项任务 典型的 “netlik 解决方案” 是定义两种消息类型MSG_SETCFG,MSG_GETCFG
:
#define MSG_SETCFG 0x11
#define MSG_GETCFG 0x12
发送一条 MSG_GETCFG
请求消息通常会收到一条包含当前配置信息的类型为 MSG_SETCFG
的应答消息。用面向对象的术语来说这叫做“内核在用户空间设置了配置信息的本地拷贝”
配置信息可以通过发送一条
MSG_SETCFG
信息来更改,这条消息会收到一条 ACK
响应信息 或是一条错误信息。此外,内核也可以在配置信息发生改变的时候发送通知信息,这样用户空间应用程序就可以 使用监听而不是频繁轮询的方式来获取改变信息。通知消息通常是使用目前已有的消息类型 这就需要应用程序使用不同的套接字处理请求消息和通知消息,当然你也可以使用特殊的消息类型来单独表示通知信息。
分片消息
理论上一条 netlink 消息的最大长度是 4GiB,但是套接字的缓冲区一般不会设置有如此大的空间来容纳4GiB消息。通常情况下,消息的长度被限制在页的大小(PAGE_SIZE )之内,然后使用分片机制把大消息分割为多个消息。分片消息设有NLM_F_MULTI
标志位,接收者需要不断的接收和解析消息,直至收到一个消息类型为NLMSG_DONE
的消息为止。
和分片之后的ip包不一样,分片之后的Netlink消息无需重组,但是如果协议需要重组,也可以执行重组动作。分片消息经常会被用来发送对象列表或者是对象树,这种情况下消息段只是简单的承载几个对象,一般允许单独处理每个消息分片。
错误消息
错误消息可以作为请求消息的响应发送回去,错误消息必须使用标准的消息类型 NLMSG_ERROR
,它的有效载荷是由错误码和原来的请求消息头部组成。
注意:错误消息的序列号必须和导致错误发生的请求消息的序列号设置为一致。
ACKs消息
发送方可以通过设置NLM_F_ACK
标志位来要求接收方为处理过的每一个请求消息都发送一 个ACK消息。这种方式通常是用来让发送方在请求被接收方处理之后同步下一步的操作。
ACK 消息和错误消息使用一样的消息类型
(NLMSG_ERROR)
和负荷格式,不同的是 ACK 消息 的错误代码被设置为 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
- 请求了 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 协议中可能会有细微的差别。
序列号
Netlink允许通过序列号来关联回复和请求。需要注意的是,这里的序列号和 TCP 这类的协议的序列号是不一样的,Netlink的序列号并不强制使用。序列号唯一的用途就是把一条应答消息和相应的请求消息联系起来,序列号是以单个套接字为基础来管理。