今天我们来动手演练一下Netlink的用法,看看它到底是如何实现用户-内核空间的数据通信的。我们依旧是在2.6.21的内核环境下进行开发。

在文件里包含了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协议簇里开发一个新的协议,只要在该文件中定义协议号即可,例如我们定义一种基于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{},定义在文件里,以及其数据块struct iovec{}定义在头文件里。这里就不展开了,大家先记住这个用法就行。以后有时间再深入到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, int len)
{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 /*该文头文件里包含了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标识,如果是内核发的则置0NETLINK_CB(skb).dst_group = 0; //如果目的组为内核或某一进程,该字段也置0message[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, int len)
{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 0003 -- Netlink动手实践相关推荐

  1. guice google_与Google Guice的动手实践

    guice google by Sankalp Bhatia 通过Sankalp Bhatia 与Google Guice的动手实践 (A hands-on session with Google G ...

  2. 我的XGBoost学习经历及动手实践

    ↑↑↑关注后"星标"Datawhale 每日干货 & 每月组队学习,不错过 Datawhale干货 作者:李祖贤  深圳大学,Datawhale高校群成员 知乎地址:htt ...

  3. 两个月入门深度学习,全靠动手实践!一位前端小哥的经验分享

    两个月入门深度学习,全靠动手实践!一位前端小哥的经验分享   在当前社会,技术日新月异,一个全栈工程师不及时学习新知识,掌握AI技能,再过两年就算不上"全栈"了. 产品发烧友.前端 ...

  4. 产品经理也能动手实践的AI(二)- 做个识别宠物的AI

    https://www.toutiao.com/a6673604688056680972/ 上一篇产品经理也能动手实践的AI(一)- FastAI介绍,介绍了为什么选择FastAI,为什么适合产品经理 ...

  5. 产品经理也能动手实践的AI(一)- FastAI介绍

    https://www.toutiao.com/a6671600787744883213/ 如果你还不了解AI的准确含义,请查看我之前的文章: 人人都能搞懂的AI(一) 人人都能搞懂的AI(二)- A ...

  6. 《Hadoop与大数据挖掘》一2.3.5 动手实践:运行MapReduce任务

    本节书摘来华章计算机<Hadoop与大数据挖掘>一书中的第2章 ,第2.3.5节,张良均 樊 哲 位文超 刘名军 许国杰 周 龙 焦正升 著 更多章节内容可以访问云栖社区"华章计 ...

  7. 【原创 HadoopSpark 动手实践 6】Spark 编程实例与案例演示

     [原创 Hadoop&Spark 动手实践 6]Spark 编程实例与案例演示 Spark 编程实例和简易电影分析系统的编写 目标: 1. 掌握理论:了解Spark编程的理论基础 2. 搭建 ...

  8. 中国.NET开发者峰会特别活动-基于k8s的微服务和CI/CD动手实践报名

    2019.11.9 的中国.NET开发者峰会将在上海举办,到目前为止,大会的主题基本确定,这两天就会和大家会面,很多社区的同学基于对社区的信任在我们议题没有确定的情况下已经购票超过了300张,而且分享 ...

  9. 《JavaScript高级程序设计》红宝书第二遍阅读(动手实践)

    <JavaScript高级程序设计>红宝书第二遍阅读(动手实践) 第1章--什么是JavaScript 第2章--HTML中的JavaScript 第3章--语言基础 第4章--变量.作用 ...

  10. linux内核内存映射实验报告,动手实践-Linux内存映射基础(上)

    Linux内核分析与应用 西安邮电大学(陈莉君) 在庞大的Linux内核源代码学习中,如何抓住主要线索和思路,如何让所学能够切实地应用起来,本课程主讲以自己20年来对Linux内核的研究和教学为基础, ...

最新文章

  1. LCD: 2D-3D匹配算法
  2. Openstack安装部署
  3. AI人才报告 | AI稳超互联网平均薪资,哪些细分领域最受追捧?
  4. 开源软件:信息共赢和开放心态
  5. 流程 - 什么是真正的Scrum?
  6. php错误提示:date_default_timezone_get
  7. 封装案例-创建士兵类-完成初始化方法
  8. 清洗弹幕数据,去不相关的列和空值,MapReduce
  9. Java入门算法(滑动窗口篇)丨蓄力计划
  10. Java源文件声明规则
  11. 从一个字符串中删除另一个字符串中出现过的字符
  12. MacOS svn:E230001 Can‘t use Subversion command line client: svn The path to the Subversion executabl
  13. python__实参前加*和**的(拆包)功能
  14. 【SpringBoot 2】(四)详析SpringBoot的常用注解
  15. leetcode - 121.买卖股票的最佳时机
  16. oracle定时关闭job,Oracle job定时操作
  17. 分割、检测与定位,高分辨率网络显神威!这会是席卷深度学习的通用结构吗?...
  18. Torch7框架学习资料整理
  19. 获取单个数据库的邮箱数量
  20. 电量分析 —— 优化耗电

热门文章

  1. 【转】请求处理机制其二:Django中间件的解析
  2. 7.28-说说对javaweb的感想吧
  3. linux-du命令详解
  4. 不要迷失在技术的海洋中(转)
  5. Java之美[从菜鸟到高手演变]之设计模式四
  6. MongoDB实战-面向文档的数据(找到最合适的数据建模方式)
  7. IE6不支持position:fixed解决方法
  8. 十一月份英语学习总结—积累
  9. Linux终端的总结和shell
  10. PHP程序员40点陋习