About AF_NETLINK in Linux Socket
由于开发和维护内核的复杂性,只把最为关键同时对性能要求最高的代码放进内核中。其他的诸如GUI,管理和控制代码,通常放在用户空间运行。这种将实现分离在内核和用户空间的思想在Linux中非常常见。现在的问题是内核代码和用户代码如何交互通信。
答案是内核空间和用户空间存在的各种IPC方法,例如系统调用、ioctl、proc文件系统和netlink socket。这篇文章讨论netlink socket及其作为一种网络特征IPC的优势。
Netlink套接字是用以实现用户进程与内核进程通信的一种特殊的进程间通信(IPC) ,也是网络应用程序与内核通信的最常用的接口。
Netlink 是一种特殊的 socket,它是 Linux 所特有的,类似于 BSD 中的AF_ROUTE 但又远比它的功能强大,目前在Linux内核中使用netlink 进行应用与内核通信的应用很多;以下是netlink socket当前支持的特征和他们的协议类型的子集:
1. NETLINK_ROUTE:用户空间路由damon,如BGP,OSPF,RIP和内核包转发模块的通信信道。用户空间路由damon通过此种netlink协议类型更新内核路由表
2. NETLINK_FIREWALL:接收IPv4防火墙代码发送的包
3. NETLINK_NFLOG:用户空间iptable管理工具和内核空间Netfilter模块的通信信道
4. NETLINK_ARPD:用户空间管理arp表
另外,netfilter子系统(NETLINK_NETFILTER),内核事件向用户态通知(NETLINK_KOBJECT_UEVENT),通用 netlink(NETLINK_GENERIC)等。它提供了一种全复用的通信链路,和TCP/IP使用的地址族AF_INET相比,Netlink socket使用地址族AF_NETLINK,每个的netlink socket特征定义协议类型在内核头文件中include/linux/netlink.h。
Netlink 是一种在内核与用户应用间进行双向数据传输的非常好的方式,用户态应用使用标准的 socket API 就可以使用 netlink 提供的强大功能,内核态需要使用专门的内核API 来使用netlink。Netlink相对于系统调用,ioctl 以及 proc文件系统而言具有以下优点:
1、netlink使用简单,只需要在include/linux/netlink.h中增加一个新类型的 netlink 协议定义即可,(如 #define NETLINK_TEST20 然后,内核和用户态应用就可以立即通过 socket API 使用该 netlink 协议类型进行数据交换);
2、netlink是一种异步通信机制,在内核与用户态应用之间传递的消息保存在socket缓存队列中,发送消息只是把消息保存在接收者的socket的接收队列,而不需要等待接收者收到消息;
3、使用 netlink 的内核部分可以采用模块的方式实现,使用 netlink 的应用部分和内核部分没有编译时依赖;
4、netlink 支持多播,内核模块或应用可以把消息多播给一个netlink组,属于该netlink 组的任何内核模块或应用都能接收到该消息,内核事件向用户态的通知机制就使用了这一特性;
5、内核可以使用 netlink 首先发起会话
为什么以上的特征使用netlink而不是系统调用,ioctl或者proc文件系统来完成通信?为新特性添加系统调用,ioctl和proc文件系统相对而言是一项比较复杂的工作,我们冒着污染内核和损害系统稳定性的风险。netlink socket相对简单:只有一个常量,协议类型,需要加入到netlink.h中。然后,内核模块和用户程序可以通过socket类型的API进行通信。
和其他socket API一样,Netlink是异步的,它提供了一个socket队列来平滑突发的信息。发送一个netlink消息的系统调用将消息排列到接受者的netlink队列中然后调用接收者的接收处理函数。接收者,在接收处理函数的上下文中,可以决定是否立即处理该消息还是等待在另一个上下文中处理。而系统调用需要同步处理。因此,如果我们使用系统调用传递一条消息到内核,而且处理该条信息需要很长时间,那么内核调度粒度可能会受影响。
在内核中实现的系统调用代码在编译时被静态的链接到内核中,因此在一个可以动态加载的模块中包括系统调用代码是不合适的。在netlink socket中,内核中的netlink核心和在一个可加载的模块中没有编译时的相互依赖。
netlink socket支持多播,这也是其与其他交互手段相比较的优势之一。一个进程可以将一条消息广播到一个netlink组地址。任意多的进程可以监听那个组地址。这提供了一种从内核到用户空间进行事件分发接近完美的机制。
从会话只能由用户空间应用发起的角度来看,系统调用和ioctl是单一的IPC。但是,如果一个内核模块有一个用户空间应用的紧急消息,没有一种直接的方法来实现这些功能。通常,应用需要阶段性的轮询内核来获取状态变化,尽管密集的轮询会有很大的开销。netlink通过允许内核初始化一个对话来优雅的解决这个问题。我们称之为netlink的复用特性。
最后,netlink提供了bsd socket风格的API,而这些API是被软件开发社区所熟知。因此,培训费用相较较小。
和BSD路由socket的关系
在BSD TCP/IP的栈实现中,有一种叫做路由套接字的特殊的socket。它有AF_ROUTE地址族,PF_ROUTE协议族和SOCK_RAW socket类型。在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个多播组。每一个多播组用一个bitmask来表示,1<
地址绑定bind()
与TCP/IP套接字一样,netlink bind()API用来将一个本地socket地址和一个打开的socket关联。一个netlink地址结构如下所示:
struct sockaddr_nl {
sa_family_t nl_family; /* AF_NETLINK */
unsigned short nl_pad; /* zero */
__u32 nl_pid; /* process pid */
__u32 nl_groups; /* mcast groups mask */
} nladdr;
当使用bind()调用的时候,字段 nl_pad 当前没有使用,因此要总是设置为 0,字段 nl_pid 为接收或发送消息的进程的 ID,把该字段设置为 0,告知内核去选择本地临时端口号【getsockname可以返回由内核赋予的本地端口号】,否则设置为处理消息的进程 ID,一般为本进程的进程 ID,这相当于 netlink socket 的本地地址。字段 nl_groups 用于指定多播组,bind 函数用于把调用进程加入到该字段指定的多播组,如果设置为 0,表示调用者不加入任何多播组。
nl_pid在这里被当做该netlink套接字的本地地址。程序负责找一个独一无二的32位整数在填充该域。一种常见的做法是:
nl_pid = getpid();
公式一使用进程ID号作为nl_pid的值,如果说该进程只需要一个netlink套接字,这是一个自然的选择。当一个进程中的不同线程需要同一个netlink协议多个netlink套接字。公式而可以用来产生nl_pid号:
pthread_self() << 16 | getpid();
通过这种方法,同一个进程中的不同线程可以有同一种netlink协议类型的netlink套接字。事实上,即使在同一个线程中,在可能使用同一种协议类型的多个套接字。开发者必须要更有创造力来产生一个唯一的nl_pid。
如果应用想接受发送给特定多播组的netlink消息,所有感兴趣的多播组bit应该or在一起,并填充到nl_groups域。否则, nl_groups应该被显式至零,说明该应用只接受到该应用的消息,填充完上述域,使用如下方式进行绑定:
bind(fd, (struct sockaddr*)&nladdr, sizeof(nladdr));
发送netlink消息
为了发送一条netlink消息到内核或者其他的用户空间进程,另外一个structsockaddr_nl nladdr需要作为目的地址,这和使用sendmsg()发送一个UDP包是一样的。如果该消息是发送至内核的,那么nl_pid和nl_groups都置为0.
如果消息是发送给另一个进程的单播消息,nl_pid是另外一个进程的pid值而nl_groups为零。
如果消息是发送给一个或多个多播组的多播消息,所有的目的多播组必须bitmask必须or起来从而形成nl_groups域。当我们填充structmsghdr结构用于sendmsg时,使用如下:
struct msghdr msg;
msg.msg_name = (void *)&(nladdr);
msg.msg_namelen = sizeof(nladdr);
netlink套接字也需要它自己本身的消息头,这是为了给所有协议类型的netlink消息提供一个统一的平台。
因为Linux内核netlink核心假设每个netlink消息中存在着以下的头,所有应用也必须在其发送的消息中提供这些头信息:
struct nlmsghdr
{
__u32 nlmsg_len; /* Length of message */
__u16 nlmsg_type; /* Message type*/
__u16 nlmsg_flags; /* Additional flags */
__u32 nlmsg_seq; /* Sequence number */
__u32 nlmsg_pid; /* Sending process PID */
};
nlmsg_len指整个netlink消息的长度,包括头信息,这也是netlink核心所必须的。nlmsg_type用于应用但是对于netlink核心而言其是透明的。nlmsg_flags用于给定附加的控制信息,其被netlink核心读取和更新。nlmsg_seq和nlmsg_pid,应用用来跟踪消息,这些对于netlink核心也是透明的。
所以一个netlink消息由消息头和消息负载组成。一旦一个消息被加入,它就加入到一个通过nlh指针指向的缓冲区。我们也可以将消息发送到struct msghdr msg:
struct iovec iov;
struct msghdr msg;
iov.iov_base = (void *)nlh;
iov.iov_len = nlh->nlmsg_len;
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
经过以上步骤,调用sendmsg()函数来发送netlink消息:
sendmsg(fd, &msg, 0);
接收netlink消息
一个接收程序必须分配一个足够大的内存用于保存netlink消息头和消息负载。然后其填充structmsghdr msg,再使用标准的recvmsg()函数来接收netlink消息,假设缓存通过nlh指针指向:
struct sockaddr_nl nladdr;
struct msghdr msg;
struct iovec iov;
iov.iov_base = (void *)nlh;
iov.iov_len = MAX_NL_MSG_LEN;
msg.msg_name = (void *)&(nladdr);
msg.msg_namelen = sizeof(nladdr);
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
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中加入下面的语句:
#define NETLINK_TEST 17
之后,亦可以在linux内核中的任何地方引用添加的协议类型。
在用户空间,我们使用socket()来创建一个netlink套接字,但是在内核空间,我们使用如下的API:
struct sock *netlink_kernel_create(int unit,void (*input)(struct sock *sk, int len));
参数unit,即为netlink协议类型,如NETLINK_TEST,回调函数会在消息到达netlink套接字时调用。当用户态程序发送一个NETLINK_TEST协议类型的消息给内核时,input()函数被调用。下面是一个实现回调函数的例子:
void input (struct sock *sk, int len)
{
struct sk_buff *skb;
struct nlmsghdr *nlh = NULL;
u8 *payload = NULL;
while ((skb = skb_dequeue(&sk->receive_queue)) != NULL)
{
/* process netlink message pointed by skb->data */
nlh = (struct nlmsghdr *)skb->data;
payload = NLMSG_DATA(nlh);
/* process netlink message with header pointed by nlh and payload pointed by payload */
}
}
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中,我们只要唤醒睡眠的内核线程,如下所示:
void input (struct sock *sk, int len)
{
wake_up_interruptible(sk->sleep);
}
这是一个更具有扩展性的用户和内核通信的模型。其也提高了上下文交换的粒度。
在内核中发送netlink消息
真如在用户空间中一样,在发送一个netlink消息时需要设置源和目的netlink消息地址。假设socket缓存中包含了将要发送的netlink消息,本地地址可以通过以下方式设置:
NETLINK_CB(skb).groups = local_groups;
NETLINK_CB(skb).pid = 0;
/* from kernel */
目的地址可以如下设置:
NETLINK_CB(skb).dst_groups = dst_groups;
NETLINK_CB(skb).dst_pid = dst_pid;
这些信息不是存储在skb->data,而是存储在skb中的netlink控制块中。发送一个消息,使用:
int netlink_unicast(struct sock *ssk, struct sk_buff *skb, u32 pid, int nonblock);
其中ssk是netlink_kernel_create返回的netlink套接字,skb->data指向netlink将要发送的消息而pid是接受该消息的用户程序id。nonblock用于标识在接收缓存不可用时,API是阻塞还是立即返回失败。
你也可以发送一个多播消息。以下的API用于将消息传送到指定的进程,同时多播至指定的多播组。
void netlink_broadcast(struct sock *ssk, struct sk_buff *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套接字。
sock_release(nl_sk->socket);
Netlink可靠性机制
在基于netlink的通信中,有两种可能的情形会导致消息丢失:
1、内存耗尽,没有足够多的内存分配给消息
2、缓存复写,接收队列中没有空间存储消息,这在内核空间和用户空间之间通信时可能会发生缓存复写在以下情况很可能会发生:
3、内核子系统以一个恒定的速度发送netlink消息,但是用户态监听者处理过慢
4、用户存储消息的空间过小
如果netlink传送消息失败,那么recvmsg()函数会返回No buffer spaceavailable(ENOBUFS)错误。那么,用户空间进程知道它丢失了信息,如果内核子系统支持dump操作,它可以重新同步来获取最新的消息。在dump操作中,netlink通过在每次调用recvmsg()函数时传输一个包的流控机制来防止接收队列的复写。改包消耗一个内存页,其包含了几个多部分netlink消息。图6中的序列图显示了在一个重新同步的过程中所使用的dump操作。
另一方面,缓存复写不会发生在用户和内核空间的通信中,因为sendmsg()同步的将netlink消息发送到内核子系统。如果使用的是阻塞套接字,那么netlink在从用户空间到内核空间的通信时完全可靠的,因为内存分配可以等待,所以没有内存耗尽的可能。
netlink也可以提供应答机制。所以如果用户空间进程发送了一个设置了NLM_F_ACK标志的请求,netlink会在netlink错误消息中报告给用户空间刚才请求操作的结果。
从用户空间的角度来看,Netlink套接字在通用的BSD套接字接口之上实现。因此,netlink套接字编程与通用的TCP/IP编程类似。但是,我们也应该考虑几个与netlink相关的特殊问题:
1、netlink套接字没有像其他协议一样对用户空间隐藏协议细节。事实上,netlink传递的是整个消息,包括netlink头和其他信息。因此,这就导致了数据处理函数与通用的TCP/IP套接字不同,所以用户态程序必须根据其格式解析和构建netlink信息。然而,没有标准的工具来完成这些工作,所以你必须实现自己的函数或者使用一些现成的库。
2、来自netlink和内核子系统的错误不是通过recvmsg()函数返回的整数值来表现的。事实上,错误信息时被包装在netlink错误消息中的。唯一的例外是(ENOBUFS)错误,该错误不是包装在netlink消息中,因为报告该错误的原因就是我们没有足够的空间来缓存新的netlink消息。标准的通用套接字错误,如(EAGAIN),通常和其他轮询原语,例如poll()和select(),也是通过recvmsg()返回整数值。