likes
comments
collection
share

内核和用户态通信的桥梁netlink socket

作者站长头像
站长
· 阅读数 3

代码破破烂烂,我们缝缝补补

1、netlink背景介绍

由于内核开发和维护的复杂性,只有最关键和性能关键的代码被放置在内核中。其他诸如图形用户界面(GUI)、管理和控制代码通常以用户空间应用程序的形式进行编程。在Linux中,将某些功能的实现在内核和用户空间之间进行划分的做法非常常见。那么问题是内核代码和用户空间代码如何进行通信呢?

答案是内核和用户空间之间存在各种IPC方法,如系统调用、ioctl、proc文件系统或Netlink套接字。本文将讨论Netlink套接字,并揭示其作为一种面向网络功能友好的IPC的优势。

1.1 引言

Netlink套接字是一种用于在内核和用户空间进程之间传输信息的特殊IPC机制。它通过标准套接字API(供用户空间进程使用)和内核模块的特殊内核API之间的全双工通信链路来实现。Netlink套接字使用AF_NETLINK地址族,而TCP/IP套接字使用AF_INET地址族。每个Netlink套接字功能在内核头文件include/linux/netlink.h中定义了自己的协议类型。。

下面是当前由Netlink套接字支持的功能及其协议类型的子集:

-   NETLINK_ROUTE:用于路由(如BGP、OSPF、RIP)和设备的钩子功能。用户空间路由守护程序可以通过这个协议类型更新内核路由表。
-   NETLINK_USERSOCK:保留给用户模式套接字协议。这个协议类型为用户空间应用程序提供了一种在内核和用户空间之间进行通信的方式。
-   NETLINK_FIREWALL:未使用的协议类型,之前用于ip_queue。
-   NETLINK_SOCK_DIAG:用于套接字监视的协议类型。
-   NETLINK_NFLOG:netfilter/iptables的ULOG协议类型,用于用户空间iptables管理工具和内核空间Netfilter模块之间的通信。
-   NETLINK_XFRM:用于IPsec的协议类型。
-   NETLINK_SELINUX:SELinux事件通知的协议类型。
-   NETLINK_AUDIT:用于审计的协议类型。
-   NETLINK_NETFILTER:netfilter子系统的协议类型。
-   NETLINK_IP6_FW:IPv6防火墙的协议类型。
-   NETLINK_DNRTMSG:DECnet路由消息的协议类型。
-   NETLINK_KOBJECT_UEVENT:内核消息到用户空间的协议类型,用于内核对象的事件。
-   NETLINK_GENERIC:通用的协议类型。
-   NETLINK_SCSITRANSPORT:SCSI传输的协议类型。
-   NETLINK_ECRYPTFS:eCryptfs的协议类型。
-   NETLINK_RDMA:RDMA(远程直接内存访问)的协议类型。
-   NETLINK_CRYPTO:加密层的协议类型。
-   NETLINK_SMC:SMC(System Management Control)监视的协议类型。

2 为什么上述功能使用Netlink而不是系统调用

ioctl或proc文件系统来在用户空间和内核之间进行通信?为新功能添加系统调用、ioctl或proc文件是一项复杂的任务;这样做可能会污染内核并损害系统的稳定性。相比之下,Netlink套接字则更为简单:只需在netlink.h中添加一个常量,即协议类型。然后,内核模块和应用程序可以立即使用类似套接字的API进行通信。

netlink特点1: 非阻塞

Netlink是异步的,因为与其他套接字API一样,它提供套接字队列来平滑处理消息的突发。发送Netlink消息的系统调用将消息排入接收方的Netlink队列,然后调用接收方的接收处理程序。在接收处理程序的上下文中,接收方可以决定立即处理消息,或者将消息留在队列中,并在不同的上下文中稍后处理。与Netlink不同,系统调用则需要同步处理。因此,如果我们使用系统调用来将消息从用户空间传递到内核,如果处理该消息的时间很长,可能会影响内核的调度粒度。

netlink特点2: 没有编译依赖

在内核中实现系统调用的代码在编译时静态地链接到内核中;因此,将系统调用代码包含在可加载模块中(这是大多数设备驱动程序的情况)是不合适的。使用Netlink套接字,则不存在Linux内核的Netlink核心与驻留在可加载内核模块中的Netlink应用程序之间的编译时依赖关系。

netlink特点3: 支持多播

Netlink套接字支持多播,这是它相对于系统调用、ioctl和proc的另一个优点。一个进程可以将消息多播到一个Netlink组地址,其他任意数量的进程可以监听该组地址。这为内核向用户空间分发事件提供了一种近乎完美的机制。

netlink特点4: 双工

系统调用和ioctl是单工IPC,这意味着仅可由用户空间应用程序发起这些IPC的会话。但是,如果内核模块需要向用户空间应用程序发送紧急消息,使用这些IPC是无法直接实现的。通常,应用程序需要定期轮询内核以获取状态变化,尽管密集轮询是昂贵的。Netlink通过允许内核也能够发起会话,优雅地解决了这个问题。我们将其称为Netlink套接字的双工特性。

netlink特点5: API通用

Netlink套接字提供了一种BSD套接字风格的API,这种风格被软件开发社区广泛理解。因此,与使用相对晦涩的系统调用API和ioctl相比,学习和使用成本要低得多。

关于BSD路由套接字 在BSD TCP/IP堆栈实现中,有一个称为路由套接字的特殊套接字。它的地址族是AF_ROUTE,协议族是PF_ROUTE,套接字类型是SOCK_RAW。在BSD中,路由套接字被用于进程在内核路由表中添加或删除路由。

在Linux中,Netlink套接字协议类型NETLINK_ROUTE提供了与路由套接字等价的功能。Netlink套接字提供了比BSD路由套接字更丰富的功能。

3 Netlink套接字API

标准套接字API(如socket()、sendmsg()、recvmsg()和close())可以被用户空间应用程序用来访问Netlink套接字。请查阅手册页面以获取这些API的详细定义。在这里,我们仅讨论如何在Netlink套接字的上下文中选择这些API的参数。这些API对于任何使用TCP/IP套接字编写过普通网络应用程序的人来说都应该非常熟悉。

1、创建socket

为了使用socket()创建一个套接字,可以使用以下接口:

int socket(int domain, int type, int protocol);

其中,套接字的协议(domain)是AF_NETLINK,而套接字的类型(type)可以是SOCK_RAW或SOCK_DGRAM,因为netlink是一种面向数据报的服务。

协议(protocol)选择了使用该套接字的netlink特性。以下是一些预定义的netlink协议类型:NETLINK_ROUTE、NETLINK_XFRM 和NETLINK_ROUTE6。也可以添加自己定义的netlink协议类型。

对于每种netlink协议类型,最多可以定义32个组播组。每个组播组用一个位掩码1<<i来表示,其中0<=i<=31。当一组进程和内核进程协调一起实现相同的功能时,发送组播netlink消息可以减少使用的系统调用数量,并减轻应用程序维护组播组成员资格所带来的负担。

2、绑定socket

就像对于TCP/IP套接字一样,netlink的bind() API将本地(源)套接字地址与已打开的套接字关联起来。netlink地址结构如下:

include/uapi/linux/netlink.h
struct sockaddr_nl
{
  sa_family_t    nl_family;  /* AF_NETLINK   */
  unsigned short nl_pad;     /* zero         */
  __u32          nl_pid;     /* 进程pid */
  __u32          nl_groups;  /* 组播组掩码 */
} nladdr;

在使用bind()时,sockaddr_nl的nl_pid字段可以填入调用进程自己的pid。在这里,nl_pid用作这个netlink套接字的本地地址。应用程序负责选择一个唯一的32位整数填充到nl_pid中:

NL_PID 算法 1:nl_pid = getpid();

算法 1使用应用程序的进程ID作为nl_pid,如果对于给定的netlink协议类型,进程只需要一个netlink套接字。

在相同进程的不同线程想要在同一netlink协议下打开不同的netlink套接字时,可以使用算法 2来生成nl_pid:

NL_PID 算式 2:pthread_self() << 16 | getpid();

这样,相同进程的不同pthread可以为相同的netlink协议类型拥有各自的netlink套接字。事实上,即使在单个pthread内部,也可以为同一协议类型创建多个netlink套接字。然而,需要生成唯一的nl_pid,当然这不是正常使用情况。

如果应用程序希望接收针对特定多播组的协议类型的netlink消息,应将所有感兴趣的多播组的位掩码OR在一起,形成sockaddr_nl的nl_groups字段。否则,应将nl_groups清零,以便应用程序只接收针对应用程序的协议类型的单播netlink消息。填充了nladdr之后,可以按如下方式进行绑定:

bind(fd, (struct sockaddr*)&nladdr, sizeof(nladdr));

3、发送Netlink消息

为了将netlink消息发送给内核或其他用户空间进程,需要提供另一个struct sockaddr_nl nladdr作为目的地址,就像发送UDP数据包一样。如果消息目标是内核,nl_pid和nl_groups都应该填写为0。

如果消息是单播消息,目的地是另一个进程,则nl_pid是另一个进程的pid,而nl_groups是0,假设系统使用了nlpid算式 1。

如果消息是多播消息,目的地是一个或多个多播组,则所有目的地多播组的位掩码应该OR在一起,形成nl_groups字段。然后,可以将netlink地址提供给struct msghdr msg,使用sendmsg() API发送消息,就像下面一样:

struct msghdr msg;
msg.msg_name = (void *)&(nladdr);
msg.msg_namelen = sizeof(nladdr);
公共消息头

netlink套接字还需要其自己的消息头。这是为所有协议类型的netlink消息提供一个共同的基础。

因为Linux内核的netlink核心假设每个netlink消息中都存在以下头部,所以应用程序必须在发送的每个netlink消息中提供此头部:

struct nlmsghdr
{
  __u32 nlmsg_len;   /* 消息长度 */
  __u16 nlmsg_type;  /* 消息类型 */
  __u16 nlmsg_flags; /* 额外标志 */
  __u32 nlmsg_seq;   /* 序列号 */
  __u32 nlmsg_pid;   /* 发送进程PID */
};

nlmsg_len必须填写netlink消息的总长度,包括头部,这是netlink所需要的。nlmsg_type可以被应用程序使用,对于netlink核心来说是不透明的值。nlmsg_flags用于给消息提供额外的控制,由netlink核心读取和更新。nlmsg_seq和nlmsg_pid由应用程序用于跟踪消息,对netlink也是不透明的。

因此,netlink消息包括nlmsghdr和消息载荷。一旦输入了消息,就将其发送到struct msghdr msg:

struct iovec iov;

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);

4、接收Netlink消息

接收应用程序需要分配足够大的缓冲区来存放netlink消息头和消息载荷。然后可以像下面这样填充struct msghdr 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和消息发送的多播组。

5、关闭连接

调用close(fd)将关闭文件描述符fd所标识的netlink套接字。

4 内核空间Netlink API

内核空间的netlink API位于net/netlink/af_netlink.c模块。头文件incude/linux/netlink.h github.com/jobs77/linu…

从内核方面来看,API与用户空间API不同。该API可以被内核模块用来访问netlink套接字,并与用户空间应用程序进行通信。除非使用现有的netlink套接字协议类型,否则需要通过向netlink.h添加一个常量来添加自己的协议类型。例如,我们可以通过向netlink.h中添加以下行来为测试目的添加一个netlink协议类型:

#define NETLINK_TEST_30  30

在用户空间中,我们调用socket()来创建netlink套接字,但在内核空间中,我们调用以下API:

/* optional Netlink kernel configuration parameters */
struct netlink_kernel_cfg {
	unsigned int	groups;
	unsigned int	flags;
	void		(*input)(struct sk_buff *skb);
	struct mutex	*cb_mutex;
	int		(*bind)(struct net *net, int group);
	void		(*unbind)(struct net *net, int group);
	bool		(*compare)(struct net *net, struct sock *sk);
};

static inline struct sock *
netlink_kernel_create(struct net *net, int unit, struct netlink_kernel_cfg *cfg)
{
	return __netlink_kernel_create(net, unit, THIS_MODULE, cfg);
}

其中,unit参数实际上是netlink协议类型,如NETLINK_TEST_30netlink_kernel_cfg中的函数指针input是当消息到达netlink套接字时调用的回调函数。

内核创建了用于NETLINK_TEST_30协议的netlink套接字后,当用户空间向内核发送NETLINK_TEST_30协议类型的netlink消息时,由netlink_kernel_create()注册的回调函数input()将被调用。以下是回调函数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) {
        /* 处理skb->data指向的netlink消息 */
        nlh = (struct nlmsghdr *)skb->data;
        payload = NLMSG_DATA(nlh);
        /* 处理nlh和payload指向的netlink消息 */
    }
}

这个input()函数在由发送进程触发的sendmsg()系统调用的上下文中调用。如果处理netlink消息很快,可以在input()内部处理netlink消息是可以的。然而,如果处理netlink消息花费的时间很长,我们希望将其从input()中分离出来,以避免阻塞其他系统调用进入内核。为此,我们可以使用专用的内核线程来不断执行以下步骤:使用skb = skb_recv_datagram(nl_sk)(其中nl_sk是netlink_kernel_create()返回的netlink套接字)来接收netlink消息,然后处理skb->data指向的netlink消息。

当nl_sk中没有netlink消息时,这个内核线程会休眠。因此,在回调函数input()内部,我们只需唤醒正在休眠的内核线程,如下所示:

void input (struct sock *sk, int len)
{
    wake_up_interruptible(sk->sleep);
}

这是用户空间和内核之间更可扩展的通信模型,并且它提高了上下文切换的粒度。

1、从内核发送Netlink消息

与用户空间一样,发送netlink消息时需要设置源netlink地址和目标netlink地址。假设保存netlink消息的套接字缓冲区为struct sk_buff *skb,则可以使用以下方式设置本地地址:

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 portid, int nonblock)

其中,ssk是netlink_kernel_create()返回的netlink套接字,skb->data指向要发送的netlink消息,pid是接收应用程序的PID(假设使用NLPID Formula 1),nonblock表示当接收缓冲区不可用时该API是否应阻塞,或立即返回失败。

您还可以发送多播消息。以下API将netlink消息传递给pid指定的进程和group指定的多播组:

int netlink_broadcast(struct sock *ssk, struct sk_buff *skb, u32 portid,
		      u32 group, gfp_t allocation)

group是所有接收多播组的OR位掩码,allocation是内核内存分配类型。通常,如果在中断上下文中调用API,则使用GFP_ATOMIC;否则,使用GFP_KERNEL。这是因为该API可能需要分配一个或多个套接字缓冲区来克隆多播消息。

2、从内核关闭Netlink套接字

假设通过netlink_kernel_create()返回的struct sock *nl_sk,我们可以调用以下内核API来关闭内核中的netlink套接字:

sock_release(nl_sk->socket);

5 举个栗子

到目前为止,我们仅展示了最小的代码框架,以阐述netlink编程的概念。现在,我们将使用我们的NETLINK_TEST_30 netlink协议类型,并假设它已经添加到内核头文件中。以下列出的内核模块代码仅包含与netlink相关的部分,因此它应该插入到完整的内核模块框架中,您可以从许多其他参考源中找到。

5.1 内核与应用程序之间的单播通信

在此示例中,用户空间进程向内核模块发送netlink消息,而内核模块会将消息回显给发送进程。以下是用户空间代码:

#include <sys/socket.h>
#include <linux/netlink.h>

#define MAX_PAYLOAD 2046  /* 最大有效载荷大小*/
struct sockaddr_nl src_addr, dest_addr;
struct msghdr msg;
struct nlmsghdr *nlh = NULL;
struct iovec iov;
int sock_fd;

void main() {
 sock_fd = socket(PF_NETLINK, SOCK_RAW, NETLINK_TEST_30);

 memset(&src_addr, 0, sizeof(src_addr));
 src_addr.nl_family = AF_NETLINK;
 src_addr.nl_pid = getpid();  /* 自身的PID */
 src_addr.nl_groups = 0;  /* 不在多播组中 */
 bind(sock_fd, (struct sockaddr*)&src_addr,
      sizeof(src_addr));
 
 memset(&dest_addr, 0, sizeof(dest_addr));
 dest_addr.nl_family = AF_NETLINK;
 dest_addr.nl_pid = 0;   /* 对于Linux内核 */
 dest_addr.nl_groups = 0; /* 单播 */

 nlh=(struct nlmsghdr *)malloc(
		         NLMSG_SPACE(MAX_PAYLOAD));
 /* 填充netlink消息头 */
 nlh->nlmsg_len = NLMSG_SPACE(MAX_PAYLOAD);
 nlh->nlmsg_pid = getpid();  /* 自身的PID */
 nlh->nlmsg_flags = 0;
 /* 填充netlink消息有效载荷 */
 strcpy(NLMSG_DATA(nlh), "Hello netlink socket!");

 iov.iov_base = (void *)nlh;
 iov.iov_len = nlh->nlmsg_len;
 msg.msg_name = (void *)&dest_addr;
 msg.msg_namelen = sizeof(dest_addr);
 msg.msg_iov = &iov;
 msg.msg_iovlen = 1;

 sendmsg(sock_fd, &msg, 0);

 /* 从内核读取消息 */
 memset(nlh, 0, NLMSG_SPACE(MAX_PAYLOAD));
 recvmsg(fd, &msg, 0);
 printf("收到消息有效载荷: %s\n",
	NLMSG_DATA(nlh));

 /* 关闭Netlink套接字 */
 close(sock_fd);
}

下面是内核代码:

struct sock *nl_sk = NULL;

void nl_data_ready (struct sock *sk, int len)
{
  wake_up_interruptible(sk->sleep);
}

void netlink_test() {
 struct sk_buff *skb = NULL;
 struct nlmsghdr *nlh = NULL;
 int err;
 u32 pid;

 nl_sk = netlink_kernel_create(NETLINK_TEST_30,
                                   nl_data_ready);
 /* 等待来自用户空间的消息 */
 skb = skb_recv_datagram(nl_sk, 0, 0, &err);

 nlh = (struct nlmsghdr *)skb->data;
 printk("%s: received netlink message payload:%s\n",
        __FUNCTION__, NLMSG_DATA(nlh));

 pid = nlh->nlmsg_pid; /*发送进程的PID */
 NETLINK_CB(skb).groups = 0; /* 不在多播组中 */
 NETLINK_CB(skb).pid = 0;      /* 来自内核 */
 NETLINK_CB(skb).dst_pid = pid;
 NETLINK_CB(skb).dst_groups = 0;  /* 单播 */
 netlink_unicast(nl_sk, skb, pid, MSG_DONTWAIT);
 sock_release(nl_sk->socket);
}

加载执行上述内核代码的内核模块后,当我们运行用户空间可执行文件时,应该会在用户空间程序的输出中看到以下内容:

收到消息有效载荷: Hello you!

在dmesg的输出中应该会出现以下消息:

netlink_test: received netlink message payload:
Hello you!

5.2 内核与应用程序之间的多播通信

在这个例子中,有两个用户空间应用程序都在监听相同的netlink多播组。内核模块通过netlink套接字发送消息到多播组,所有应用程序都会接收到该消息。以下是用户空间的代码示例:

#include <sys/socket.h>
#include <linux/netlink.h>

#define MAX_PAYLOAD 1024  
struct sockaddr_nl src_addr, dest_addr;
struct nlmsghdr *nlh = NULL;
struct iovec iov;
int sock_fd;

void main() {
 sock_fd=socket(PF_NETLINK, SOCK_RAW, NETLINK_TEST_30);

 memset(&src_addr, 0, sizeof(local_addr));
 src_addr.nl_family = AF_NETLINK;
 src_addr.nl_pid = getpid();  
 src_addr.nl_groups = 1;  // interested in group 1

 bind(sock_fd, (struct sockaddr*)&src_addr, sizeof(src_addr));

 memset(&dest_addr, 0, sizeof(dest_addr));

 nlh = (struct nlmsghdr *)malloc(NLMSG_SPACE(MAX_PAYLOAD));
 memset(nlh, 0, NLMSG_SPACE(MAX_PAYLOAD));

 iov.iov_base = (void *)nlh;
 iov.iov_len = NLMSG_SPACE(MAX_PAYLOAD);
 msg.msg_name = (void *)&dest_addr;
 msg.msg_namelen = sizeof(dest_addr);
 msg.msg_iov = &iov;
 msg.msg_iovlen = 1;

 printf("Waiting for message from kernel\n");

 /* Read message from kernel */
 recvmsg(fd, &msg, 0);
 printf("Received message payload: %s\n", NLMSG_DATA(nlh));
 close(sock_fd);
}

以下是内核空间的代码示例:

#define MAX_PAYLOAD 2048
struct sock *nl_sk = NULL;

void netlink_test() {
 sturct sk_buff *skb = NULL;
 struct nlmsghdr *nlh;
 int err;

 nl_sk = netlink_kernel_create(NETLINK_TEST_30, nl_data_ready);
 skb=alloc_skb(NLMSG_SPACE(MAX_PAYLOAD),GFP_KERNEL);
 nlh = (struct nlmsghdr *)skb->data;
 nlh->nlmsg_len = NLMSG_SPACE(MAX_PAYLOAD);
 nlh->nlmsg_pid = 0;  
 nlh->nlmsg_flags = 0;
 strcpy(NLMSG_DATA(nlh), "Greeting from kernel!");
 NETLINK_CB(skb).groups = 1;  
 NETLINK_CB(skb).pid = 0;  
 NETLINK_CB(skb).dst_pid = 0;  
 NETLINK_CB(skb).dst_groups = 1;  

 netlink_broadcast(nl_sk, skb, 0, 1, GFP_KERNEL);
 sock_release(nl_sk->socket);
}

假设用户空间的代码被编译为可执行文件nl_recv,我们可以运行两个nl_recv的实例:

./nl_recv &
Waiting for message from kernel
./nl_recv &
Waiting for message from kernel

然后,在加载执行内核空间代码的内核模块之后,两个nl_recv实例应该都会接收到以下消息:

Received message payload: Greeting from kernel!
Received message payload: Greeting from kernel!

6、总结

Netlink套接字是用户空间应用程序和内核模块之间通信的灵活接口。它为应用程序和内核提供了易于使用的套接字API。它提供了高级通信功能,如全双工、缓冲I/O、多播和异步通信,这些在其他内核/用户空间IPC中是不存在的。

更详细的,可以参考内核xfrm如何使用netlink。 switch-router.gitee.io/blog/IPsec-…