由于开发和维护内核的复杂性,只用最为关键同时对性能要求最高的代码才会放在内核中。其他的诸如GUI,管理和控制代码,通常放在用户空间运行。这种将实现分离在内核和用户空间的思想在Linux中非常常见。现在的问题是内核代码和用户代码如果彼此通信。
答案是内核空间和用户空间存在的各种IPC方法,例如系统调用,ioctl,proc文件系统和netlink socket。这篇文章讨论netlink socket和讨论其作为一种网络特征IPC的优势。
简介
Netlink socket是用于内核和用户空间之间交换信息的特殊的IPC机制。它提供了一种全复用的通信链路。和TCP/IP使用的地址族AF_INET相 比,Netlink socket使用地址族AF_NETLINK,每个的netlink socket特征定义协议类型在内核头文件中include/linux/netlink.h
以下是netlink socket当前支持的特征和他们的协议类型的子集:
- NETLINK_ROUTE:用户空间路由damon,如BGP,OSPF,RIP和内核包转发模块的通信信道。用户空间路由damon通过此种netlink协议类型更新内核路由表
- NETLINK_FIREWALL:接收IPv4防火墙代码发送的包
- NETLINK_NFLOG:用户空间iptable管理工具和内核空间Netfilter模块的通信信道
- NETLINK_ARPD:用户空间管理arp表
为 什么以上的特征使用netlink而不是系统调用,ioctl或者proc文件系统来完成通信?为新特性添加系统调用,ioctl和proc文件系统相对 而言是一项比较复杂的工作,我们冒着污染内核和损害系统稳定性的风险。netlink socket相对简单:只有一个常量,协议类型,需要加入到netlink.h中。然后,内核模块和用户程序可以通过socket类型的API进行通信。
和 其他socket API一样,Netlink是异步的,它提供了一个socket队列来平滑突发的信息。发送一个netlink消息的系统调用将消息排列到接受者的 netlink队列中然后调用接收者的接收处理函数。接收者,在接收处理函数的上下文中,可以决定是否立即处理该消息还是等待在另一个上下文中处理。不想 netlink,系统调用需要同步处理。因此,如果我们使用了一个系统来传递一条消息到内核,如果需要处理该条信息的时间很长,那么内核调度粒度可以会受 影响。
在内核中实现的系统调用代码在编译时被静态的链接到内核中,因此在一个可以动态加载的模块中包括系统调用代码是不合适的。在netlink socket中,内核中的netlink核心和在一个可加载的模块中没有编译时的相互依赖。
netlink socket支持多播,这也是其与其他交互手段相比较的优势之一。一个进程可以将一条消息广播到一个netlink组地址。任意多的进程可以监听那个组地址。这提供了一种从内核到用户空间进行事件分发接近完美的机制。
从 会话只能由用户空间应用发起的角度来看,系统调用和ioctl是单一的IPC。但是,如果一个内核模块有一个用户空间应用的紧急消息,没有一种直接的方法 来实现这些功能。通常,应用需要阶段性的轮询内核来获取状态变化,尽管密集的轮询会有很大的开销。netlink通过允许内核初始化一个对话来优雅的解决 这个问题。我们称之为netlink的复用特性。
最后,netlink提供了bsd socket风格的API,而这些API是被软件开发社区所熟知。因此,培训费用相较较小。
和BSD路由socket的关系
在BSC TCP/IP的栈实现中,有一种叫做路由套接字的特殊的socket。它有AF_ROUTE地址族,PF_ROUTE协议族和SOCK_RAWsocket类型。在bsd中,路由套接字用于在内核路由表中添加和删除路由。
在Linux中,路由套接字的实现通过netlink套接字的NETLINK_ROUTE协议类型来支持。netlink套接字提供了bsd路由套接字的功能的超集。
Netlink套接字API
标 准的套接字API,socket(),sendmsg(),recvmsg()和close(),可以被用户态程序使用。可以通过查询man手册页来看这 些函数的具体定义。这儿,我们讨论在netlink上下文中为这些API选择参数。对于写过TCP/IP套接字程序的人对这些API都应该非常熟悉。
创建一个套接字,
int socket(int domain,int type, int protocol)
domain指代地址族,AF_NETLINK,套接字类型不是SOCK_RAW就是SOCK_DGRAM,因为netlink是一个面向数据报的服务。
protocol选择该套接字使用那种netlink特征。以下是几种预定义的协议类型:NETLINK_ROUTE,NETLINK_FIREWALL,NETLINK_APRD,NETLINK_ROUTE6_FW。你也可以非常容易的添加自己的netlink协议。
为 每一个协议类型最多可以定义32个多播组。每一个多播组用一个bit mask来表示,1<<i(0<= i<= 31),这在一组进程和内核进程协同完成一项任务时非常有用。发送多播netlink消息可以减少系统调用的数量,同时减少用来维护多播组成员信息的负 担。
bind()
和TCP/IP套接字一样,netlink bind()API用来将一个本地socket地址和一个打开的socket关联。一个netlink地址结构如下所示:
1: struct sockaddr_nl
2: {
3: sa_family_t nl_family; /* AF_NETLINK */
4: unsigned short nl_pad; /* zero */
5: __u32 nl_pid; /* process pid */
6: __u32 nl_groups; /* mcast groups mask */
7: } nladdr;
当使用bind()调用的时候,nl_pid域可以被赋值为调用进程的pid。nl_pid在这儿被当做该netlink套接字的本地地址。程序负责找一个独一无二的32位整数在填充该域。一种常见的做法是:
1: NL_PID Formula 1: nl_pid = getpid();
公式一使用进程ID号作为nl_pid的值,如果说该进程只需要一个netlink套接字,这是一个自然的选择。当一个进程中的不同线程需要同一个netlink协议多个netlink套接字。公式而可以用来产生nl_pid号:
1: NL_PID Formula 2: pthread_self() << 16 | getpid();
通过这种方法,同一个进程中的不同线程可以有同一种netlink协议类型的netlink套接字。事实上,即使在同一个线程中,在可能使用同一种协议类型的多个套接字。开发者必须要更有创造力来产生一个唯一的nl_pid。
如果应用想接受发送给特定多播组的netlink消息,所有感兴趣的多播组bit应该or在一起,并填充到nl_groups域。否则, nl_groups应该被显式至零,说明该应用只接受到该应用的消息,填充完上述域, 使用如下方式进行绑定:
1: bind(fd, (struct sockaddr*)&nladdr, sizeof(nladdr));
发送一条netlink消息
为了发送一条netlink消息到内核或者其他的用户空间进程,另外一个struct sockaddr_nl nladdr需要作为目的地址,这和使用sendmsg()发送一个UDP包是一样的。如果该消息是发送至内核的,那么nl_pid和nl_groups 都置为0.
如果说消息时发送给另一个进程的单播消息,nl_pid是另外一个进程的pid值而nl_groups为零。
如果消息是发送给一个或多个多播组的多播消息,所有的目的多播组必须bitmask必须or起来从而形成nl_groups域。当我们填充struct msghdr结构用于sendmsg时,使用如下:
1: struct msghdr msg;
2: msg.msg_name = (void *)&(nladdr);
3: msg.msg_namelen = sizeof(nladdr);
netlink套接字也需要它自己本身的消息头,这是为了给所有协议类型的netlink消息提供一个统一的平台。
因为Linux内核netlink核心假设每个netlink消息中存在着以下的头,所有应用也必须在其发送的消息中提供这些头信息:
1: struct nlmsghdr
2: {
3: __u32 nlmsg_len; /* Length of message */
4: __u16 nlmsg_type; /* Message type*/
5: __u16 nlmsg_flags; /* Additional flags */
6: __u32 nlmsg_seq; /* Sequence number */
7: __u32 nlmsg_pid; /* Sending process PID */
8: };
nlmsg_len指整个netlink消息的长度,包括头信息,这也是netlink核心所必须的。nlmsg_type用于应用但是对于 netlink核心而言其是透明的。nlmsg_flags用于给定附加的控制信息,其被netlink核心读取和更新。nlmsg_seq和 mlmsg_pid,应用用来跟踪消息,这些对于netlink核心也是透明的。
所以一个netlink消息由消息头和消息负载组成。一旦一个消息被加入,它就加入到一个通过nlh指针指向的缓冲区。我们也可以将消息发送到struct msghdr msg:
1: struct iovec iov;
2:
3: iov.iov_base = (void *)nlh;
4: iov.iov_len = nlh->nlmsg_len;
5:
6: msg.msg_iov = &iov;
7: msg.msg_iovlen = 1;
经过以上步骤,调用sendmsg()函数来发送netlink消息:
接收netlink消息
一个接收程序必须分配一个足够大的内存用于保存netlink消息头和消息负载。然后其填充struct msghdr msg,然后使用标准的recvmsg()函数来接收netlink消息,假设缓存通过nlh指针指向:
1: struct sockaddr_nl nladdr;
2: struct msghdr msg;
3: struct iovec iov;
4:
5: iov.iov_base = (void *)nlh;
6: iov.iov_len = MAX_NL_MSG_LEN;
7: msg.msg_name = (void *)&(nladdr);
8: msg.msg_namelen = sizeof(nladdr);
9:
10: msg.msg_iov = &iov;
11: msg.msg_iovlen = 1;
12: recvmsg(fd, &msg, 0);
当消息被正确的接收之后,nlh应该指向刚刚接收到的netlink消息的头。nladdr应该包含接收消息的目的地址,其中包括了消息发送者的 pid和多播组。同时,宏NLMSG_DATA(nlh),定义在netlink.h中,返回一个指向netlink消息的负载的指针。 close(fd)调用关闭fd描述符所标识的socket。
内核空间netlink API
内核空间的netlinkAPI在内核中被netlink核心支持,即net/core/af_netlink.c。从内核角度看,这些API不同 于用户空间的API。这些API可以被内核模块使用从而存取netlink套接字与用户空间程序通信。除非你使用现存的netlink套接字协议类型,否 则你必须通过在netlink.h中定义一个常量来添加你自己的协议类型。例如,我们需要添加一个netlink协议类型用于测试,则在 netlink.h中加入下面的语句:
1: #define NETLINK_TEST 17
之后,亦可以在linux内核中的任何地方引用添加的协议类型。
在用户空间,我们使用socket()来创建一个netlink套接字,但是在内核空间,我们使用如下的API:
1: truct sock *
2: netlink_kernel_create(int unit,
3: void (*input)(struct sock *sk, int len));
参数unit,即为netlink协议类型,如NETLINK_TEST,回调函数会在消息到达netlink套接字时调用。当用户态程序发送一个NETLINK_TEST协议类型的消息给内核时,input()函数被调用。下面是一个实现回调函数的例子:
1: void input (struct sock *sk, int len)
2: {
3: struct sk_buff *skb;
4: struct nlmsghdr *nlh = NULL;
5: u8 *payload = NULL;
6:
7: while ((skb = skb_dequeue(&sk->receive_queue))
8: != NULL) {
9: /* process netlink message pointed by skb->data */
10: nlh = (struct nlmsghdr *)skb->data;
11: payload = NLMSG_DATA(nlh);
12: /* process netlink message with header pointed by
13: * nlh and payload pointed by payload
14: */
15: }
16: }
input()函数在发送进程的sendmsg()系统调用上下文执行。如果在input中处理netlink消息非常快,那是没有问题的。如果处 理netlink消息需要很长的时间,我们希望在input()外面处理消息来避免阻塞其他系统调用进入内核。事实上,我们可以使用一个指定的内核线程来 来不断执行以下的步骤。使用skb=skb_recv_datagram(nl_sk),其中nl_sk是 netlink_kernel_create()返回的netlink套接字。然后,处理由skb->data指向的netlink消息。
以下的内核线程在没有netlink消息在nl_sk中时睡眠,在回调函数input中,我们只要唤醒睡眠的内核线程,如下所示:
1: void input (struct sock *sk, int len)
2: {
3: wake_up_interruptible(sk->sleep);
4: }
这是一个更具有扩展性的用户和内核通信的模型。其也提高了上下文交换的粒度。
在内核中发送netlink消息
真如在用户空间中一样,在发送一个netlink消息时需要设置源和目的netlink消息地址。假设socket缓存中包含了将要发送的netlink消息,本地地址可以通过以下方式设置:
1: NETLINK_CB(skb).groups = local_groups;
2: NETLINK_CB(skb).pid = 0; /* from kernel */
目的地址可以如下设置:
1: NETLINK_CB(skb).dst_groups = dst_groups;
2: NETLINK_CB(skb).dst_pid = dst_pid;
这些信息不是存储在skb->data,而是存储在skb中的netlink控制块中。发送一个消息,使用:
1: int
2: netlink_unicast(struct sock *ssk, struct sk_buff
3: *skb, u32 pid, int nonblock);
其中ssk是netlink_kernel_create返回的netlink套接字,skb->data指向netlink将要发送的消息而pid是接受该消息的用户程序id。nonblock用于标识在接收缓存不可用时,API是阻塞还是立即返回失败。
你也可以发送一个多播消息。以下的API用于将消息传送到指定的进程,同时多播至指定的多播组。
1: void
2: netlink_broadcast(struct sock *ssk, struct sk_buff
3: *skb, u32 pid, u32 group, int allocation);
group是所有接收多播组的bitmask。allocation是内核内存分配的类型。通常,GFP_ATOMIC用于中断上下文而在其他情况下是用GFP_KERNEL.只是因为API可能需要分配一个或者多个套接字缓存来克隆多播消息。
在内核中关闭一个netlink套接字
给定了netlink_kernel_create()函数返回的struct sock *nl_sk,我们可以通过调用以下的API关闭netlink套接字。
1: sock_release(nl_sk->socket);
在内核和用户态使用单播通信
1: #include <sys/socket.h>
2: #include <linux/netlink.h>
3:
4: #define MAX_PAYLOAD 1024 /* maximum payload size*/
5: struct sockaddr_nl src_addr, dest_addr;
6: struct nlmsghdr *nlh = NULL;
7: struct iovec iov;
8: int sock_fd;
9:
10: void main() {
11: sock_fd = socket(PF_NETLINK, SOCK_RAW,NETLINK_TEST);
12:
13: memset(&src_addr, 0, sizeof(src_addr));
14: src__addr.nl_family = AF_NETLINK;
15: src_addr.nl_pid = getpid(); /* self pid */
16: src_addr.nl_groups = 0; /* not in mcast groups */
17: bind(sock_fd, (struct sockaddr*)&src_addr,
18: sizeof(src_addr));
19:
20: memset(&dest_addr, 0, sizeof(dest_addr));
21: dest_addr.nl_family = AF_NETLINK;
22: dest_addr.nl_pid = 0; /* For Linux Kernel */
23: dest_addr.nl_groups = 0; /* unicast */
24:
25: nlh=(struct nlmsghdr *)malloc(
26: NLMSG_SPACE(MAX_PAYLOAD));
27: /* Fill the netlink message header */
28: nlh->nlmsg_len = NLMSG_SPACE(MAX_PAYLOAD);
29: nlh->nlmsg_pid = getpid(); /* self pid */
30: nlh->nlmsg_flags = 0;
31: /* Fill in the netlink message payload */
32: strcpy(NLMSG_DATA(nlh), "Hello you!");
33:
34: iov.iov_base = (void *)nlh;
35: iov.iov_len = nlh->nlmsg_len;
36: msg.msg_name = (void *)&dest_addr;
37: msg.msg_namelen = sizeof(dest_addr);
38: msg.msg_iov = &iov;
39: msg.msg_iovlen = 1;
40:
41: sendmsg(fd, &msg, 0);
42:
43: /* Read message from kernel */
44: memset(nlh, 0, NLMSG_SPACE(MAX_PAYLOAD));
45: recvmsg(fd, &msg, 0);
46: printf(" Received message payload: %s\n",
47: NLMSG_DATA(nlh));
48:
49: /* Close Netlink Socket */
50: close(sock_fd);
51: }
1: struct sock *nl_sk = NULL;
2:
3: void nl_data_ready (struct sock *sk, int len)
4: {
5: wake_up_interruptible(sk->sleep);
6: }
7:
8: void netlink_test() {
9: struct sk_buff *skb = NULL;
10: struct nlmsghdr *nlh = NULL;
11: int err;
12: u32 pid;
13:
14: nl_sk = netlink_kernel_create(NETLINK_TEST,
15: nl_data_ready);
16: /* wait for message coming down from user-space */
17: skb = skb_recv_datagram(nl_sk, 0, 0, &err);
18:
19: nlh = (struct nlmsghdr *)skb->data;
20: printk("%s: received netlink message payload:%s\n",
21: __FUNCTION__, NLMSG_DATA(nlh));
22:
23: pid = nlh->nlmsg_pid; /*pid of sending process */
24: NETLINK_CB(skb).groups = 0; /* not in mcast group */
25: NETLINK_CB(skb).pid = 0; /* from kernel */
26: NETLINK_CB(skb).dst_pid = pid;
27: NETLINK_CB(skb).dst_groups = 0; /* unicast */
28: netlink_unicast(nl_sk, skb, pid, MSG_DONTWAIT);
29: sock_release(nl_sk->socket);
30: }
在内核和用户态使用单播通信
1: #include <sys/socket.h>
2: #include <linux/netlink.h>
3:
4: #define MAX_PAYLOAD 1024 /* maximum payload size*/
5: struct sockaddr_nl src_addr, dest_addr;
6: struct nlmsghdr *nlh = NULL;
7: struct iovec iov;
8: int sock_fd;
9:
10: void main() {
11: sock_fd=socket(PF_NETLINK, SOCK_RAW, NETLINK_TEST);
12:
13: memset(&src_addr, 0, sizeof(local_addr));
14: src_addr.nl_family = AF_NETLINK;
15: src_addr.nl_pid = getpid(); /* self pid */
16: /* interested in group 1<<0 */
17: src_addr.nl_groups = 1;
18: bind(sock_fd, (struct sockaddr*)&src_addr,
19: sizeof(src_addr));
20:
21: memset(&dest_addr, 0, sizeof(dest_addr));
22:
23: nlh = (struct nlmsghdr *)malloc(
24: NLMSG_SPACE(MAX_PAYLOAD));
25: memset(nlh, 0, NLMSG_SPACE(MAX_PAYLOAD));
26:
27: iov.iov_base = (void *)nlh;
28: iov.iov_len = NLMSG_SPACE(MAX_PAYLOAD);
29: msg.msg_name = (void *)&dest_addr;
30: msg.msg_namelen = sizeof(dest_addr);
31: msg.msg_iov = &iov;
32: msg.msg_iovlen = 1;
33:
34: printf("Waiting for message from kernel\n");
35:
36: /* Read message from kernel */
37: recvmsg(fd, &msg, 0);
38: printf(" Received message payload: %s\n",
39: NLMSG_DATA(nlh));
40: close(sock_fd);
41: }
1: #define MAX_PAYLOAD 1024
2: struct sock *nl_sk = NULL;
3:
4: void netlink_test() {
5: sturct sk_buff *skb = NULL;
6: struct nlmsghdr *nlh;
7: int err;
8:
9: nl_sk = netlink_kernel_create(NETLINK_TEST,
10: nl_data_ready);
11: skb=alloc_skb(NLMSG_SPACE(MAX_PAYLOAD),GFP_KERNEL);
12: nlh = (struct nlmsghdr *)skb->data;
13: nlh->nlmsg_len = NLMSG_SPACE(MAX_PAYLOAD);
14: nlh->nlmsg_pid = 0; /* from kernel */
15: nlh->nlmsg_flags = 0;
16: strcpy(NLMSG_DATA(nlh), "Greeting from kernel!");
17: /* sender is in group 1<<0 */
18: NETLINK_CB(skb).groups = 1;
19: NETLINK_CB(skb).pid = 0; /* from kernel */
20: NETLINK_CB(skb).dst_pid = 0; /* multicast */
21: /* to mcast group 1<<0 */
22: NETLINK_CB(skb).dst_groups = 1;
23:
24: /*multicast the message to all listening processes*/
25: netlink_broadcast(nl_sk, skb, 0, 1, GFP_KERNEL);
26: sock_release(nl_sk->socket);
27: }
Netlink可靠性机制
在基于netlink的通信中,有两种可能的情形会导致消息丢失:
- 内存耗尽,没有足够多的内存分配给消息
- 缓存复写,接收队列中没有空间存储消息,这在内核空间和用户空间之间通信时可能会发生
缓存复写在以下情况很可能会发生:
- 内核子系统以一个恒定的速度发送netlink消息,但是用户态监听者处理过慢
- 用户存储消息的空间过小
如果netlink传送消息失败,那么recvmsg()函数会返回No buffer space available(ENOBUFS)错误。那么,用户空间进程知道它丢失了信息,如果内核子系统支持dump操作,它可以重新同步来获取最新的消息。在 dump操作中,netlink通过在每次调用recvmsg()函数时传输一个包的流控机制来防止接收队列的复写。改包消耗一个内存页,其包含了几个多 部分netlink消息。图6中的序列图显示了在一个重新同步的过程中所使用的dump操作。
另一方面,缓存复写不会发生在用户和内核空间的通信中,因为sendmsg()同步的将netlink消息发送到内核子系统。如果使用的是阻塞套接字,那么netlink在从用户空间到内核空间的通信时完全可靠的,因为内存分配可以等待,所以没有内存耗尽的可能。
netlink也可以提供应答机制。所以如果用户空间进程发送了一个设置了NLM_F_ACK标志的请求,netlink会在netlink错误消息中报告给用户空间刚才请求操作的结果。
从用户空间的角度来看,Netlink套接字在通用的BSD套接字接口之上实现。因此,netlink套接字编程与通用的TCP/IP编程类似。但是,我们也应该考虑几个与netlink相关的特殊问题:
- netlink套接字没有像其他协议一样对用户空间隐藏协议细节。事实上,netlink传递的是整个消息,包括netlink头和其他信 息。因此,这就导致了数据处理函数与通用的TCP/IP套接字不同,所以用户态程序必须根据其格式解析和构建netlink信息。然而,没有标准的工具来 完成这些工作,所以你必须实现自己的函数或者使用一些现成的库。
- 来自netlink和内核子系统的错误不是通过recvmsg()函数返回的整数值来表现的。事实上,错误信息时被包装在netlink错误 消息中的。唯一的例外是(ENOBUFS)错误,该错误不是包装在netlink消息中,因为报告该错误的原因就是我们没有足够的空间来缓存新的 netlink消息。标准的通用套接字错误,如(EAGAIN),通常和其他轮询原语,例如poll()和select(),也是通过recvmsg() 返回整数值。