Netlink 库 -- 官方开发者教程中文版第二部分

2. Netlink 协议基础

Netlink 协议是基于套接字的进程间通信(IPC)机制,它可用于用户空间进程和内核之间 或者用户空间进程之间的通信。Netlink 协议基于 BSD 套接字并使用 AF_NETLINK 地址簇。 每一个 Netlink 协议都有自己的协议号(比如:NETLINK_ROUTE,NETLINK_NETFILTER, 等等)。它的寻址方案是基于 32 位的端口号(之前被称为 PID),这个端口号用来唯一的 标识每一个对等通信节点。

2.1. 寻址

Netlink 地址(端口)由一个 32 位的整数组成。端口(port)零保留给内核使用,代表每 个 Netlink 协议簇中内核部分的套接字。虽然不是强制规定,其他的端口则通常指的是用 户空间的套接字。

注意: 一开始,我们通常都是使用进程标识符(PID)作为本地端口号。这种方式随着 线程化的 netlink 应用程序的引入而失效,因为这种进程需要多个套接字。因此 libnl 以进程标识符为基数再加上一个偏移量的来生成唯一的端口号,这种方式可以让一个进程 使用多个套接字。出于向后兼容方面的考虑,第一个套接字还是以进程标识符作为端口号。

Netlink 寻址

上面这幅图中用户空间有三个应用程序,而内核空间则存在两个套接字。这幅图展示了 netlink 的常见的应用场景:

  • 用户空间和内核之间通信

  • 用户空间之间通信

  • 监听内核的多播通知

用户空间和内核之间的通信

Netlink 最常见的应用场景就是用户空间的应用程序发送请求给内核,然后处理内核返回的 信息,这个回复信息要么是请求出错的信息,要么就是请求成功的通知信息。

App1                 App2                    App3
 |                    |                        |
 |           request(src=11,dst=0)             |
 +--------------------+------------------------>
 |                    |                        |
 |            reply(src=0,dst=11)              |
 <--------------------+------------------------+
 |                    |                        |
 |                    |                        |
 |                    |                        |
 |                    |  request(src=21,dst=0) |
 |                    +------------------------>
 |                    |                        |
 |                    |    reply(src=0,dst=21) |
 |                    <------------------------+
 |                    |                        |
 |                    |                        |

用户空间之间通信

Netlink 也可以直接作为用户空间应用程序之间的进程间通信机制。这种通信并不限制在两 个对等通信节点之间,任意一个节点都可以和其他的对等点进行通信。此外因为 Netlink 支持多播,一条消息可以由多个节点同时接收到。

为了让套接字对通信双方可见,这两个套接字必须在同一个 netlink 协议簇下创建。

App2                                    App3
 |                                       |
 |          request(src=22,dst=31)       |
 +--------------------------------------->
 |                                       |
 |           reply(src=31,dst=22)        |
 <---------------------------------------+
 |                                       |
 |                                       |

监听内核的通知信息

这一类 netlink 通信的典型使用者是用户空间那些需要处理特定内核事件的的守护进程。 这些守护进程通常会订阅内核使用的某个多播组,内核则在某些事件发生的时候通过它来 通知那部分订阅过该组的进程。

Kernel                                 App3
  |                                      |
  |     notification(src=0,group=foo)    |
  +-------------------------------------->
  |                                      |
  |                                      |
  |                                      |

相对于直接寻址来说,使用多播是一个更好的方式,因为它有更高的灵活性。我们可以在不 通知内核的情况下随时更改用户空间的相关组件。

2.2. 消息格式

一个 Netlink 协议通常是基于消息的,消息则通常是由 netlink 消息头部(struct nlmsghdr)加上有效载荷组成。虽然有效载荷可以由任何数据组成,但是它通常的格式是一 个固定大小的协议相关头部后面紧跟一系列的属性。

Netlink 消息头部(struct nlmsghdr)

Netlink 消息头部

总长度(32 位)

消息包括 netlink 消息头部在内的总字节数

消息类型(16 位)

消息类型指明了消息的有效载荷的类型。netlink 协议定义了多个标准的消息类型。每个协 议簇都可能定义了额外的消息类型。详细信息请参考 消息类型 一节。

消息标志(16 位)

消息标志可以用来更改消息类型的行为。标准消息标志列表请参见 消息标志 一节。

序列号(32 位)

序列号的使用是可选的,它可以用来引用前一条消息。比如一条错误消息中可以引用导致错 误的那条请求消息。

端口号

端口号指明了这条消息需要发往哪个对等节点。如果没有指定端口号,那么这条消息会被投 递给同一个协议簇中第一个匹配的内核端套接字。

2.3. 消息类型

Netlink 在请求消息(requests)、通知消息(notifications)和应答消息(replies)的 处理上是有区别的。请求消息设有 NLM_F_REQUEST 标志位,它用来向接收方请求某种响应 。一般来说请求消息都是从用户空间发送到内核的。虽然不是强制规定,但每次发送的请求 消息序列号都应该是上一个序列号加一。

由于请求自身的特性,接收方在收到请求消息之后可能会发送另一条 netlink 消息来响应 这个请求。应答消息的序列号必须和它响应的那条请求消息的序列号一致。

通知消息则没有那么严谨,它不需要应答,所以序列号通常是被设置成 0 的。

A                                     B
|                                     |
|       GET(seq=1,NLM_F_REQUEST)      |
+------------------------------------->
|                                     |
|               PUT(seq=1)            |
<-------------------------------------+
|                                     |
.                                     .
.                                     .
|              NOTIFY(seq=0)          |
<-------------------------------------+
|                                     |

消息的类型主要是由消息头部中 16 位的消息类型字段确定的。Netlink 定义了下面这些标 准的消息类型:

  • NLMSG_NOOP - 无需任何操作,消息必须被丢弃

  • NLMSG_ERROR - 错误消息或者是 ACK,参考 错误消息ACKs 这两个小节

  • NLMSG_DONE - 分段序列的结束,参考 分段消息 一节

  • NLMSG_OVERRUN - 通知信息越界(错误)[Overrun notification]

每个 netlink 协议都可以自由的定义自己的消息类型。需要注意的是小于 NLMSG_MIN_TYPE(0x10) 的类型是保留的,所以不能使用。

通常我们会定义自己的消息类型来实现 RPC 模式。假设你的 netlink 协议的目的是允许你 配置一个网络设备的某些部分,所以你想要提供各种配置选项的读/写访问。完成这项任务 典型的 “netlik 解决方案” 是定义两种消息类型 MSG_SETCFG,MSG_GETCFG:

#define MSG_SETCFG    0x11
#define MSG_GETCFG    0x12

发送一条 MSG_GETCFG 请求消息通常会收到一条包含当前配置信息的类型为 MSG_SETCFG 的应答消息。用面向对象的术语来说这叫做“内核在用户空间设置了配置信息 的本地拷贝”

A                                     B
|                                     |
|   MSG_GETCFG(seq=1,NLM_F_REQUEST)   |
+------------------------------------->
|                                     |
|          MSG_SETCFG(seq=1)          |
<-------------------------------------+
|                                     |

配置信息可以通过发送一条 MSG_SETCFG 信息来更改,这条消息会收到一条 ACK 响应信息 (参考 ACKs 一节)或是一条错误信息(参考 错误消息 一节)。

A                                           B
|                                           |
| MSG_SETCFG(seq=1,NLM_F_REQUEST,NLM_F_ACK) |
+------------------------------------------->
|                                           |
|                 ACK(seq=1)                |
<-------------------------------------------+
|                                           |
|                                           |

此外,内核也可以在配置信息发生改变的时候发送通知信息,这样用户空间应用程序就可以 使用监听而不是频繁轮询的方式来获取改变信息。通知消息通常是使用目前已有的消息类型 这就需要应用程序使用不同的套接字处理请求消息和通知消息,当然你也可以使用特殊的消 息类型来单独表示通知信息。

A                             B
|       MSG_SETCFG(seq=0)     |
<-----------------------------+
|                             |

2.3.1 分段式消息

虽然理论上一条 netlink 消息的最大长度是 4GiB,套接字的缓冲区一般不太可能有这么大 的空间来容纳这么长的消息。所以通常情况下,消息的长度被限制在页的大小(PAGE_SIZE )之内,然后通过使用分段机制把大的数据切分成多个消息。分段式消息设有 NLM_F_MULTI 标志位,接收者需要不断的接收和解析消息直到收到一个消息类型为 NLMSG_DONE 的特殊消 息为止。

和分片之后的 ip 包不一样的是分段之后的消息不需要重新组合,不过如果协议需要这么做 的话,重组当然也是完全合法的。分段式消息经常会被用来发送对象列表或者是对象树,这 种情况下消息段只是简单的承载几个对象,这也就允许每个消息段被单独处理。

A                                B
|                                |
|    GET(seq=1,NLM_F_REQUEST)    |
+-------------------------------->
|                                |
|     PUT(seq=1,NLM_F_MULTI)     |
<--------------------------------+
|                                |
.                                .
.                                .
|     PUT(seq=1,NLM_F_MULTI)     |
<--------------------------------+
|                                |
|       NLMSG_DONE(seq=1)        |
<--------------------------------+
|                                |
|                                |

2.3.2 错误消息

错误消息可以作为请求消息的响应发送出去,错误消息必须使用标准的消息类型 NLMSG_ERROR,它的有效载荷是由错误码和原来的请求消息头部组成。

错误消息

错误消息的序列号必须是导致错误发生的请求消息的序列号。

2.3.3 ACKs

发送方可以通过设置 NLM_F_ACK 标志位来要求接收方为处理过的每一个请求消息都发送一 个 ACK 消息。这种方式通常是用来让发送方在请求被接收方处理之后同步下一步的操作。

ACK 消息和错误消息使用一样的消息类型(NLMSG_ERROR)和负荷格式,不同的是 ACK 消息 的错误代码被设置为 0。

2.3.4 消息标志

标准的消息标志包括下面这些:

#define NLM_F_REQUEST     1
#define NLM_F_MUL     2
#define NLM_F_ACK     4
#define NLM_F_ECHO     8
  • NLM_F_REQUEST - 这个消息是请求消息,参考 消息类型 一节。

  • NLM_F_MULTI - 这个消息是分段式消息,参考 分段式消息 一节。

  • NLM_F_ACK - 请求了 ACK 回复,参考 ACKs 一节。

  • 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 的序列号并不强制使用。序列号唯一的用途就是把一条 应答消息和相应的请求消息联系起来。更多信息详见 消息类型 一节。

序列号是以单个套接字为基础来管理的,关于如何使用序列号,请参考第三章中的 序列号 一小节。

2.5. 多播组

TODO

参考 多播组订阅 一节。