linux学习笔记:netlink实践演练

内核和用户空间之间存在如下交互手段:

1.内核启动参数 2.模块参数与 3.sysfs、4.sysctl、5.系统调用、6.netlink、7.procfs、8.seq_file、9.debugfs 10.relayfs

另外 call_usermodehelper 可以从内核发起用户态的应用程序运行

  其中netlink作为一种进程之间的通讯手段 ,和其他内核与用户空间的通讯手段比较,有很大的优势:

 1.  新增通讯通道的灵活性:

     netlink提供了一套完善的机制,用户既可以静态的增加通讯通道,也可以动态的创建通讯通道,这样用户可以很灵活的根据自己的需要来定制和开发。

  2. 丰富的特性支持:

     netlink可以支持异步双工, 其他机制只支持用户到内核单向的通道,或者只支持内核到用户的单向通道,netlink支持对称的双向通道。

      同时neilink支持单点传输和多点传输,这些优势都是其他通讯机制所不具备的。

 3. 传输效率比较高:  

     和其他用户向内核通讯手段一样,netlink也是借助某些系统调用接口实现的,并且netlink的目标数据接收操作是直接在软中断里面执行的,比有些在内核开辟线程接收数据的方式要快。

 4. 易于扩展:

     内核已为netlink提供的动态机制扩展,新增一个应用通道非常方便,只需要修改少量代码。

      netlink充分体现了linux开发的宗旨:“提供机制而不是策略”,“do one thing and do it well”, 从内核版本的演进历程看来,同一类型的机制,linux提供的功能越来越强大,给用户的选择空间也是越来越丰富。

从学习的角度出发,这里使用静态方式新增了一个netlink通道,并实现了一个用户态和内核态通讯的双向通讯的样例,设计如下:

       user                                                    kernel

          |                                                                |

        send    -> "hello from usr"->  receive and print 

           |                                                               |

 receive and print  < -"hello from usr " <-send

           |                                                               |

         exit

 如上图所述,由用户首先发起一个问候消息给内核,内核收到这个消息以后返回一个问候消息给用户, 以下通过代码来分析netlink的实现:

一、内核部分代码:

1. 头文件和静态申明

这里包含了必要的头文件, 新增了一个 netlink协议号,这个协议号和内核自定义的NETLINK_GENERIC是同一类型,应该定义在<linux/netlink.h>中,为了方便显示放到了这里。

新增内核源码文件 eknetlink.c :

/*kernel example code of netlink*/ #include <linux/netlink.h> #include <linux/module.h> #include <linux/skbuff.h> #include <linux/init.h> #include <linux/netdevice.h> #include <linux/netfilter.h> #include <linux/spinlock.h> #include <linux/netlink.h> #include <net/sock.h> #include <net/flow.h>  #define NETLINK_EXAMPLE	 31 /*新增netlink协议号*/  /*本netlink过滤类型*/ enum nf_eknetlink_hooks { NF_EKNETLINK_IN,  NF_EKNETLINK_OUT, NF_EKNETLINK_NUMHOOKS };  #define TFR printk /*trace function routine*/  static struct sock *eknl = NULL; /*模块内部全局的netlink套接字*/  static DEFINE_MUTEX(eknl_mutex); /*多线程互斥, 必需,有可能多个用户进程(如多个shell窗口)同时向本模块发消息*/  static void eknl_lock(void) { mutex_lock(&eknl_mutex); } static void eknl_unlock(void) { mutex_unlock(&eknl_mutex); } 

 

2. 内核netlink初始化

   1) .netlink_kernel_create 为 NETLINK_EXAMPLE协议创建了一个套接字。

   2) &init_net 使用当前的网络命名空间。

   3) eknetlink_rcv 使用这个处理用户发送过来的soket数据。

   4) nf_register_hook 注册一个数据包过滤函数。 

static int __init eknetlink_init(void) { int rv = 0;  printk("example kernel netlink_init.\n");  eknl = netlink_kernel_create(&init_net, NETLINK_EXAMPLE, 0, eknetlink_rcv, NULL, THIS_MODULE); if (!eknl) { printk(KERN_ERR "cannot initialize nfnetlink!\n"); return -1; }  rv = nf_register_hook(&eknl_ops); if (rv) { netlink_kernel_release(eknl); }  return rv; }  static void __exit eknetlink_exit(void) { printk("Removing example kernel NETLINK layer.\n"); netlink_kernel_release(eknl); return; }  module_init(eknetlink_init); module_exit(eknetlink_exit); MODULE_AUTHOR("monan"); MODULE_LICENSE("GPL");


3. 内核netlink消息接收函数

/* netlink是基于socket套接字实现的通讯,所以netlink接收到的原始数据是socket数据类型,由struct sk_buff定义,函数中的netlink_rcv_sk()会通过nlh = nlmsg_hdr(skb);解析sk_buff,获取socket数据中的netlink的数据(由struct nlmsghdr定义),并交给eknetlink_rcv_msg,让我们自行处理netlink消息 */ static void eknetlink_rcv(struct sk_buff *skb) { TFR("eknetlink_rcv enter.skb(0x%x)\n", (unsigned int) skb);  eknl_lock(); netlink_rcv_skb(skb, &eknetlink_rcv_msg); eknl_unlock();  TFR("eknetlink_rcv exit.\n"); }

/*这里开始解析netlink数据:struct nlmsghdr *nlh */

static int eknetlink_rcv_msg(struct sk_buff *skb, struct nlmsghdr *nlh) { char* str_buf; int ret;  TFR("eknetlink_rcv_msg enter.\n");  printk("nlmsg_len(%d) nlmsg_type(0x%x) nlmsg_flags(0x%x) nlmsg_seq(%d) nlmsg_pid(0x%x)\n", nlh->nlmsg_len, nlh->nlmsg_type, nlh->nlmsg_flags, nlh->nlmsg_seq, nlh->nlmsg_pid);  /*对接收数据进行过滤处理*/ ret = NF_HOOK(AF_NETLINK, NF_EKNETLINK_IN, skb, NULL, NULL, eknetlink_okfn); if (NF_ACCEPT != ret) { printk("out NF_HOOK not accept (%d)!\n", ret); return -1; }  /* netlink数据中的用户数据(这里即用户控件发来的"hello kernel"字符串),是netlink消息数据中消息头(struct nlmsghdr)后面紧接着的部分, 长度由struct nlmsghdr.nlmsg_len描述,如果只有消息头没有用户数据,则struct nlmsghdr.nlmsg_len 为0。 */ if (nlh->nlmsg_len > 0) { str_buf = kmalloc(nlh->nlmsg_len, GFP_KERNEL);  memcpy(str_buf, NLMSG_DATA(nlh), nlh->nlmsg_len); str_buf[nlh->nlmsg_len - 1] = '\0'; printk("Message received(%d):%s\n", nlh->nlmsg_pid, str_buf) ; kfree(str_buf);  /*nlh->nlmsg_pid是发送者的用户进程ID,传递,用于描述内核返回消息时的发送对象*/ if(!eknetlink_sendmsg("From kernel: hello user!", nlh->nlmsg_pid)){ printk("eknetlink_rcv_msg send fail. \n"); } }  TFR("eknetlink_rcv_msg exit.\n");  return 0; } 

4. 内核netlink消息发送函数:

/* message: 要发送的消息字符串 pid :发送的目标,用户进程ID */ int eknetlink_sendmsg(char *message, int pid) { struct sk_buff *skb; struct nlmsghdr *nlh; int msize = 0; int ret;  if(!message || !eknl) { return -1; }  msize = strlen(message) + 1;   /* nlmsg_new 会新申请一个socket buffer ,其大小为 socket消息头大小+ netlink 消息头大小+ 用户消息大小*/  skb = nlmsg_new(msize, GFP_KERNEL); if(!skb) { printk(KERN_ERR "eknetlink_sendnlmsg:alloc_skb_1 error\n"); return -1; }  nlh = nlmsg_put(skb,0,0,0,msize,0); /*填充部分netlink消息头*/  NETLINK_CB(skb).pid = 0; /*描述发送者ID,这里发送者是内核,填0*/ NETLINK_CB(skb).dst_group = 0;  memcpy(NLMSG_DATA(nlh), message, msize);/*填充用户区数据*/ printk("Message send '%s'.\n",(char *)NLMSG_DATA(nlh));  ret = NF_HOOK(AF_NETLINK, NF_EKNETLINK_OUT, skb, NULL, NULL, eknetlink_okfn); if (NF_ACCEPT != ret) { printk("out NF_HOOK not accept (%d)!\n", ret); return -1; } /*单播消息,目标用户态pid*/ /*需要特别注意的是: skb申请的空间会在这里面释放, 所以不能重复调用此接口发送同一个skb,会造成严重后果*/ return netlink_unicast(eknl, skb, pid, MSG_DONTWAIT); } 

 

5. 内核netlink消息过滤:

netfilter 提供了一种过滤机制,上文在eknetlink_init  中 调用nf_register_hook注册了一个过滤钩子,即可以对收发消息进行截取或者过滤。

这个机制不是netlink独有的,是socket网络代码的一部分,但是也可以修改后被netlink借过来使用。

需要在本模块的消息接收和发送接口中主动调用NF_HOOK,这个钩子才能被执行。

查看宏 NF_HOOK 的定义可知:只有当eknl_hook()和eknetlink_okfn()都返回NF_ACCEPT,此消息包才会判定为允许接收。

static unsigned int eknl_hook(unsigned int hook, struct sk_buff *skb, const struct net_device *in, const struct net_device *out, int (*okfn)(struct sk_buff *)) {  TFR("eknl_hook .\n"); return NF_ACCEPT; /*钩子回调,返回NF_ACCEPT表示数据允许接收*/ }
int eknetlink_okfn (struct sk_buff * skb) {     TFR("eknetlink_okfn .skb(0x%x)\n", (unsigned int)skb);     return NF_ACCEPT; /*钩子回调的回调,返回NF_ACCEPT表示数据允许接收,*/ } 
static struct nf_hook_ops eknl_ops __read_mostly = { .hook = eknl_hook, .pf = AF_NETLINK, /*设定这个值的同时需要修改NFPROTO_NUMPROTO, 否则nf_register_hook访问nf_hooks会溢出,系统一直起不来。*/ .hooknum = NF_EKNETLINK_IN, .priority = 0, .owner = THIS_MODULE, }; 

修改NFPROTO_NUMPROTO,在文件netfilter.h中:

enum { NFPROTO_UNSPEC = 0, NFPROTO_IPV4 = 2, NFPROTO_ARP = 3, NFPROTO_BRIDGE = 7, NFPROTO_IPV6 = 10, NFPROTO_DECNET = 12, NFPROTO_NETLINK = 16 , /*add for netlink study*/ NFPROTO_NUMPROTO, }; 

 6.  新增编译脚本并编译内核:

Kconfig

config EKNETLINK tristate "Netlink study example." default n help Netlink example for study, this is kernel part , cooperation with net link usr example part.  

Makefile

obj-$(CONFIG_EKNETLINK) += eknetlink.o

make menuconfig 打开eknetlink模块编译

make -j4 编译内核

至此内核的netlink新增通道接收和发送流程描述完成,接下来记录用户态的netlink新增通道实现。

 

二、用户部分代码:

在用户态新增源码文件

external/netlink/eunetlink.c

1) 头文件和静态申明

<p style="margin-top: 0px; margin-bottom: 10px; padding-top: 0px; padding-bottom: 0px; text-indent: 28px;">/*user example code for netlink study*/ #include <asm/types.h> #include <sys/socket.h> #include <unistd.h> #include <err.h> #include <stdio.h> #include <netinet/in.h> #include <linux/netlink.h> #include <linux/rtnetlink.h></p><p style="margin-top: 0px; margin-bottom: 10px; padding-top: 0px; padding-bottom: 0px; text-indent: 28px;">enum {     EUNETLINK_MSG_OPEN =  NLMSG_MIN_TYPE, /*比NLMSG_MIN_TYPE 小的类型是系统保留的,不能使用,                                       否则消息在内核回调中会被netlink_rcv_skb过滤,eknetlink_rcv_msg得不到调用*/          EUNETLINK_MSG_CLOSE }EUNETLINK_MSG;</p><p style="margin-top: 0px; margin-bottom: 10px; padding-top: 0px; padding-bottom: 0px; text-indent: 28px;">#define NETLINK_EXAMPLE  31  /*新增netlink协议类型,和内核态的定义保持一致*/ </p>

2) 用户态主函数

int main(int argc, char *argv[]) {  int nlsk = eunetlink_open(); /*打开一个netlink 套接字*/  int ret; if (nlsk<0) {  err(1,"netlink");  return -1; }  printf("eunetlink open socket = 0x%x\n", nlsk);   ret = eunetlink_send(nlsk); /*向内核发送"hello"消息*/  printf("eunetlink send ret = 0x%x\n", ret);   eunetlink_recv(nlsk); /*等待接收内核返回的消息*/   close(nlsk); /*关闭netlink 套接字*/  return 0; }

 

3)用户态消息发送接口

int eunetlink_send(int nlsk) { char buffer[] = "From user : hello kernel!"; struct nlmsghdr* nlhdr; struct iovec iov; /* 用于把多个消息通过一次系统调用来发送*/ struct msghdr msg;  struct sockaddr_nl nladdr;  /*必须,消息头数据清零,否则会包含错误的数据,发送失败*/ memset(&msg, 0 ,sizeof(struct msghdr)); memset(&nladdr, 0 ,sizeof(struct sockaddr_nl));  nlhdr = (struct nlmsghdr *)malloc(NLMSG_SPACE(strlen(buffer) + 1));  strcpy(NLMSG_DATA(nlhdr),buffer);   /*填充待发送的消息体*/ nlhdr->nlmsg_len = NLMSG_LENGTH(strlen(buffer) + 1);  nlhdr->nlmsg_pid = getpid(); /*发送者进程ID,内核代码会根据这个ID返回消息*/ nlhdr->nlmsg_flags = NLM_F_REQUEST; /*必须,不设置的话,内核中netlink_rcv_skb() 会过滤此消息,不会调用户回调*/ nlhdr->nlmsg_type = EUNETLINK_MSG_OPEN; /*必须,不设置的话,内核中netlink_rcv_skb() 会过滤此消息,不会调用户回调*/  /*填充目标地址结构体*/ nladdr.nl_family = AF_NETLINK; nladdr.nl_pid = 0; /*目标地址是内核,所以这里需要填0*/ nladdr.nl_groups = 0;  printf("Message Send :%s\n",buffer);   #if 0 /*使用struct iovec iov[]数组 和 sendmsg可以实现一次调用发送多个消息请求*/ iov.iov_base = (void *)nlhdr;  iov.iov_len = nlhdr->nlmsg_len;  msg.msg_iov = &iov; msg.msg_iovlen = 1; msg.msg_name = (void *)&(nladdr); msg.msg_namelen = sizeof(nladdr);  return sendmsg(nlsk, &msg, 0);  #else /*发送单个个请求*/ return sendto(nlsk, nlhdr, nlhdr->nlmsg_len, 0, (struct sockaddr*)&nladdr, sizeof(nladdr)); #endif  }

4)用户态消息接收

int eunetlink_recv(int sock) {  struct sockaddr_nl nladdr;  struct msghdr msg;  struct iovec iov[1];  struct nlmsghdr * nlh;  char buffer[65536]; /*临时buffer,用于接收内核发过来的数据*/  int len;   /*填充待接收消息结构体*/  msg.msg_name = (void *)&(nladdr); /*设定接收的发送源地址*/  msg.msg_namelen = sizeof(nladdr);   /*设定临时缓冲*/ iov[0].iov_base = (void *)buffer;  iov[0].iov_len = sizeof(buffer); msg.msg_iov = iov;  msg.msg_iovlen = sizeof(iov)/sizeof(iov[0]); /*允许多个临时缓冲*/  len = recvmsg(sock, &msg, 0); /*阻塞等待接收数(除非特别设定,socket都是阻塞式接收)*/ if (len < 0) {  printf("recvmsg error: %d", len);  return len;  }   /*遍历接收到的消息数据,进行处理 NLMSG_OK会判断数据是不是已经结束 NLMSG_NEXT 会修改nlh和len的值,使之指向buffer中的下一个netlink消息 */  for (nlh = (struct nlmsghdr *)buffer; NLMSG_OK(nlh, len); nlh = NLMSG_NEXT(nlh, len)) {  if (nlh->nlmsg_type == NLMSG_ERROR) {  // Do some error handling.  puts("eunetlink_recv: NLMSG_ERROR");  return 0;  }   /*处理有效的用户数据*/ if (nlh->nlmsg_len > 0) { char *str_buf = malloc(nlh->nlmsg_len);  memcpy(str_buf, NLMSG_DATA(nlh), nlh->nlmsg_len);  str_buf[nlh->nlmsg_len - 1] = '\0'; printf("Message received:%s\n",str_buf) ;  free(str_buf); } }  return 0; }

5)新增编译脚本并编译用户应用程序
新增external/netlink/Android.mk

LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE_TAGS := optional LOCAL_MODULE := eunetlink LOCAL_SRC_FILES := $(call all-subdir-c-files) include $(BUILD_EXECUTABLE)

 编译用户程序:

mmm external/netlink/


打包system.img:

make snod

 

三、运行和调试

   1)。运行android的goldfish模拟器,使用新编译出来的内核:

  emulator -kernel (YOUR_KERNEL_PATH)/arch/arm/boot/zImage -shell

 运行用户测试程序:

eunetlink

正确执行会显示如下信息:

linux学习

本篇代码的内核态打印:

演练

2)调试用户态

 运行: strace eunetlink

   会显示所有用户态的系统调用路径和返回值,并且会把入参展开显示,这个对调试很有用:

   这里截取其中本文代码相关的部分:

笔记

 

四、总结

       netlink提供了一种很好很强大的的用户与内核之间的通讯机制,本文通过静态的新增一个netlink协议类型,并使用这个新的netlink类型实现用户态和内核态的双向通讯,对linux的netlink通讯方式有了一个初步的认识。

       动态申请 netlink通道机制在genetlink.c中实现,是对NETLINK_GENERIC协议的上层封装, 可以参考内核代码net\tipc\netlink.c中的对他的应用,这个文件实现比较简单,可以作为很好的样例。

说明:本文所含代码的运行环境为 android提供的 goldfish 模拟器平台, Linux 内核版本为 2.6.29

参考文档:

android-goldfish-2.6.29 code

《Linux内核设计与实现》(Linux Kernel Development)第3版 》

Linux 用户态与内核态的交互——netlink 篇:http://bbs.chinaunix.net/thread-2162796-1-1.html

《Linux 系统内核空间与用户空间通信的实现与分析》   陈鑫  http://www-128.ibm.com/developerworks/cn/linux/l-netlink/?ca=dwcn-newsletter-linux
《在 Linux 下用户空间与内核空间数据交换的方式》      杨燚  http://www-128.ibm.com/developerworks/cn/linux/l-kerns-usrs/

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值