内核和用户空间之间存在如下交互手段:
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是同一类型,应该定义在
新增内核源码文件 eknetlink.c :
/*kernel example code of netlink*/
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#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
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/