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) 头文件和静态申明
/*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>
enum { EUNETLINK_MSG_OPEN = NLMSG_MIN_TYPE, /*比NLMSG_MIN_TYPE 小的类型是系统保留的,不能使用, 否则消息在内核回调中会被netlink_rcv_skb过滤,eknetlink_rcv_msg得不到调用*/ EUNETLINK_MSG_CLOSE }EUNETLINK_MSG;
#define NETLINK_EXAMPLE 31 /*新增netlink协议类型,和内核态的定义保持一致*/
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
正确执行会显示如下信息:
本篇代码的内核态打印:
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/