发送一个netlink 消息
为了能够把一个netlink消息发送给内核或者别的用户进程,类似于UDP数据包发送的sendmsg()函数一样,我们需要另外一个结构体 struct sockaddr_nl nladdr作为目的地址。如果这个netlink消息是发往内核的话,nl_pid属性和nl_groups属性都应该设置为0。
如果这个消息是发往另外一个进程的单点传输消息,nl_pid应该设置为接收者进程的PID,nl_groups应该设置为0,假设系统中使用了公式1。
如果消息是发往一个或者多个多播组的话,应该用所有目的多播组的比特位与运算形成nl_groups的值。然后我们就可以将netlink地址应用到结构体struct msghdr msg中,供函数sendmsg()来调用:
struct msghdr msg; msg.msg_name = (void *)&(nladdr); msg.msg_namelen = sizeof(nladdr);
|
netlink消息同样也需要它自身的消息头,这样做是为了给所有协议类型的netlink消息提供一个通用的背景。
由于linux内核的netlink部分总是认为在每个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核心需要的信息。mlmsg_type可以被应用程序所用,它对于netlink核心来说是一个透明的值。Nsmsg_flags 用来该对消息体进行另外的控制,会被netlink核心代码读取并更新。Nlmsg_seq和nlmsg_pid同样对于netlink核心部分来说是透明的,应用程序用它们来跟踪消息。
因此,一个netlink消息体由nlmsghdr和消息的payload部分组成。一旦输入一个消息,它就会进入一个被nlh指针指向的缓冲区。我们同样可以把消息发送个结构体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消息发送出去:
接收netlink消息:接收程序需要申请足够大的空间来存储netlink消息头和消息的payload部分。它会用如下的方式填充结构体 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和消息将要发往的多播组的值组成。Netlink.h中的宏定义NLMSG_DATA(nlh)返回指向netlink消息体的payload的指针。调用
就可以关闭掉fd描述符代表的netlink socket.
内核空间的netlink API接口 内核空间的netlink API是由内核中的netlink核心代码支持的,在net/core/af_netlink.c中实现。从内核的角度来说,API接口与用户空间的 API是不一样的。内核模块通过这些API访问netlink socket并且与用户空间的程序进行通讯。如果你不想使用netlink预定义好的协议类型的话,可以在netlink.h中添加一个自定义的协议类型。例如,我们可以通过在netlink.h中插入下面的代码行,添加一个测试用的协议类型:
然后,就可以在linux内核的任何部分访问这个协议类型了。
在用户空间,我们通过socket()调用来创建一个netlink socket,但是在内核空间,我们调用如下的API:
struct sock * netlink_kernel_create(int unit, void (*input)(struct sock *sk, int len));
参数uint是netlink协议类型,例如NETLINK_TEST。函数指针,input,是netlink socket在收到消息时调用的处理消息的回调函数指针。
在内核创建了一个NETLINK_TEST类型的netlink socket后,无论什么时候,只要用户程序发送一个NETLINK_TEST类型的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) { /* 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消息花费很长时间的话,我们则希望把消息的处理部分放在input()函数的外面,因为长时间的消息处理过程可能会阻止其它系统调用进入内核。取而代之,我们可以牺牲一个内核线程来完成后续的无限的的处理动作。
使用
skb = skb_recv_datagram(nl_sk)
来接收消息。nl_sk是netlink_kernel_create()函数返回的netlink socket,然后,只需要处理skb->data指针指向的netlink消息就可以了。
这个内核线程会在nl_sk中没有消息的时候睡眠。因此,在回调函数input()中我们要做的事情就是唤醒睡眠的内核线程,像这样的方式:
void input (struct sock *sk, int len) { wake_up_interruptible(sk->sleep); } |
这就是一个升级版的内核与用户空间的通讯模型,它提高了上下文切换的粒度。
从内核中发送netlink消息
就像从用户空间发送消息一样,内核在发送netlink消息时也需要设置源netlink地址和目的netlink地址。假设结构体struct sk_buff * skb指向存储着要发送的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中,相反,它们存储在socket缓冲区的netlink控制块skb中.
发送一个单播消息,使用:
int netlink_unicast(struct sock *ssk, struct sk_buff *skb, u32 pid, int nonblock);
|
ssk是by netlink_kernel_create()函数返回的netlink socket, skb->data指向需要发送的netlink消息体,如果使用公式一的话,pid是接收程序的pid,noblock表明当接收缓冲区不可用时是否应该阻塞还是立即返回一个失败信息。
你同样可以从内核发送一个多播消息。下面的函数同时把一个netlink消息发送给pid指定的进程和group标识的多个组。
void netlink_broadcast(struct sock *ssk, struct sk_buff *skb, u32 pid, u32 group, int allocation);
|
group的值是接收消息的各个组的比特位进行与运算的结果。Allocation是内核内存的申请类型。通常情况下在中断上下文使用 GFP_ATOMIC,否则使用GFP_KERNEL。这是由于发送多播消息时,API可能需要申请一个或者多个socket缓冲区并进行拷贝所引起的。
从内核空间关闭netlink socket
netlink_kernel_create()函数返回的netlink socket为struct sock *nl_sk,我们可以通过访问下面的API来从内核空间关闭这个netlink socket:
sock_release(nl_sk->socket);
到目前为止,我们已经演示了netlink编程概念的最小代码框架。接着我们会使用NETLINK_TEST协议类型,并且假设它已经被添加到内核头文件中了。这里列举的内核模块代码只是与netlink相关的,所以,你应该把它插入到一个完整的内核模块代码当中去,这样的完整代码在其它代码中可以找到很多。
实例:
net_link.c
#include <linux/kernel.h> #include <linux/module.h> #include <linux/types.h> #include <linux/sched.h> #include <net/sock.h> #include <net/netlink.h>
#define NETLINK_TEST 21
struct sock *nl_sk = NULL; EXPORT_SYMBOL_GPL(nl_sk);
void nl_data_ready (struct sk_buff *__skb) { struct sk_buff *skb; struct nlmsghdr *nlh; u32 pid; int rc; int len = NLMSG_SPACE(1200); char str[100];
printk("net_link: data is ready to read.\n"); skb = skb_get(__skb);
if (skb->len >= NLMSG_SPACE(0)) { nlh = nlmsg_hdr(skb); printk("net_link: recv %s.\n", (char *)NLMSG_DATA(nlh)); memcpy(str,NLMSG_DATA(nlh), sizeof(str)); pid = nlh->nlmsg_pid; /*pid of sending process */ printk("net_link: pid is %d\n", pid); kfree_skb(skb);
skb = alloc_skb(len, GFP_ATOMIC); if (!skb){ printk(KERN_ERR "net_link: allocate failed.\n"); return; } nlh = nlmsg_put(skb,0,0,0,1200,0); NETLINK_CB(skb).pid = 0; /* from kernel */
memcpy(NLMSG_DATA(nlh), str, sizeof(str)); printk("net_link: going to send.\n"); rc = netlink_unicast(nl_sk, skb, pid, MSG_DONTWAIT); if (rc < 0) { printk(KERN_ERR "net_link: can not unicast skb (%d)\n", rc); } printk("net_link: send is ok.\n"); } return; }
static int test_netlink(void) { nl_sk = netlink_kernel_create(&init_net, NETLINK_TEST, 0, nl_data_ready, NULL, THIS_MODULE);
if (!nl_sk) { printk(KERN_ERR "net_link: Cannot create netlink socket.\n"); return -EIO; } printk("net_link: create socket ok.\n"); return 0; }
int init_module() { test_netlink(); return 0; } void cleanup_module( ) { if (nl_sk != NULL){ sock_release(nl_sk->sk_socket); } printk("net_link: remove ok.\n"); } MODULE_LICENSE("GPL"); MODULE_AUTHOR("kidoln");
sender.c #include <sys/stat.h> #include <unistd.h> #include <stdio.h> #include <stdlib.h> #include <sys/socket.h> #include <sys/types.h> #include <string.h> #include <asm/types.h> #include <linux/netlink.h> #include <linux/socket.h>
#define MAX_PAYLOAD 1024 /* maximum payload size*/ struct sockaddr_nl src_addr, dest_addr; struct nlmsghdr *nlh = NULL; struct iovec iov; int sock_fd; struct msghdr msg;
int main(int argc, char* argv[]) { sock_fd = socket(PF_NETLINK, SOCK_RAW, 21); memset(&msg, 0, sizeof(msg)); memset(&src_addr, 0, sizeof(src_addr)); src_addr.nl_family = AF_NETLINK; src_addr.nl_pid = getpid(); /* self pid */ src_addr.nl_groups = 0; /* not in mcast groups */ 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; /* For Linux Kernel */ dest_addr.nl_groups = 0; /* unicast */
nlh=(struct nlmsghdr *)malloc(NLMSG_SPACE(MAX_PAYLOAD)); /* Fill the netlink message header */ nlh->nlmsg_len = NLMSG_SPACE(MAX_PAYLOAD); nlh->nlmsg_pid = getpid(); /* self pid */ nlh->nlmsg_flags = 0; /* Fill in the netlink message payload */ strcpy(NLMSG_DATA(nlh), "Hello you!");
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;
printf(" Sending message. ...\n"); sendmsg(sock_fd, &msg, 0);
/* Read message from kernel */ memset(nlh, 0, NLMSG_SPACE(MAX_PAYLOAD)); printf(" Waiting message. ...\n"); recvmsg(sock_fd, &msg, 0); printf(" Received message payload: %s\n",NLMSG_DATA(nlh));
/* Close Netlink Socket */ close(sock_fd); }
Makefile MODULE_NAME :=net_link obj-m :=$(MODULE_NAME).o KERNELDIR ?= /lib/modules/$(shell uname -r)/build PWD := $(shell pwd) all: $(MAKE) -C $(KERNELDIR) M=$(PWD) gcc -o sender sender.c clean: rm -fr *.ko *.o *.cmd sender $(MODULE_NAME).mod.c
|