from:http://blog.chinaunix.net/uid-23069658-id-3400761.html
引言
Alan Cox在内核1.3版本的开发阶段最先引入了Netlink,刚开始时Netlink是以字符驱动接口的方式提供内核与用户空间的双向数据通信;随后,在2.1内核开发过程中,Alexey Kuznetsov将Netlink改写成一个更加灵活、且易于扩展的基于消息通信接口,并将其应用到高级路由子系统的基础框架里。自那时起,Netlink就成了Linux内核子系统和用户态的应用程序通信的主要手段之一。
2001年,ForCES IETF委员会正式对Netlink进行了标准化的工作。Jamal Hadi Salim提议将Netlink定义成一种用于网络设备的路由引擎组件和其控制管理组件之间通信的协议。不过他的建议最终没有被采纳,取而代之的是我们今天所看到的格局:Netlink被设计成一个新的协议域,domain。
Linux之父托瓦斯曾说过“Linuxis evolution, not intelligent design”。什么意思?就是说,Netlink也同样遵循了Linux的某些设计理念,即没有完整的规范文档,亦没有设计文档。只有什么?你懂得---“Read the f**king source code”。
当然,本文不是分析Netlink在Linux上的实现机制,而是就“什么是Netlink”以及“如何用好Netlink”的话题和大家做个分享,只有在遇到问题时才需要去阅读内核源码弄清个所以然。
什么是Netlink
关于Netlink的理解,需要把握几个关键点:
1、面向数据报的无连接消息子系统
2、基于通用的BSD Socket架构而实现
关于第一点使我们很容易联想到UDP协议,能想到这一点就非常棒了。按着UDP协议来理解Netlink不是不无道理,只要你能触类旁通,做到“活学”,善于总结归纳、联想,最后实现知识迁移这就是学习的本质。Netlink可以实现内核->用户以及用户->内核的双向、异步的数据通信,同时它还支持两个用户进程之间、甚至两个内核子系统之间的数据通信。本文中,对后两者我们不予考虑,焦点集中在如何实现用户<->内核之间的数据通信。
看到第二点脑海中是不是瞬间闪现了下面这张图片呢?如果是,则说明你确实有慧根;当然,不是也没关系,慧根可以慢慢长嘛,呵呵。
Netlink通信类型
Netlink支持两种类型的通信方式:单播和多播。
单播:经常用于一个用户进程和一个内核子系统之间1:1的数据通信。用户空间发送命令到内核,然后从内核接受命令的返回结果。
Netlink的消息格式
Netlink消息由两部分组成:消息头和有效数据载荷,且整个Netlink消息是4字节对齐,一般按主机字节序进行传递。消息头为固定的16字节,消息体长度可变:Netlink的消息头
消息头定义在<include/linux/netlink.h>文件里,由结构体nlmsghdr表示:
点击(此处)折叠或打开
- struct nlmsghdr
- {
- __u32 nlmsg_len; /* Length of message including header*/
- __u16 nlmsg_type; /* Message content*/
- __u16 nlmsg_flags; /* Additional flags*/
- __u32 nlmsg_seq; /* Sequence number*/
- __u32 nlmsg_pid; /* Sending process PID*/
- };
消息头中各成员属性的解释及说明:
nlmsg_len:整个消息的长度,按字节计算。包括了Netlink消息头本身。
nlmsg_type:消息的类型,即是数据还是控制消息。目前(内核版本2.6.21)Netlink仅支持四种类型的控制消息,如下:
NLMSG_NOOP-空消息,什么也不做;
NLMSG_ERROR-指明该消息中包含一个错误;
NLMSG_DONE-如果内核通过Netlink队列返回了多个消息,那么队列的最后一条消息的类型为NLMSG_DONE,其余所有消息的nlmsg_flags属性都被设置NLM_F_MULTI位有效。
NLMSG_OVERRUN-暂时没用到。
nlmsg_flags:附加在消息上的额外说明信息,如上面提到的NLM_F_MULTI。摘录如下: 标记 | 作用及说明 |
NLM_F_REQUEST | 如果消息中有该标记位,说明这是一个请求消息。所有从用户空间到内核空间的消息都要设置该位,否则内核将向用户返回一个EINVAL无效参数的错误 |
NLM_F_MULTI | 消息从用户->内核是同步的立刻完成,而从内核->用户则需要排队。如果内核之前收到过来自用户的消息中有NLM_F_DUMP位为1的消息,那么内核就会向用户空间发送一个由多个Netlink消息组成的链表。除了最后个消息外,其余每条消息中都设置了该位有效。 |
NLM_F_ACK | 该消息是内核对来自用户空间的NLM_F_REQUEST消息的响应 |
NLM_F_ECHO | 如果从用户空间发给内核的消息中该标记为1,则说明用户的应用进程要求内核将用户发给它的每条消息通过单播的形式再发送给用户进程。和我们通常说的“回显”功能类似。 |
… | … |
nlmsg_seq:消息序列号。因为Netlink是面向数据报的,所以存在丢失数据的风险,但是Netlink提供了如何确保消息不丢失的机制,让程序开发人员根据其实际需求而实现。消息序列号一般和NLM_F_ACK类型的消息联合使用,如果用户的应用程序需要保证其发送的每条消息都成功被内核收到的话,那么它发送消息时需要用户程序自己设置序号,内核收到该消息后对提取其中的序列号,然后在发送给用户程序回应消息里设置同样的序列号。有点类似于TCP的响应和确认机制。
注意:当内核主动向用户空间发送广播消息时,消息中的该字段总是为0。
Netlink的消息体
Netlink的消息体采用TLV(Type-Length-Value)格式:
Netlink提供的错误指示消息
当用户空间的应用程序和内核空间的进程之间通过Netlink通信时发生了错误,Netlink必须向用户空间通报这种错误。Netlink对错误消息进行了单独封装,<include/linux/netlink.h>:点击(此处)折叠或打开
- struct nlmsgerr
- {
- int error;//标准的错误码,定义在errno.h头文件中。可以用perror()来解释
- struct nlmsghdr msg; //指明了哪条消息触发了结构体中error这个错误值
- };
Netlink编程需要注意的问题
基于Netlink的用户-内核通信,有两种情况可能会导致丢包:
1、内存耗尽;
2、用户空间接收进程的缓冲区溢出。导致缓冲区溢出的主要原因有可能是:用户空间的进程运行太慢;或者接收队列太短。
如果Netlink不能将消息正确传递到用户空间的接收进程,那么用户空间的接收进程在调用recvmsg()系统调用时就会返回一个内存不足(ENOBUFS)的错误,这一点需要注意。换句话说,缓冲区溢出的情况是不会发送在从用户->内核的sendmsg()系统调用里,原因前面我们也说过了,请大家自己思考一下。
当然,如果使用的是阻塞型socket通信,也就不存在内存耗尽的隐患了,这又是为什么呢?赶紧去谷歌一下,查查什么是阻塞型socket吧。学而不思则罔,思而不学则殆嘛。
Netlink的地址结构体
在TCP博文中我们提到过在Internet编程过程中所用到的地址结构体和标准地址结构体,它们和Netlink地址结构体的关系如下:struct sockaddr_nl{}的详细定义和描述如下:
点击(此处)折叠或打开
- struct sockaddr_nl
- {
- sa_family_t nl_family; /*该字段总是为AF_NETLINK */
- unsigned short nl_pad; /* 目前未用到,填充为0*/
- __u32 nl_pid; /* process pid */
- __u32 nl_groups; /* multicast groups mask*/
- };
nl_pid:该属性为发送或接收消息的进程ID,前面我们也说过,Netlink不仅可以实现用户-内核空间的通信还可使现实用户空间两个进程之间,或内核空间两个进程之间的通信。该属性为0时一般适用于如下两种情况:
第一,我们要发送的目的地是内核,即从用户空间发往内核空间时,我们构造的Netlink地址结构体中nl_pid通常情况下都置为0。这里有一点需要跟大家交代一下,在Netlink规范里,PID全称是Port-ID(32bits),其主要作用是用于唯一的标识一个基于netlink的socket通道。通常情况下nl_pid都设置为当前进程的进程号。然而,对于一个进程的多个线程同时使用netlink socket的情况,nl_pid的设置一般采用如下这个样子来实现:
点击(此处)折叠或打开
- pthread_self()<< 16| getpid();
第二,从内核发出的多播报文到用户空间时,如果用户空间的进程处在该多播组中,那么其地址结构体中nl_pid也设置为0,同时还要结合下面介绍到的另一个属性。
nl_groups:如果用户空间的进程希望加入某个多播组,则必须执行bind()系统调用。该字段指明了调用者希望加入的多播组号的掩码(注意不是组号,后面我们会详细讲解这个字段)。如果该字段为0则表示调用者不希望加入任何多播组。对于每个隶属于Netlink协议域的协议,最多可支持32个多播组(因为nl_groups的长度为32比特),每个多播组用一个比特来表示。
关于Netlink剩下的知识点,我们在后面的实战环节有用到时再讨论。
今天我们来动手演练一下Netlink的用法,看看它到底是如何实现用户-内核空间的数据通信的。我们依旧是在2.6.21的内核环境下进行开发。 在</usr/include/linux/netlink.h>文件里包含了Netlink协议簇已经定义好的一些预定义协议:
点击(此处)折叠或打开
- #define NETLINK_ROUTE 0 /* Routing/device hook */
- #define NETLINK_UNUSED 1 /* Unused number */
- #define NETLINK_USERSOCK 2 /* Reservedfor 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 messagesto userspace*/
- #define NETLINK_GENERIC 16
- /* leave roomfor NETLINK_DM(DM Events)*/
- #define NETLINK_SCSITRANSPORT 18 /* SCSI Transports*/
- #define NETLINK_ECRYPTFS 19
- #define NETLINK_TEST 20 /* 用户添加的自定义协议 */
如果我们在Netlink协议簇里开发一个新的协议,只要在该文件中定义协议号即可,例如我们定义一种基于Netlink协议簇的、协议号是20的自定义协议,如上所示。同时记得,将内核头文件目录中的netlink.h也做对应的修改,在我的系统中它的路径是:/usr/src/linux-2.6.21/include/linux/netlink.h
接下来我们在用户空间以及内核空间模块的开发过程中就可以使用这种协议了,一共分为三个阶段。
Stage 1:
我们首先实现的功能是用户->内核的单向数据通信,即用户空间发送一个消息给内核,然后内核将其打印输出,就这么简单。用户空间的示例代码如下【mynlusr.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 /*消息最大负载为1024字节*/
- int main(int argc, char* argv[])
- {
- struct sockaddr_nl dest_addr;
- struct nlmsghdr *nlh = NULL;
- struct iovec iov;
- int sock_fd=-1;
- struct msghdr msg;
- if(-1==(sock_fd=socket(PF_NETLINK, SOCK_RAW,NETLINK_TEST))){ //创建套接字
- perror("can't create netlink socket!");
- return 1;
- }
- memset(&dest_addr, 0, sizeof(dest_addr));
- dest_addr.nl_family = AF_NETLINK;
- dest_addr.nl_pid = 0; /*我们的消息是发给内核的*/
- dest_addr.nl_groups = 0; /*在本示例中不存在使用该值的情况*/
- //将套接字和Netlink地址结构体进行绑定
- if(-1== bind(sock_fd,(struct sockaddr*)&dest_addr, sizeof(dest_addr))){
- perror("can't bind sockfd with sockaddr_nl!");
- return 1;
- }
- if(NULL==(nlh=(struct nlmsghdr*)malloc(NLMSG_SPACE(MAX_PAYLOAD)))){
- perror("alloc mem failed!");
- return 1;
- }
- memset(nlh,0,MAX_PAYLOAD);
- /* 填充Netlink消息头部*/
- nlh->nlmsg_len = NLMSG_SPACE(MAX_PAYLOAD);
- nlh->nlmsg_pid = 0;
- nlh->nlmsg_type = NLMSG_NOOP; //指明我们的Netlink是消息负载是一条空消息
- nlh->nlmsg_flags= 0;
- /*设置Netlink的消息内容,来自我们命令行输入的第一个参数*/
- strcpy(NLMSG_DATA(nlh), argv[1]);
- /*这个是模板,暂时不用纠结为什么要这样用。有时间详细讲解socket时再说*/
- memset(&iov, 0, sizeof(iov));
- iov.iov_base = (void *)nlh;
- iov.iov_len = nlh->nlmsg_len;
- memset(&msg, 0, sizeof(msg));
- msg.msg_iov = &iov;
- msg.msg_iovlen = 1;
- sendmsg(sock_fd,&msg, 0);//通过Netlink socket向内核发送消息
- /* 关闭netlink套接字*/
- close(sock_fd);
- free(nlh);
- return 0;
- }
上面的代码逻辑已经非常清晰了,都是socket编程的API,唯一不同的是我们这次编程是针对Netlink协议簇的。这里我们提前引入了BSD层的消息结构体struct msghdr{},定义在<include/linux/socket.h>文件里,以及其数据块structiovec{}定义在<include/linux/uio.h>头文件里。这里就不展开了,大家先记住这个用法就行。以后有时间再深入到socket的骨子里去转悠一番。
另外,需要格外注意的就是Netlink的地址结构体和其消息头结构中pid字段为0的情况,很容易让人产生混淆,再总结一下:
| 0 |
netlink地址结构体.nl_pid | 1、内核发出的多播报文 2、消息的接收方是内核,即从用户空间发往内核的消息 |
netlink消息头体. nlmsg_pid | 来自内核主动发出的消息 |
这个例子仅是从用户空间到内核空间的单向数据通信,所以Netlink地址结构体中我们设置了dest_addr.nl_pid = 0,说明我们的报文的目的地是内核空间;在填充Netlink消息头部时,我们做了nlh->nlmsg_pid = 0这样的设置。
需要注意几个宏的使用:
NLMSG_SPACE(MAX_PAYLOAD),该宏用于返回不小于MAX_PAYLOAD且4字节对齐的最小长度值,一般用于向内存系统申请空间是指定所申请的内存字节数,和NLMSG_LENGTH(len)所不同的是,前者所申请的空间里不包含Netlink消息头部所占的字节数,后者是消息负载和消息头加起来的总长度。
NLMSG_DATA(nlh),该宏用于返回Netlink消息中数据部分的首地址,在写入和读取消息数据部分时会用到它。
它们之间的关系如下:内核空间的示例代码如下【mynlkern.c】:
点击(此处)折叠或打开
- #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 <linux/netlink.h>
- MODULE_LICENSE("GPL");
- MODULE_AUTHOR("Koorey King");
- struct sock *nl_sk = NULL;
- static void nl_data_ready(struct sock*sk,intlen)
- {
- struct sk_buff *skb;
- struct nlmsghdr *nlh = NULL;
- while((skb= skb_dequeue(&sk->sk_receive_queue))!=NULL)
- {
- nlh = (struct nlmsghdr *)skb->data;
- printk("%s: received netlink message payload: %s \n", __FUNCTION__,(char*)NLMSG_DATA(nlh));
- kfree_skb(skb);
- }
- printk("recvied finished!\n");
- }
- static int __init myinit_module()
- {
- printk("my netlink in\n");
- nl_sk = netlink_kernel_create(NETLINK_TEST,0,nl_data_ready,THIS_MODULE);
- return 0;
- }
- static void __exit mycleanup_module()
- {
- printk("my netlink out!\n");
- sock_release(nl_sk->sk_socket);
- }
- module_init(myinit_module);
- module_exit(mycleanup_module);
在内核模块的初始化函数里我们用
nl_sk =netlink_kernel_create(NETLINK_TEST,0,nl_data_ready,THIS_MODULE);
创建了一个内核态的socket,第一个参数我们扩展的协议号;第二个参数为多播组号,目前我们用不上,将其置为0;第三个参数是个回调函数,即当内核的Netlink socket套接字收到数据时的处理函数;第四个参数就不多说了。
在回调函数nl_data_ready()中,我们不断的从socket的接收队列去取数据,一旦拿到数据就将其打印输出。在协议栈的INET层,用于存储数据的是大名鼎鼎的sk_buff结构,所以我们通过nlh = (struct nlmsghdr *)skb->data;可以拿到netlink的消息体,然后通过NLMSG_DATA(nlh)定位到netlink的消息负载。
将上述代码编译后测试结果如下:
Stage 2:
我们将上面的代码稍加改造就可以实现用户<->内核的双向数据通信。
首先是改造用户空间的代码:点击(此处)折叠或打开
- #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 /*消息最大负载为1024字节*/
- int main(int argc, char* argv[])
- {
- struct sockaddr_nl dest_addr;
- struct nlmsghdr *nlh = NULL;
- struct iovec iov;
- int sock_fd=-1;
- struct msghdr msg;
- if(-1==(sock_fd=socket(PF_NETLINK, SOCK_RAW,NETLINK_TEST))){
- perror("can't create netlink socket!");
- return 1;
- }
- memset(&dest_addr, 0, sizeof(dest_addr));
- dest_addr.nl_family = AF_NETLINK;
- dest_addr.nl_pid = 0; /*我们的消息是发给内核的*/
- dest_addr.nl_groups = 0; /*在本示例中不存在使用该值的情况*/
- if(-1== bind(sock_fd,(struct sockaddr*)&dest_addr, sizeof(dest_addr))){
- perror("can't bind sockfd with sockaddr_nl!");
- return 1;
- }
- if(NULL==(nlh=(struct nlmsghdr*)malloc(NLMSG_SPACE(MAX_PAYLOAD)))){
- perror("alloc mem failed!");
- return 1;
- }
- memset(nlh,0,MAX_PAYLOAD);
- /* 填充Netlink消息头部*/
- nlh->nlmsg_len= NLMSG_SPACE(MAX_PAYLOAD);
- nlh->nlmsg_pid = getpid();//我们希望得到内核回应,所以得告诉内核我们ID号
- nlh->nlmsg_type= NLMSG_NOOP;//指明我们的Netlink是消息负载是一条空消息
- nlh->nlmsg_flags= 0;
- /*设置Netlink的消息内容,来自我们命令行输入的第一个参数*/
- strcpy(NLMSG_DATA(nlh), argv[1]);
- /*这个是模板,暂时不用纠结为什么要这样用。*/
- memset(&iov, 0, sizeof(iov));
- iov.iov_base = (void *)nlh;
- iov.iov_len = nlh->nlmsg_len;
- memset(&msg, 0, sizeof(msg));
- msg.msg_iov = &iov;
- msg.msg_iovlen = 1;
- sendmsg(sock_fd,&msg, 0);//通过Netlink socket向内核发送消息
- //接收内核消息的消息
- printf("waiting message from kernel!\n");
- memset((char*)NLMSG_DATA(nlh),0,1024);
- recvmsg(sock_fd,&msg,0);
- printf("Got response: %s\n",NLMSG_DATA(nlh));
- /* 关闭netlink套接字*/
- close(sock_fd);
- free(nlh);
- return 0;
- }
内核空间的修改如下:
点击(此处)折叠或打开
- #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> /*该文头文件里包含了linux/netlink.h,因为我们要用到net/netlink.h中的某些API函数,nlmsg_pug()*/
- MODULE_LICENSE("GPL");
- MODULE_AUTHOR("Koorey King");
- struct sock *nl_sk = 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 = 0; //如果目的组为内核或某一进程,该字段也置0
- message[slen] = '\0';
- memcpy(NLMSG_DATA(nlh), message, slen+1);
- //通过netlink_unicast()将消息发送用户空间由dstPID所指定了进程号的进程
- netlink_unicast(nl_sk,skb,dstPID,0);
- printk("send OK!\n");
- return;
- }
- static void nl_data_ready (struct sock *sk, intlen)
- {
- struct sk_buff *skb;
- struct nlmsghdr *nlh = NULL;
- while((skb= skb_dequeue(&sk->sk_receive_queue))!=NULL)
- {
- nlh = (struct nlmsghdr *)skb->data;
- printk("%s: received netlink message payload: %s \n", __FUNCTION__,(char*)NLMSG_DATA(nlh));
- kfree_skb(skb);
- sendnlmsg("I see you",nlh->nlmsg_pid); //发送者的进程ID我们已经将其存储在了netlink消息头部里的nlmsg_pid字段里,所以这里可以拿来用。
- }
- printk("recvied finished!\n");
- }
- static int __init myinit_module()
- {
- printk("my netlink in\n");
- nl_sk = netlink_kernel_create(NETLINK_TEST,0,nl_data_ready,THIS_MODULE);
- return 0;
- }
- static void __exit mycleanup_module()
- {
- printk("my netlink out!\n");
- sock_release(nl_sk->sk_socket);
- }
- module_init(myinit_module);
- module_exit(mycleanup_module);
重新编译后,测试结果如下:
Stage 3:
前面我们提到过,如果用户进程希望加入某个多播组时才需要调用bind()函数。前面的示例中我们没有这个需求,可还是调了bind(),心头有些不爽。在前几篇博文里有关于socket编程时几个常见API的详细解释和说明,不明白的童鞋可以回头去复习一下。
因为Netlink是面向无连接的数据报的套接字,所以我们还可以用sendto()和recvfrom()来实现数据的收发,这次我们不再调用bind()。将Stage 2的例子稍加改造一下,用户空间的修改如下:
点击(此处)折叠或打开
- #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 /*消息最大负载为1024字节*/
- int main(int argc, char* argv[])
- {
- struct sockaddr_nl dest_addr;
- struct nlmsghdr *nlh = NULL;
- //struct iovec iov;
- int sock_fd=-1;
- //struct msghdr msg;
- if(-1==(sock_fd=socket(PF_NETLINK, SOCK_RAW,NETLINK_TEST))){
- perror("can't create netlink socket!");
- return 1;
- }
- memset(&dest_addr, 0, sizeof(dest_addr));
- dest_addr.nl_family = AF_NETLINK;
- dest_addr.nl_pid = 0; /*我们的消息是发给内核的*/
- dest_addr.nl_groups = 0; /*在本示例中不存在使用该值的情况*/
- /*不再调用bind()函数了
- if(-1 == bind(sock_fd, (struct sockaddr*)&dest_addr, sizeof(dest_addr))){
- perror("can't bind sockfd with sockaddr_nl!");
- return 1;
- }*/
- if(NULL==(nlh=(struct nlmsghdr*)malloc(NLMSG_SPACE(MAX_PAYLOAD)))){
- perror("alloc mem failed!");
- return 1;
- }
- memset(nlh,0,MAX_PAYLOAD);
- /* 填充Netlink消息头部*/
- nlh->nlmsg_len= NLMSG_SPACE(MAX_PAYLOAD);
- nlh->nlmsg_pid= getpid();//我们希望得到内核回应,所以得告诉内核我们ID号
- nlh->nlmsg_type= NLMSG_NOOP;//指明我们的Netlink是消息负载是一条空消息
- nlh->nlmsg_flags= 0;
- /*设置Netlink的消息内容,来自我们命令行输入的第一个参数*/
- strcpy(NLMSG_DATA(nlh), argv[1]);
- /*这个模板就用不上了。*/
- /*
- memset(&iov, 0, sizeof(iov));
- iov.iov_base = (void *)nlh;
- iov.iov_len = nlh->nlmsg_len;
- memset(&msg, 0, sizeof(msg));
- msg.msg_iov = &iov;
- msg.msg_iovlen = 1;
- */
- //sendmsg(sock_fd, &msg, 0); //不再用这种方式发消息到内核
- sendto(sock_fd,nlh,NLMSG_LENGTH(MAX_PAYLOAD),0,(struct sockaddr*)(&dest_addr),sizeof(dest_addr));
-
- //接收内核消息的消息
- printf("waiting message from kernel!\n");
- //memset((char*)NLMSG_DATA(nlh),0,1024);
- memset(nlh,0,MAX_PAYLOAD); //清空整个Netlink消息头包括消息头和负载
- //recvmsg(sock_fd,&msg,0);
- recvfrom(sock_fd,nlh,NLMSG_LENGTH(MAX_PAYLOAD),0,(struct sockaddr*)(&dest_addr),NULL);
- printf("Got response: %s\n",NLMSG_DATA(nlh));
- /* 关闭netlink套接字*/
- close(sock_fd);
- free(nlh);
- return 0;
- }
内核空间的代码完全不用修改,我们仍然用netlink_unicast()从内核空间发送消息到用户空间。
重新编译后,测试结果如下:和Stage 2中代码运行效果完全一样。也就是说,在开发Netlink程序过程中,如果没牵扯到多播机制,那么用户空间的socket代码其实是不用执行bind()系统调用的,但此时就需要用sendto()和recvfrom()完成数据的发送和接收的任务;如果执行了bind()系统调用,当然也可以继续用sendto()和recvfrom(),但给它们传递的参数就有所区别。这时候一般使用sendmsg()和recvmsg()来完成数据的发送和接收。大家根据自己的实际情况灵活选择。
关于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 /* Reservedfor 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 messagesto userspace*/
- #define NETLINK_GENERIC 16
- /* leave roomfor 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. Itis freeto usefor every
- * layer. Please put yourprivate variables there.If you
- * want to keep them across layers you havetodo a skb_clone()
- * first. Thisis owned by whoever has the skb queued ATM.
- */
- char cb[48];
- … …
- }
点击(此处)折叠或打开
- 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传递大数据时的处理等等都是开发人员需要考虑的问题。完。
开发和维护内核是一件很繁杂的工作,因此,只有那些最重要或者与系统性能息息相关的代码才将其安排在内核中。其它程序,比如GUI,管理以及控制部分的代码,一般都会作为用户态程序。在linux系统中,把系统的某个特性分割成在内核中和在用户空间中分别实现一部分的做法是很常见的(比如linux系统的防火墙就分成了内核态的Netfilter和用户态的iptables)。然而,内核程序与用户态的程序又是怎样行通讯的呢?
答案就是通过各种各样的用户态和内核态的IPC(interprocess communication )机制来实现。比如系统调用,ioctl接口,proc文件系统以及netlink socket,本文就是要讨论netlink socekt并向读者展示这种用网络
通讯接口方式实现的IPC机制的优点。
介绍:
netlink socekt是一种用于在内核态和用户态进程之间进行数据传输的特殊的IPC。它通过为内核模块提
供一组特殊的API,并为用户程序提供了一组标准的socket 接口的方式,实现了一种全双工的通讯连接。类似于TCP/IP中使用AF_INET地址族一样,netlink socket使用地址族AF_NETLINK。每一个netlink
socket在内核头文件
中定义自己的协议类型。
下面是netlink socket 目前的特性集合以及它支持的协议类型:
NETLINK_ROUTE 用户空间的路由守护程序之间的通讯通道,比如BGP,OSPF,RIP以及内核数据转发模块。用户态的路由守护程序通过此类型的协议来更新内核中的路由表。 NETLINK_FIREWALL:接收IPV4防火墙代码发送的数据包。 NETLINK_NFLOG:用户态的iptables管理工具和内核中的netfilter模块之间通讯的通道。 NETLINK_ARPD:用来从用户空间管理内核中的ARP表。 |
为什么以上的功能在实现用户程序和内核程序通讯时,都使用netlink方法而不是系统调用,ioctls
或者proc文件系统呢?原因在于:为新的特性添加一个新的系统调用,ioctls或者一个proc文件的做法并不是很容易的一件事情,因为我们要冒着污染内核代码并且可能破坏系统稳定性的风险去完成这件事情。
然而,netlink socket却是如此的简单,你只需要在文件netlink.h中添加一个常量来标识你的协议类型,然后,内核模块和用户程序就可以立刻使用socket风格的API进行通讯了!
Netlink提供了一种异步通讯方式,与其他socket API一样,它提供了一个socket队列来缓冲或者平滑
瞬时的消息高峰。发送netlink消息的系统调用在把消息加入到接收者的消息对列后,会触发接收者的接收处理函数。接收者在接收处理函数上下文中,可以决定立即处理消息还是把消息放在队列中,在以后其它上下文去处理它(因为我们希望接收处理函数执行的尽可能快)。系统调用与netlink不同,它需要一个同步的处理,因此,当我们使用一个系统调用来从用户态传递消息到内核时,如果处理这个消息的时间很长的话,内核调度的粒度就会受到影响。
内核中实现系统调用的代码都是在编译时静态链接到内核的,因此,在动态加载模块中去包含一个系统调用的做法是不合适的,那是大多数设备驱动的做法。使用netlink socket时,动态加载模块中的netlink程序不会和linux内核中的netlink部分产生任何编译时依赖关系。
Netlink优于系统调用,ioctls和proc文件系统的另外一个特点就是它支持多点传送。一个进程可以把消息传输给一个netlink组地址,然后任意多个进程都可以监听那个组地址(并且接收消息)。这种机制为内核到用户态的事件分发提供了一种近乎完美的解决方案。
系统调用和ioctl都属于单工方式的IPC,也就是说,这种IPC会话的发起者只能是用户态程序。但是,如果内核有一个紧急的消息想要通知给用户态程序时,该怎么办呢?如果直接使用这些IPC的话,是没办法做到这点的。通常情况下,应用程序会周期性的轮询内核以获取状态的改变,然而,高频度的轮询势必会增加系统的负载。Netlink 通过允许内核初始化会话的方式完美的解决了此问题,我们称之为netlink socket的双工特性。
最后,netlink socket提供了一组开发者熟悉的BSD风格的API函数,因此,相对于使用神秘的系统调用API或者ioctl而言,netlink开发培训的费用会更低些。
与BSD的Routing socket的关系
在BSD TCP/IP的协议栈实现中,有一种特殊的socket叫做Routing socket.它的地址族为AF_ROUTE, 协议族为PF_ROUTE, socket类型为SOCK_RAW. 这种Routing socket是用户态进程用来向内核中的路由表增加或者删除路由信息用的。在Linux系统中,netlink socket通过协议类型NETLINK_ROUTE实现了与Routing socket相同的功能,可以说,netlink socket提供了BSD Routing socket功能的超集。
Netlink Socket 的API
标准的socket API函数-
- 都能够被用户态程序直接调用来访问netlink socket.你可以访问man手册来获取这些函数的详细定义。在本文,我们只讨论怎样在netlink socket的上下文中为这些函数选择参数。这些API对于使用TCP/IP socket写过一些简单网络程序的读者来说应该很熟悉了。
使用socket()函数创建一个socket,输入:
int socket(int domain, int type, int protocol) |
socket域(地址族)是AF_NETLINK,socket的类型是SOCK_RAW或者SOCK_DGRAM,因为netlink是一种面向数据包的服务。
协议类型选择netlink要使用的类型即可。下面是一些预定义的netlink协议类型:
和 NETLINK_IP6_FW.
你同样可以很轻松的在netlink.h中添加自定义的协议类型。
每个netlink协议类型可以定义高达32个多点传输的组。每个组用一个比特位来表示,1<<i,0<=i<=31.
当一组用户态进程和内核态进程协同实现一个相同的特性时,这个方法很有用,因为发送多点传输的netlink消息可以减少系统调用的次数,并且减少了相关应用程序的个数,这些程序本来是要用来处理维护多点传输组之间关系而带来的负载的。
bind()函数
跟TCP/IP中的socket一样,netlink的bind()函数把一个本地socket地址(源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()函数调用时,sockaddr_nl的nl_pid属性的值可以设置为访问netlink socket的当前进程的PID,nl_pid作为这个netlink socket的本地地址。应用程序应该选择一个唯一的32位整数来填充nl_pid的值。
公式一使用进程的PID作为nl_pid的值,如果这个进程只需要一个该类型协议的netlink socket的话,选用进程pid作为nl_pid是一个很自然的做法。
换一种情形,如果一个进程的多个线程想要创建属于各个线程的相同协议类型的netlink socket的话,公式二可以用来为每个线程的netlink socket产生nl_pid值。
采用这种方法,同一进程的不同线程都能获取属于它们的相同协议类型的不同netlink socket。事实上,即便是在一个单独的线程里,也可能需要创建同一协议类型的多个netlink socket。所以开发人员需要更多聪明才智去创建不同的nl_pid值,然而本文中不会就如何创建多个不同的nl_pid的值进行过多的讨论
如果应用程序想要接收特定协议类型的发往指定多播组的netlink消息的话,所有接收组的比特位应该进行与运算,形成sockaddr_nl的 nl_groups域的值。否则的话,nl_groups应该设置为0,以便应用程序只能够收到发送给它的netlink消息。在填充完结构体 nladdr后,作如下的绑定工作:
bind(fd, (struct sockaddr*)&nladdr, sizeof(nladdr)
为了能够把一个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消息发送出去:
sendmsg(fd, &msg, 0); |
接收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:
参数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()函数的外面,因为长时间的消息处理过程可能会阻止其它系统调用进入内核。取而代之,我们可以牺牲一个内核线程来完成后续的无限的的处理动作。
使用
来接收消息。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相关的,所以,你应该把它插入到一个完整的内核模块代码当中去,这样的完整代码在其它代码中可以找到很多。
实例:
|