关于Netlink多播机制的用法
在上一篇博文中我们所遇到的情况都是用户空间作为消息进程的发起者,Netlink还支持内核作为消息的发送方的情况。这一般用于内核主动向用户空间报告一些内核状态,例如我们在用户空间看到的USB的热插拔事件的通告就是这样的应用。
先说一下我们的目标,内核线程每个一秒钟往一个多播组里发送一条消息,然后用户空间所以加入了该组的进程都会收到这样的消息,并将消息内容打印出来。
Netlink地址结构体中的nl_groups是32位,也就是说每种Netlink协议最多支持32个多播组。如何理解这里所说的每种Netlink协议?在</usr/include/linux/netlink.h>里预定义的如下协议都是Netlink协议簇的具体协议,还有我们添加的NETLINK_TEST也是一种Netlink协议。
点击(此处)折叠或打开
#define NETLINK_ROUTE 0 /* Routing/device hook */
#define NETLINK_UNUSED 1 /* Unused number */
#define NETLINK_USERSOCK 2 /* Reserved for user mode socket protocols */
#define NETLINK_FIREWALL 3 /* Firewalling hook */
#define NETLINK_INET_DIAG 4 /* INET socket monitoring */
#define NETLINK_NFLOG 5 /* netfilter/iptables ULOG */
#define NETLINK_XFRM 6 /* ipsec */
#define NETLINK_SELINUX 7 /* SELinux event notifications */
#define NETLINK_ISCSI 8 /* Open-iSCSI */
#define NETLINK_AUDIT 9 /* auditing */
#define NETLINK_FIB_LOOKUP 10
#define NETLINK_CONNECTOR 11
#define NETLINK_NETFILTER 12 /* netfilter subsystem */
#define NETLINK_IP6_FW 13
#define NETLINK_DNRTMSG 14 /* DECnet routing messages */
#define NETLINK_KOBJECT_UEVENT 15 /* Kernel messages to userspace */
#define NETLINK_GENERIC 16
/* leave room for NETLINK_DM (DM Events) */
#define NETLINK_SCSITRANSPORT 18 /* SCSI Transports */
#define NETLINK_ECRYPTFS 19
#define NETLINK_TEST 20 /* 用户添加的自定义协议 */
在我们自己添加的NETLINK_TEST协议里,同样地,最多允许我们设置32个多播组,每个多播组用1个比特表示,所以不同的多播组不可能出现重复。你可以根据自己的实际需求,决定哪个多播组是用来做什么的。用户空间的进程如果对某个多播组感兴趣,那么它就加入到该组中,当内核空间的进程往该组发送多播消息时,所有已经加入到该多播组的用户进程都会收到该消息。
再回到我们Netlink地址结构体里的nl_groups成员,它是多播组的地址掩码,注意是掩码不是多播组的组号。如何根据多播组号取得多播组号的掩码呢?在af_netlink.c中有个函数:
点击(此处)折叠或打开
static u32 netlink_group_mask(u32 group)
{
return group ? 1 << (group - 1) : 0;
}
也就是说,在用户空间的代码里,如果我们要加入到多播组1,需要设置nl_groups设置为1;多播组2的掩码为2;多播组3的掩码为4,依次类推。为0表示我们不希望加入任何多播组。理解这一点很重要。所以我们可以在用户空间也定义一个类似于netlink_group_mask()的功能函数,完成从多播组号到多播组掩码的转换。最终用户空间的代码如下:
点击(此处)折叠或打开
#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>
#include <errno.h>
#define MAX_PAYLOAD 1024 // Netlink消息的最大载荷的长度
unsigned int netlink_group_mask(unsigned int group)
{
return group ? 1 << (group - 1) : 0;
}
int main(int argc, char* argv[])
{
struct sockaddr_nl src_addr;
struct nlmsghdr *nlh = NULL;
struct iovec iov;
struct msghdr msg;
int sock_fd, retval;
// 创建Socket
sock_fd = socket(PF_NETLINK, SOCK_RAW, NETLINK_TEST);
if(sock_fd == -1){
printf("error getting socket: %s", strerror(errno));
return -1;
}
memset(&src_addr, 0, sizeof(src_addr));
src_addr.nl_family = PF_NETLINK;
src_addr.nl_pid = 0; // 表示我们要从内核接收多播消息。注意:该字段为0有双重意义,另一个意义是表示我们发送的数据的目的地址是内核。
src_addr.nl_groups = netlink_group_mask(atoi(argv[1])); // 多播组的掩码,组号来自我们执行程序时输入的第一个参数
// 因为我们要加入到一个多播组,所以必须调用bind()。
retval = bind(sock_fd, (struct sockaddr*)&src_addr, sizeof(src_addr));
if(retval < 0){
printf("bind failed: %s", strerror(errno));
close(sock_fd);
return -1;
}
// 为接收Netlink消息申请存储空间
nlh = (struct nlmsghdr *)malloc(NLMSG_SPACE(MAX_PAYLOAD));
if(!nlh){
printf("malloc nlmsghdr error!\n");
close(sock_fd);
return -1;
}
memset(nlh, 0, NLMSG_SPACE(MAX_PAYLOAD));
iov.iov_base = (void *)nlh;
iov.iov_len = NLMSG_SPACE(MAX_PAYLOAD);
memset(&msg, 0, sizeof(msg));
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
// 从内核接收消息
printf("waitinf for...\n");
recvmsg(sock_fd, &msg, 0);
printf("Received message: %s \n", NLMSG_DATA(nlh));
close(sock_fd);
return 0;
}
可以看到,用户空间的程序基本没什么变化,唯一需要格外注意的就是Netlink地址结构体中的nl_groups的设置。由于对它的解释很少,加之没有有效的文档,所以我也是一边看源码,一边在网上搜集资料。有分析不当之处,还请大家帮我指出。
内核空间我们添加了内核线程和内核线程同步方法completion的使用。内核空间修改后的代码如下:
点击(此处)折叠或打开
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/skbuff.h>
#include <linux/init.h>
#include <linux/ip.h>
#include <linux/types.h>
#include <linux/sched.h>
#include <net/sock.h>
#include <net/netlink.h>
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Koorey King");
struct sock *nl_sk = NULL;
static struct task_struct *mythread = NULL; //内核线程对象
//向用户空间发送消息的接口
void sendnlmsg(char *message/*,int dstPID*/)
{
struct sk_buff *skb;
struct nlmsghdr *nlh;
int len = NLMSG_SPACE(MAX_MSGSIZE);
int slen = 0;
if(!message || !nl_sk){
return;
}
// 为新的 sk_buffer申请空间
skb = alloc_skb(len, GFP_KERNEL);
if(!skb){
printk(KERN_ERR "my_net_link: alloc_skb Error./n");
return;
}
slen = strlen(message)+1;
//用nlmsg_put()来设置netlink消息头部
nlh = nlmsg_put(skb, 0, 0, 0, MAX_MSGSIZE, 0);
// 设置Netlink的控制块里的相关信息
NETLINK_CB(skb).pid = 0; // 消息发送者的id标识,如果是内核发的则置0
NETLINK_CB(skb).dst_group = 5; //多播组号为5,但置成0好像也可以。
message[slen] = '\0';
memcpy(NLMSG_DATA(nlh), message, slen+1);
//通过netlink_unicast()将消息发送用户空间由dstPID所指定了进程号的进程
//netlink_unicast(nl_sk,skb,dstPID,0);
netlink_broadcast(nl_sk, skb, 0,5, GFP_KERNEL); //发送多播消息到多播组5,这里我故意没有用1之类的“常见”值,目的就是为了证明我们上面提到的多播组号和多播组号掩码之间的对应关系
printk("send OK!\n");
return;
}
//每隔1秒钟发送一条“I am from kernel!”消息,共发10个报文
static int sending_thread(void *data)
{
int i = 10;
struct completion cmpl;
while(i--){
init_completion(&cmpl);
wait_for_completion_timeout(&cmpl, 1 * HZ);
sendnlmsg("I am from kernel!");
}
printk("sending thread exited!");
return 0;
}
static int __init myinit_module()
{
printk("my netlink in\n");
nl_sk = netlink_kernel_create(NETLINK_TEST,0,NULL,THIS_MODULE);
if(!nl_sk){
printk(KERN_ERR "my_net_link: create netlink socket error.\n");
return 1;
}
printk("my netlink: create netlink socket ok.\n");
mythread = kthread_run(sending_thread,NULL,"thread_sender");
return 0;
}
static void __exit mycleanup_module()
{
if(nl_sk != NULL){
sock_release(nl_sk->sk_socket);
}
printk("my netlink out!\n");
}
module_init(myinit_module);
module_exit(mycleanup_module);
关于内核中netlink_kernel_create(int unit, unsigned int groups,…)函数里的第二个参数指的是我们内核进程最多能处理的多播组的个数,如果该值小于32,则默认按32处理,所以在调用netlink_kernel_create()函数时可以不用纠结第二个参数,一般将其置为0就可以了。
在skbuff{}结构体中,有个成员叫做"控制块",源码对它的解释如下:
点击(此处)折叠或打开
struct sk_buff {
/* These two members must be first. */
struct sk_buff *next;
struct sk_buff *prev;
… …
/*
* This is the control buffer. It is free to use for every
* layer. Please put your private variables there. If you
* want to keep them across layers you have to do a skb_clone()
* first. This is owned by whoever has the skb queued ATM.
*/
char cb[48];
… …
}
当内核态的Netlink发送数据到用户空间时一般需要填充skbuff的控制块,填充的方式是通过强制类型转换,将其转换成struct netlink_skb_parms{}之后进行填充赋值的:
点击(此处)折叠或打开
struct netlink_skb_parms
{
struct ucred creds; /* Skb credentials */
__u32 pid;
__u32 dst_group;
kernel_cap_t eff_cap;
__u32 loginuid; /* Login (audit) uid */
__u32 sid; /* SELinux security id */
};
填充时的模板代码如下:
点击(此处)折叠或打开
NETLINK_CB(skb).pid=xx;
NETLINK_CB(skb).dst_group=xx;
这里要注意的是在Netlink协议簇里提到的skbuff的cb控制块里保存的是属于Netlink的私有信息。怎么讲,就是Netlink会用该控制块里的信息来完成它所提供的一些功能,只是完成Netlink功能所必需的一些私有数据。打个比方,以开车为例,开车的时候我们要做的就是打火、控制方向盘、适当地控制油门和刹车,车就开动了,这就是汽车提供给我们的“功能”。汽车的发动机,轮胎,传动轴,以及所用到的螺丝螺栓等都属于它的“私有”数据cb。汽车要运行起来这些东西是不可或缺的,但它们之间的协作和交互对用户来说又是透明的。就好比我们Netlink的私有控制结构struct netlink_skb_parms{}一样。
目前我们的例子中,将NETLINK_CB(skb).dst_group设置为相应的多播组号和0效果都是一样,用户空间都可以收到该多播消息,原因还不是很清楚,还请Netlink的大虾们帮我点拨点拨。
编译后重新运行,最后的测试结果如下:
注意,这里一定要先执行insmod加载内核模块,然后再运行用户空间的程序。如果没有加载mynlkern.ko而直接执行./test 5在bind()系统调用时会报如下的错误:
bind failed: No such file or directory
因为网上有写文章在讲老版本Netlink的多播时用法时先执行了用户空间的程序,然后才加载内核模块,现在(2.6.21)已经行不通了,这一点请大家注意。
小结:通过这三篇博文我们对Netlink有了初步的认识,并且也可以开发基于Netlink的基本应用程序。但这只是冰山一角,要想写出高质量、高效率的软件模块还有些差距,特别是对Netlink本质的理解还需要提高一个层次,当然这其中牵扯到内核编程的很多基本功,如临界资源的互斥、线程安全性保护、用Netlink传递大数据时的处理等等都是开发人员需要考虑的问题。