Netlink 内核实现分析(三):通信实现
参考自:
http://blog.chinaunix.net/uid-28541347-id-5578403.html
https://blog.csdn.net/jasenwan88/article/details/7365060
https://www.cnblogs.com/oracleloyal/p/5991819.html
https://blog.csdn.net/kanda2012/article/details/7580623
https://blog.csdn.net/u012819339/article/details/51334600
一:Netlink简介
(一)什么是Netlink?
Netlink是linux提供的用于内核和用户态进程之间的通信方式。但是注意虽然Netlink主要用于用户空间和内核空间的通信,但是也能用于用户空间的两个进程通信。只是进程间通信有其他很多方式,一般不用Netlink,除非需要用到Netlink的广播特性时。
(二)那么Netlink有什么优势呢?
一般来说用户空间和内核空间的通信方式有三种:/proc、ioctl、Netlink。而前两种都是单向的,但是Netlink可以实现双工通信。
(三)Netlink特点
Netlink协议基于BSD socket和AF_NETLINK地址簇(address family),使用32位的端口号寻址(以前称作PID),每个Netlink协议(或称作总线,man手册中则称之为netlink family),通常与一个或一组内核服务/组件相关联,如NETLINK_ROUTE用于获取和设置路由与链路信息、NETLINK_KOBJECT_UEVENT用于内核向用户空间的udev进程发送通知等。
netlink具有以下特点:
① 支持全双工、异步通信(当然同步也支持)
② 用户空间可使用标准的BSD socket接口(但netlink并没有屏蔽掉协议包的构造与解析过程,推荐使用libnl等第三方库)
③ 在内核空间使用专用的内核API接口
④ 支持多播(因此支持“总线”式通信,可实现消息订阅)
⑤ 在内核端可用于进程上下文与中断上下文
(四)NETLINK对比UDP
AF_NETLINK和AF_INET对应,是一个协议族,而NETLINK_ROUTE、NETLINK_GENERIC这些是协议,对应于UDP。
那么我们主要关注Netlink和UDP socket之间的不同点,其中最重要的一点就是:
使用UDP socket发送数据包时,用户无需构造UDP数据包的包头,内核协议栈会根据原、目的地址(sockaddr_in)填充头部信息。但是Netlink需要我们自己构造一个包头(这个包头有什么用,我们后面再说)。
一般我们使用Netlink都要指定一个协议,我们可以使用内核为我们预留的NETLINK_GENERIC(定义在linux/netlink.h中),也可以使用我们自定义的协议,其实就是定义一个内核还没有占用的数字。下面我们用NETLINK_TEST做为我们定义的协议写一个例子(注意:自定义协议不一定非要添加到linux/netlink.h中,只要用户态和内核态代码都能找到该定义就行)。我们知道使用UDP发送报文有两种方式:sendto和sendmsg,同样Netlink也支持这两种方式。下面先看使用sendmsg的方式。
二:用户态数据结构
首先看一下几个重要的数据结构的关系:
(一)struct msghdr
msghdr这个结构在socket变成中就会用到,并不算Netlink专有的。
我们知道socket消息的发送和接收函数一般有这几对:recv/send、readv/writev、recvfrom/sendto。当然还有recvmsg/sendmsg,前面三对函数各有各的特点功能,而recvmsg/sendmsg就是要囊括前面三对的所有功能,当然还有自己特殊的用途。msghdr的前两个成员就是为了满足recvfrom/sendto的功能,中间两个成员msg_iov和msg_iovlen则是为了满足readv/writev的功能,而最后的msg_flags则是为了满足recv/send中flag的功能,剩下的msg_control和msg_controllen则是满足recvmsg/sendmsg特有的功能。
struct user_msghdr { void __user *msg_name; /* ptr to socket address structure */ int msg_namelen; /* size of socket address structure */ struct iovec __user *msg_iov; /* scatter/gather array */ __kernel_size_t msg_iovlen; /* # elements in msg_iov */ void __user *msg_control; /* ancillary data */ __kernel_size_t msg_controllen; /* ancillary data buffer length */ unsigned int msg_flags; /* flags on received message */ };
应用层向内核传递消息可以使用sendto()或sendmsg()函数,其中sendmsg函数需要应用程序手动封装msghdr消息结构,而sendto()函数则会由内核代为分配。其中
(1)msg_name:指向数据包的目的地址;
(2)msg_namelen:目的地址数据结构的长度;
(3)msg_iov:消息包的实际数据块,定义如下:
struct iovec { void *iov_base; /* BSD uses caddr_t (1003.1g requires void *) */ __kernel_size_t iov_len; /* Must be size_t (1003.1g) */ };
iov_base:消息包实际载荷的首地址;
iov_len:消息实际载荷的长度。
(4)msg_control:消息的辅助数据;
(5)msg_controllen:消息辅助数据的大小;
(6)msg_flags:接收消息的标识。
对于该结构,我们更需要关注的是前三个变量参数,对于netlink数据包来说其中msg_name指向的就是目的sockaddr_nl地址结构实例的首地址,iov_base指向的就是消息实体中的nlmsghdr消息头的地址,而iov_len赋值为nlmsghdr中的nlmsg_len即可(消息头+实际数据)。
(二)struct sockaddr_nl
struct sockaddr_nl为Netlink的地址,和我们通常socket编程中的sockaddr_in作用一样,他们的结构对比如下:
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 */ };
(1) nl_pid:在Netlink规范里,PID全称是Port-ID(32bits),其主要作用是用于唯一的标识一个基于netlink的socket通道。通常情况下nl_pid都设置为当前进程的进程号。前面我们也说过,Netlink不仅可以实现用户-内核空间的通信还可使现实用户空间两个进程之间,或内核空间两个进程之间的通信。该属性为0时一般指内核。
(2) nl_groups:如果用户空间的进程希望加入某个多播组,则必须执行bind()系统调用。该字段指明了调用者希望加入的多播组号的掩码(注意不是组号,后面我们会详细讲解这个字段)。如果该字段为0则表示调用者不希望加入任何多播组。对于每个隶属于Netlink协议域的协议,最多可支持32个多播组(因为nl_groups的长度为32比特),每个多播组用一个比特来表示。
(三)struct nlmsghdr
Netlink的报文由消息头和消息体构成,struct nlmsghdr即为消息头。消息头定义在文件里,由结构体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 */ };
消息头中各成员属性的解释及说明:
(1) nlmsg_len:整个消息的长度,按字节计算。包括了Netlink消息头本身。
(2) nlmsg_type:消息的类型,即是数据还是控制消息。目前(内核版本2.6.21)Netlink仅支持四种类型的控制消息,如下:
a) NLMSG_NOOP-空消息,什么也不做;不执行任何动作,必须将该消息丢弃;
b) NLMSG_ERROR-指明该消息中包含一个错误;
c) NLMSG_DONE-标识分组消息的末尾;如果内核通过Netlink队列返回了多个消息,那么队列的最后一条消息的类型为NLMSG_DONE,其余所有消息的nlmsg_flags属性都被设置NLM_F_MULTI位有效。
d) NLMSG_OVERRUN-缓冲区溢出,表示某些消息已经丢失。
(3) nlmsg_flags:附加在消息上的额外说明信息,如上面提到的NLM_F_MULTI。消息标记,它们用以表示消息的类型.
#define NLM_F_REQUEST 1 /* It is request message. */ #define NLM_F_MULTI 2 /* Multipart message, terminated by NLMSG_DONE */ #define NLM_F_ACK 4 /* Reply with ack, with zero or error code */ #define NLM_F_ECHO 8 /* Echo this request */ #define NLM_F_DUMP_INTR 16 /* Dump was inconsistent due to sequence change */ /* Modifiers to GET request */ #define NLM_F_ROOT 0x100 /* specify tree root */ #define NLM_F_MATCH 0x200 /* return all matching */ #define NLM_F_ATOMIC 0x400 /* atomic GET */ #define NLM_F_DUMP (NLM_F_ROOT|NLM_F_MATCH) /* Modifiers to NEW request */ #define NLM_F_REPLACE 0x100 /* Override existing */ #define NLM_F_EXCL 0x200 /* Do not touch, if it exists */ #define NLM_F_CREATE 0x400 /* Create, if it does not exist */ #define NLM_F_APPEND 0x800 /* Add to end of list */
那消息体怎么设置呢?可以使用NLMSG_DATA,具体见后面例子。
(四)netlink消息处理宏
#define NLMSG_ALIGNTO 4U #define NLMSG_ALIGN(len) ( ((len)+NLMSG_ALIGNTO-1) & ~(NLMSG_ALIGNTO-1) ) /* 对len执行4字节对齐 */ #define NLMSG_HDRLEN ((int) NLMSG_ALIGN(sizeof(struct nlmsghdr))) /* netlink消息头长度 */ #define NLMSG_LENGTH(len) ((len) + NLMSG_HDRLEN) /* netlink消息载荷len加上消息头 */ #define NLMSG_SPACE(len) NLMSG_ALIGN(NLMSG_LENGTH(len)) /* 对netlink消息全长执行字节对齐 */ #define NLMSG_DATA(nlh) ((void*)(((char*)nlh) + NLMSG_LENGTH(0))) /* 获取netlink消息实际载荷位置 */ #define NLMSG_NEXT(nlh,len) ((len) -= NLMSG_ALIGN((nlh)->nlmsg_len), \ (struct nlmsghdr*)(((char*)(nlh)) + NLMSG_ALIGN((nlh)->nlmsg_len)))/* 取得下一个消息的首地址,同时len也减少为剩余消息的总长度 */ #define NLMSG_OK(nlh,len) ((len) >= (int)sizeof(struct nlmsghdr) && \ (nlh)->nlmsg_len >= sizeof(struct nlmsghdr) && \ (nlh)->nlmsg_len <= (len)) /* 验证消息的长度 */ #define NLMSG_PAYLOAD(nlh,len) ((nlh)->nlmsg_len - NLMSG_SPACE((len))) /* 返回PAYLOAD的长度 */
Linux为了处理netlink消息方便,在 include/uapi/linux/netlink.h中定义了以上消息处理宏,用于各种场合。对于Netlink消息来说,处理如下格式(见netlink.h):
/* ======================================================================== * Netlink Messages and Attributes Interface (As Seen On TV) * ------------------------------------------------------------------------ * Messages Interface * ------------------------------------------------------------------------ * * Message Format: * <--- nlmsg_total_size(payload) ---> * <-- nlmsg_msg_size(payload) -> * +----------+- - -+-------------+- - -+-------- - - * | nlmsghdr | Pad | Payload | Pad | nlmsghdr * +----------+- - -+-------------+- - -+-------- - - * nlmsg_data(nlh)---^ ^ * nlmsg_next(nlh)-----------------------+ * * Payload Format: * <---------------------- nlmsg_len(nlh) ---------------------> * <------ hdrlen ------> <- nlmsg_attrlen(nlh, hdrlen) -> * +----------------------+- - -+--------------------------------+ * | Family Header | Pad | Attributes | * +----------------------+- - -+--------------------------------+ * nlmsg_attrdata(nlh, hdrlen)---^ * * ------------------------------------------------------------------------ * Attributes Interface * ------------------------------------------------------------------------ * * Attribute Format: * <------- nla_total_size(payload) -------> * <---- nla_attr_size(payload) -----> * +----------+- - -+- - - - - - - - - +- - -+-------- - - * | Header | Pad | Payload | Pad | Header * +----------+- - -+- - - - - - - - - +- - -+-------- - - * <- nla_len(nla) -> ^ * nla_data(nla)----^ | * nla_next(nla)-----------------------------' * *========================================================================= */
三:内核态实现
(一)内核netlink api
1.创建netlink socket
struct sock *netlink_kernel_create(struct net *net,
int unit,unsigned int groups,
void (*input)(struct sk_buff *skb),
struct mutex *cb_mutex,struct module *module);
参数说明:
(1) net:是一个网络名字空间namespace,在不同的名字空间里面可以有自己的转发信息库,有自己的一套net_device等等。默认情况下都是使用 init_net这个全局变量。
(2) unit:表示netlink协议类型,如NETLINK_TEST、NETLINK_SELINUX。
(3) groups:多播地址。
(4) input:为内核模块定义的netlink消息处理函数,当有消 息到达这个netlink socket时,该input函数指针就会被引用,且只有此函数返回时,调用者的sendmsg才能返回。
(5) cb_mutex:为访问数据时的互斥信号量。
(6) module: 一般为THIS_MODULE。
2.发送单播消息netlink_unicast
int netlink_unicast(struct sock *ssk, struct sk_buff *skb, u32 pid, int nonblock)
参数说明:
(1) ssk:为函数 netlink_kernel_create()返回的socket。
(2) skb:存放消息,它的data字段指向要发送的netlink消息结构,而 skb的控制块保存了消息的地址信息,宏NETLINK_CB(skb)就用于方便设置该控制块。
(3) pid:为接收此消息进程的pid,即目标地址,如果目标为组或内核,它设置为 0。
(4) nonblock:表示该函数是否为非阻塞,如果为1,该函数将在没有接收缓存可利用时立即返回;而如果为0,该函数在没有接收缓存可利用定时睡眠。
3.发送广播消息netlink_broadcast
int netlink_broadcast(struct sock *ssk, struct sk_buff *skb, u32 pid, u32 group, gfp_t allocation)
前面的三个参数与 netlink_unicast相同,参数group为接收消息的多播组,该参数的每一个位代表一个多播组,因此如果发送给多个多播组,就把该参数设置为多个多播组组ID的位或。参数allocation为内核内存分配类型,一般地为GFP_ATOMIC或GFP_KERNEL,GFP_ATOMIC用于原子的上下文(即不可以睡眠),而GFP_KERNEL用于非原子上下文。
4.释放netlink socket
void netlink_kernel_release(struct sock *sk)
(二)代码实现
#include <linux/init.h> #include <linux/kernel.h> #include <linux/module.h> #include <linux/types.h> #include <linux/sched.h> #include <linux/netlink.h> #include <net/sock.h> #define NETLINK_USER 22 #define USER_MSG (NETLINK_USER+1) #define USER_PORT 50 static int stringlen(char* buf); static int send_msg(char* pbuf,int pid); static void recv_cb(struct sk_buff* skb); static struct sock* netlink_fd = NULL; struct netlink_kernel_cfg cfg = { .input = recv_cb, }; static int __init test_netlink_init(void) { printk("init netlink!\n"); netlink_fd = netlink_kernel_create(&init_net,USER_MSG,&cfg); if(!netlink_fd) { printk(KERN_ERR "can not create a netlink socket!\n"); return -1; } printk("netlink init successful!\n"); return 0; } static void __exit test_netlink_exit(void) { printk("exit netlink!\n"); sock_release(netlink_fd->sk_socket); printk(KERN_DEBUG "netlink exit!\n"); } module_init(test_netlink_init); module_exit(test_netlink_exit); MODULE_AUTHOR("SSYFJ"); MODULE_LICENSE("GPL"); static void recv_cb(struct sk_buff* __skb) { struct nlmsghdr* nlh = NULL; void* data = NULL; int pid; struct sk_buff* skb = skb_get(__skb); //通过为原始数据__skb添加引用来获取数据 printk("skb->len:%u\n",skb->len); if(skb->len>=NLMSG_SPACE(0)) //NLMSG_SPACE(0)表示 只有头部的大小 { nlh = nlmsg_hdr(skb); printk("nlmsg->len:%u %u\n",nlh->nlmsg_len,nlmsg_len(nlh)); data = NLMSG_DATA(nlh); if(data) { printk("kernel receive data:%s\n",(int8_t*)data); pid = nlh->nlmsg_pid; send_msg("hello userspace",pid); } } kfree_skb(skb); //前面引用+1,这里一定要释放,不然会warn } static int stringlen(char* buf) { int len = 0; for(;*buf;buf++) { len++; } return len; } static int send_msg(char* pbuf,int pid) { int len = stringlen(pbuf),ret; struct sk_buff* nl_skb; struct nlmsghdr* nlh; nl_skb = nlmsg_new(len,GFP_ATOMIC); if(!nl_skb) { printk("netlink_alloc_skb error!\n"); return -1; } nlh = nlmsg_put(nl_skb,0,0,pid,len,0); if(!nlh) { printk("nlmsg_put() error!\n"); nlmsg_free(nl_skb); return -1; } memcpy(nlmsg_data(nlh),pbuf,len); ret = netlink_unicast(netlink_fd,nl_skb,pid,MSG_DONTWAIT); return ret; }
(三)Makefile文件
obj-m := kern.o KDIR := /lib/modules/$(shell uname -r)/build PWD := $(shell pwd) default: $(MAKE) -C $(KDIR) M=$(PWD) modules
注意:kern.o与我们编写的代码文件一致
(四)内核操作
sudo insmod kern.ko
sudo rmmod kern
其中kern是我们编译出来的内核文件,对于ko文件的插入移除,可以通过dmesg查看printk的打印结果
四:用户态实现
(一)sendmsg与recvmsg方法的用户态实现
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <errno.h> #include <sys/stat.h> #include <sys/types.h> #include <sys/socket.h> #include <linux/netlink.h> #define NETLINK_USER 22 //从22开始到32可以自定义Netlink通信宏 #define USER_MSG (NETLINK_USER+1) //自定义用于和内核空间通信 #define MAX_PLOAD 1024 //nlmsg最大负载(包含头部) int main(int argc,char** argv) { char* data = "hello kernel space by user 1"; int sockfd; struct sockaddr_nl local,remote; struct nlmsghdr* nlh = NULL; struct iovec iov; struct msghdr msg; int ret; /* 第一个参数必须是 AF_NETLINK 或 PF_NETLINK,在 Linux 中,它们俩实际为一个东西,它表示要使用netlink, 第二个参数必须是SOCK_RAW或SOCK_DGRAM,因为netlink是一种面向数据包的服务. 第三个参数指定netlink协议类型,可以是自己在netlink.h中定义的,也可以是内核中已经定义好的。 */ sockfd = socket(AF_NETLINK,SOCK_RAW,USER_MSG); if(sockfd == -1) { printf("create socket failure! %s\n",strerror(errno)); return -1; } memset(&local,0,sizeof(local)); local.nl_family = AF_NETLINK; local.nl_pid = 50; //nl_pid 实际上未必是进程 ID,它只是用于区分不同的接收者或发送者的一个标识,可以看做通信双方端口。 local.nl_groups = 0; ret = bind(sockfd,(struct sockaddr*)&local,sizeof(local)); if(ret < 0) { printf("bind() error!\n"); close(sockfd); return -1; } memset(&remote,0,sizeof(remote)); remote.nl_family = AF_NETLINK; remote.nl_pid = 0; //为0表示与内核通信 remote.nl_groups = 0; //1.先设置nlh格式和数据 nlh = (struct nlmsghdr*)malloc(NLMSG_SPACE(MAX_PLOAD)); if(!nlh) { printf("malloc nlmsghdr error!\n"); close(sockfd); return -1; } memset(nlh,0,sizeof(struct nlmsghdr)); nlh->nlmsg_len = NLMSG_SPACE(MAX_PLOAD); nlh->nlmsg_flags = 0; nlh->nlmsg_type = 0; nlh->nlmsg_seq = 0; nlh->nlmsg_pid = local.nl_pid; memcpy(NLMSG_DATA(nlh),data,strlen(data)); //2.设置iovec数据 iov.iov_base = (void*)nlh; iov.iov_len = NLMSG_SPACE(MAX_PLOAD); //3.设置msg数据 memset(&msg,0,sizeof(msg)); msg.msg_name = (void*)&remote; msg.msg_namelen = sizeof(remote); msg.msg_iov = &iov; msg.msg_iovlen = 1; //开始发送数据 printf("sendmsg start....\n"); ret = sendmsg(sockfd,&msg,0); if(ret < 0) { perror("send to kernel failure!\n"); close(sockfd); free(nlh); exit(-1); } //接受数据 printf("recvmsg start....\n"); ret = recvmsg(sockfd,&msg,0); if(ret < 0) { perror("recv from kernel failure!\n"); close(sockfd); free(nlh); exit(-1); } printf("recv data:%s\n", (char*)NLMSG_DATA(nlh)); close(sockfd); free((void*)nlh); return 0; }
(二)sendto与recvfrom方法的用户态实现
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <errno.h> #include <sys/stat.h> #include <sys/types.h> #include <sys/socket.h> #include <linux/netlink.h> #define NETLINK_USER 22 #define USER_MSG (NETLINK_USER+1) #define MAX_PLOAD 1024 int main(int argc,char** argv) { char* data = "hello kernel space by user 2"; int sockfd,ret; struct sockaddr_nl local,remote; struct nlmsghdr* nlh = NULL; sockfd = socket(AF_NETLINK,SOCK_RAW,USER_MSG); if(sockfd == -1) { printf("create socket failure! %s\n",strerror(errno)); return -1; } memset(&local,0,sizeof(local)); local.nl_family = AF_NETLINK; local.nl_pid = 50; local.nl_groups = 0; if(bind(sockfd,(struct sockaddr*)&local,sizeof(local)) != 0) { printf("bind() error!\n"); close(sockfd); return -1; } memset(&remote,0,sizeof(remote)); remote.nl_family = AF_NETLINK; remote.nl_pid = 0; remote.nl_groups = 0; nlh = (struct nlmsghdr*)malloc(NLMSG_SPACE(MAX_PLOAD)); memset(nlh,0,sizeof(struct nlmsghdr)); nlh->nlmsg_len = NLMSG_SPACE(MAX_PLOAD); nlh->nlmsg_flags = 0; nlh->nlmsg_type = 0; nlh->nlmsg_seq = 0; nlh->nlmsg_pid = local.nl_pid; printf("sendmsg start....\n"); memcpy(NLMSG_DATA(nlh),data,strlen(data)); ret = sendto(sockfd,nlh,nlh->nlmsg_len,0,(struct sockaddr*)&remote,sizeof(struct sockaddr_nl)); if(ret<0) { perror("send to kernel failure!\n"); close(sockfd); exit(-1); } //接受数据 printf("recvmsg start....\n"); memset(nlh,0,NLMSG_SPACE(MAX_PLOAD)); ret = recvfrom(sockfd,nlh,NLMSG_LENGTH(MAX_PLOAD),0,NULL,NULL); if(ret<0) { printf("recv from kernel failure!\n"); close(sockfd); exit(-1); } printf("recv data:%s\n",(char*)NLMSG_DATA(nlh)); close(sockfd); free((void*)nlh); return 0; }
(三)两种情况测试
使用gcc编译:
gcc ./user.c -o user
gcc ./user2.c -o user2