一. 简介

netlink协议是一种基于套接字的IPC机制,多用于用户空间进程与内核之间的通信。用户态进程偶尔会向内核请求某个对象的列表,例如ip route list table allnf-ct-list

内核针对这种不指明单个对象的请求,提供了一种dump的方法,这种dump方法将大块的数据拆分为多个消息(通常将消息限制为一个页大小(page_size)),每个消息设置标志位NLM_F_MULTI,期望接收方继续接收和解析,直到接收到特殊消息类型NLMSG_DONE。每条消息包含一个或多个对象的完整信息,允许接收方独立解析每个消息。

本文将以RTM_GETLINK为例子,详细分析以下问题:

  • 如何创建一个dump请求
  • netlink协议如何处理多部分消息
  • 用户空间应当如何接收分段消息

二. RTM_GETLINK请求

2.1 构建RTM_GETLINK请求

如果对netlink协议不熟悉,建议先点此了解netlink协议

我将创建一个协议为NETLINK_ROUTE的netlink sock,构建一个nlmsgtype为RTM_GETLINK, nlmsg_flags为NLM_F_DUMP的Netlink header对象。
根据NETLINK_ROUTE子协议,他有一个固定的十六字节的协议头struct ifinfomsg作为netlink header的payload。

1
2
3
4
5
6
7
8
9
10
11
-- Debug: Sent Message:
-------------------------- BEGIN NETLINK MESSAGE ---------------------------
[NETLINK HEADER] 16 octets
.nlmsg_len = 32
.type = 18 <0x12>
.flags = 773 <REQUEST,ACK,ROOT,MATCH>
.seq = 1
.port = -800140975
[PAYLOAD] 16 octets
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
--------------------------- END NETLINK MESSAGE ---------------------------

至此,我们已经构造好了总计32字节的消息,准备调用sendmsg发送请求。

2.2 发送消息、构建回复skb、并发送

本文的重点并不是sendmsg流程,我这里简单描述一下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
# tracer: function_graph
#
# CPU TASK/PID DURATION FUNCTION CALLS
# | | | | | | | | |
2) my_link-686091 | | __sys_sendmsg() {
/* 通过fd找到对应的struct socket */
2) my_link-686091 | | sockfd_lookup_light();
2) my_link-686091 | | __sock_sendmsg() {
/* 交给netlink协议的sendmsg来处理业务 */
2) my_link-686091 | | netlink_sendmsg() {
/* 创建skb,将用户空间的请求拷贝到内核空间 */
2) my_link-686091 | | __alloc_skb();
/* netlink单播 */
2) my_link-686091 | | netlink_unicast() {
/* 通过NETLINK_ROUTE协议找到内核用来接收次协议的struct sock */
2) my_link-686091 | | __netlink_lookup();
/* 调用NETLINK_ROUTE的接受函数 */
2) my_link-686091 | | rtnetlink_rcv() {
/* netlink协议的recv函数 */
2) my_link-686091 | | netlink_rcv_skb() {
/* NETLINK_ROUTE协议的recv函数 */
2) my_link-686091 | | rtnetlink_rcv_msg() {
/* netlink协议的dump入口 */
2) my_link-686091 | | __netlink_dump_start() {
/* 通过NETLINK_CB(skb).portid找到用户态用来接受消息的struct sock */
2) my_link-686091 | | __netlink_lookup();
/* netlink协议的dump函数 */
2) my_link-686091 | | netlink_dump() {
/* 创建skb,开始构造返回消息 */
2) my_link-686091 | | __alloc_skb();
/* NETLINK_ROUTE协议的dump方法 */
2) my_link-686091 | | rtnl_dump_ifinfo() {
/* 构建ifindex为1的消息 */
2) my_link-686091 | | rtnl_fill_ifinfo();
/* 构建ifindex为2的消息 */
2) my_link-686091 | | rtnl_fill_ifinfo();
/* 构建ifindex为3的消息 */
2) my_link-686091 | | rtnl_fill_ifinfo();
/* 构建ifindex为4的消息 */
2) my_link-686091 | | rtnl_fill_ifinfo();
2) my_link-686091 | | __netlink_sendskb() {
/* 把新的sbk加入到sock的队尾*/
2) my_link-686091 | | skb_queue_tail();
2) my_link-686091 | 6.493 us | } /* __netlink_sendskb */
2) my_link-686091 | ! 256.386 us | } /* netlink_dump */
2) my_link-686091 | ! 262.652 us | } /* __netlink_dump_start */
2) my_link-686091 | ! 267.100 us | } /* rtnetlink_rcv_msg */
2) my_link-686091 | ! 268.442 us | } /* netlink_rcv_skb */
2) my_link-686091 | ! 269.158 us | } /* rtnetlink_rcv */
2) my_link-686091 | ! 278.392 us | } /* netlink_unicast */
2) my_link-686091 | ! 300.308 us | } /* netlink_sendmsg */
2) my_link-686091 | ! 305.762 us | } /* __sock_sendmsg */
2) my_link-686091 | ! 315.666 us | } /* __sys_sendmsg */

2.3 用户态接受消息

我将忽略每条netlink消息携带的devinfo信息,仅展示收到的消息头

主要关注以下三点:

  • 所有消息头的flags字段都有MULTI标记——这意味着本条消息是dump消息,后续可能还会有数据包,需要用户判断是否收到NONE的标志,否则应当继续接收。
  • 最后一条消息的flag字段被置为DONE——这意味着本条消息就是多部分消息中的最后一条,针对之前的请求,用户已经没有数据需要接收了。
  • seq都是1——这意味着这些分部的消息都是针对之前的RTM_GETLINK请求进行的回复
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
-- Debug: Received Message:
-------------------------- BEGIN NETLINK MESSAGE ---------------------------
[NETLINK HEADER] 16 octets
.nlmsg_len = 1392
.type = 16 <0x10>
.flags = 2 <MULTI>
.seq = 1
.port = -800140975
[PAYLOAD] 1376 octets
/* 此处携带lo接口的信息 */
--------------------------- END NETLINK MESSAGE ---------------------------
-- Debug: Received Message:
-------------------------- BEGIN NETLINK MESSAGE ---------------------------
[NETLINK HEADER] 16 octets
.nlmsg_len = 1440
.type = 16 <0x10>
.flags = 2 <MULTI>
.seq = 1
.port = -800140975
[PAYLOAD] 1424 octets
/* 此处携带enp2s0接口的信息 */
--------------------------- END NETLINK MESSAGE ---------------------------
-- Debug: Received Message:
-------------------------- BEGIN NETLINK MESSAGE ---------------------------
[NETLINK HEADER] 16 octets
.nlmsg_len = 1436
.type = 16 <0x10>
.flags = 2 <MULTI>
.seq = 1
.port = -800140975
[PAYLOAD] 1420 octets
/* 此处携带wlp3s0接口的信息 */
--------------------------- END NETLINK MESSAGE ---------------------------
-- Debug: Received Message:
-------------------------- BEGIN NETLINK MESSAGE ---------------------------
[NETLINK HEADER] 16 octets
.nlmsg_len = 1824
.type = 16 <0x10>
.flags = 2 <MULTI>
.seq = 1
.port = -800140975
[PAYLOAD] 1808 octets
/* 此处携带docker0接口的信息 */
--------------------------- END NETLINK MESSAGE ---------------------------
-- Debug: Received Message:
-------------------------- BEGIN NETLINK MESSAGE ---------------------------
[NETLINK HEADER] 16 octets
.nlmsg_len = 20
.type = 3 <DONE>
.flags = 2 <MULTI>
.seq = 1
.port = -800140975
[PAYLOAD] 4 octets
00 00 00 00 ....
--------------------------- END NETLINK MESSAGE ---------------------------
-- Debug: End of multipart message block: type=DONE length=20 flags=<MULTI> sequence-nr=1 pid=3494826321

2.4 总结

尽管在理论上,网络链接消息的大小可以高达4GiB。但套接字缓冲区很可能不够大,无法容纳这样大小的消息。因此,通常将消息限制为一个页面大小(page_size),并使用多部分机制将大块数据拆分为多个消息。多部分消息具有设置的标志NLM_F_MULTI,并且期望接收方继续接收和解析,直到接收到特殊消息类型NLMSG_DONE。
通常,多部分消息用于发送对象的列表或树,因为每个多部分消息只携带多个对象,允许独立解析每个消息。

Multipart Messages

三. 软件实现

本节将从代码的角度出发,看一看第二节中的消息是如何构建、发送、接收、并解析的:hushed:。

3.1 用户态程序

由于希望对netlink协议有更深的理解,测试程序不会调用libnl封装的接口函数,直接调用libc库,点击此处下载测试代码.

我们将创建一个类型为RTM_GETLINK的netlink请求,内核将返回所有interface信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
User space                              Kernel space
| |
| nlmsg_type=RTM_GETLINK |
|---------------->----------------------|
| |
| nlmsg_flags=NLM_F_MULTI |
|----------------<----------------------|
| |
| nlmsg_flags=NLM_F_MULTI |
|----------------<----------------------|
| |
| nlmsg_type=NLMSG_DONE |
|----------------<----------------------|
| |

测试效果如下:

可以看到,内核将四个接口信息通过两条netlink消息返回到了用户进程(有两次recvmsg动作),并且最后发送了一个NLMSG_DONE的标志,用户态进程将对每次recvmsg的消息进行遍历,解析完所有netlink msg。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
USER@debian:~$ ./my_link_list 
recvmsg len is 2832
[my_recvmsg_msg_cb][36] parse_netlink_msg_count is 1
ifindex is 1
[my_recvmsg_msg_cb][36] parse_netlink_msg_count is 2
ifindex is 2

recvmsg len is 3260
[my_recvmsg_msg_cb][36] parse_netlink_msg_count is 3
ifindex is 3
[my_recvmsg_msg_cb][36] parse_netlink_msg_count is 4
ifindex is 4

recvmsg len is 20
recv NLMSG_DONE, stop recvmsg

1. 创建socket

1
2
int sock;
sock = socket(AF_NETLINK, SOCK_RAW , NETLINK_ROUTE);

2. 构建请求消息

1
2
3
4
5
6
7
8
9
10
11
12
13
struct my_request {
struct nlmsghdr nlh;
struct ifinfomsg ifh;
} r = {
.nlh = {
.nlmsg_len = sizeof(struct my_request),
.nlmsg_type = RTM_GETLINK,
.nlmsg_flags = NLM_F_REQUEST|NLM_F_ACK|NLM_F_DUMP,
.nlmsg_seq = 1,
.nlmsg_pid = 0,
},
.ifh = { .ifi_family = AF_UNSPEC}
};

3. 发送请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
static void my_sendmsg(int sock, void *nlh, int len)
{
struct iovec iov = {
.iov_base = nlh,
.iov_len = len,
};
struct sockaddr_nl peer = {
.nl_family = AF_NETLINK,
.nl_pid = 0,
};
struct msghdr hdr = {
.msg_name = (void *)&peer,
.msg_namelen = sizeof(struct sockaddr_nl),
.msg_iov = &iov,
.msg_iovlen = 1,
};
sendmsg(sock, &hdr, 0);
}

cb回调函数配合user指针来控制是否需要继续进行recvmsg动作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
typedef int (*my_recvmsg_msg_cb_t)(struct nlmsghdr *msg, void *arg);

static void my_recvmsg_one(int sock, my_recvmsg_msg_cb_t cb, void *user)
{
int n;
struct sockaddr_nl nla = {0};
struct iovec iov;
struct msghdr msg = {
.msg_name = (void *)&nla,
.msg_namelen = sizeof(struct sockaddr_nl),
.msg_iov = &iov,
.msg_iovlen = 1,
};
iov.iov_len = getpagesize();
iov.iov_base = malloc(iov.iov_len);
n = recvmsg(sock, &msg, 0);

printf("recvmsg len is %d\n", n);
cb(iov.iov_base, n, user);
putchar('\n');
free(iov.iov_base);
}

5. 解析回调函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
static void my_parse_netdev_one(struct ifinfomsg *ifinfo)
{
printf("\t\tifindex is %d\n", ifinfo->ifi_index);
return ;
}

static void my_recvmsg_msg_cb(struct nlmsghdr *nlh, int len, void *arg)
{
struct ifinfomsg *data;
/* 收到NLMSG_DONE消息,应该停止recvmsg了 */
if (nlh->nlmsg_type == NLMSG_DONE)
{
printf("\trecv NLMSG_DONE, stop recvmsg\n");
*(int *)arg = 1;
return ;
}

while (NLMSG_OK(nlh, len))
{
data = NLMSG_DATA(nlh);
printf("\t[%s][%d] parse_netlink_msg_count is %d\n", __func__, __LINE__, ++parse_netlink_msg_count);
my_parse_netdev_one(data);

nlh = NLMSG_NEXT(nlh, len);
}

return ;
}

3.2 内核态实现

内核netlink模块与rtnetlink模块之间的调用关系可以参考2.2小节的发送消息、构建回复skb、并发送

本节将主要分析下rtnetlink是如何决定一个skb中包含有几个netlinkmsg实例的。

下一篇文章再分析吧,太复杂了:scream:。