一. 简介 netlink协议是一种基于套接字的IPC机制,多用于用户空间进程与内核之间的通信 。用户态进程偶尔会向内核请求某个对象的列表,例如ip route list table all
、nf-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。 通常,多部分消息用于发送对象的列表或树,因为每个多部分消息只携带多个对象,允许独立解析每个消息。
三. 软件实现 本节将从代码的角度出发,看一看第二节中的消息是如何构建、发送、接收、并解析的 :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 ); }
4. 解析一条netlink msg 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 ; 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:。