Linux 系统内核空间与用户空间通信的实现与分析
本文转载自:https://www.ibm.com/developerworks/cn/linux/l-netlink/index.html
多数的 Linux 内核态程序都需要和用户空间的进程交换数据,但 Linux 内核态无法对传统的 Linux 进程间同步和通信的方法提供足够的支持。本文总结并比较了几种内核态与用户态进程通信的实现方法,并推荐使用 netlink 套接字实现中断环境与用户态进程通信。
1 引言
Linux 是一个源码开放的操作系统,无论是普通用户还是企业用户都可以编写自己的内核代码,再加上对标准内核的裁剪从而制作出适合自己的操作系统。目前有很多中低端用户使用的网络设备的操作系统是从标准 Linux 改进而来的,这也说明了有越来越多的人正在加入到 Linux 内核开发团体中。
一个或多个内核模块的实现并不能满足一般 Linux 系统软件的需要,因为内核的局限性太大,如不能在终端上打印,不能做大延时的处理等等。当我们需要做这些的时候,就需要将在内核态采集到的数据传送到用户态的一个或多个进程中进行处理。这样,内核态与用户空间进程通信的方法就显得尤为重要。在 Linux 的内核发行版本中没有对该类通信方法的详细介绍,也没有其他文章对此进行总结,所以本文将列举几种内核态与用户态进程通信的方法并详细分析它们的实现和适用环境。
2 Linux 内核模块的运行环境与传统进程间通信
在一台运行 Linux 的计算机中,CPU 在任何时候只会有如下四种状态:
【1】 在处理一个硬中断。
【2】 在处理一个软中断,如 softirq、tasklet 和 bh。
【3】 运行于内核态,但有进程上下文,即与一个进程相关。
【4】 运行一个用户态进程。
其中,【1】、【2】和【3】是运行于内核空间的,而【4】是在用户空间。其中除了【4】,其他状态只可以被在其之上的状态抢占。比如,软中断只可以被硬中断抢占。
Linux 内核模块是一段可以动态在内核装载和卸载的代码,装载进内核的代码便立即在内核中工作起来。Linux 内核代码的运行环境有三种:用户上下文环境、硬中断环境和软中断环境。但三种环境的局限性分两种,因为软中断环境只是硬中断环境的延续。比较如表【1】。
表【1】
内核态环境 | 介绍 | 局限性 |
---|---|---|
用户上下文 | 内核态代码的运行与一用户空间进程相关,如系统调用中代码的运行环境。 | 不可直接将本地变量传递给用户态的内存区,因为内核态和用户态的内存映射机制不同。 |
硬中断和软中断环境 | 硬中断或软中断过程中代码的运行环境,如 IP 数据报的接收代码的运行环境,网络设备的驱动程序等。 | 不可直接向用户态内存区传递数据; 代码在运行过程中不可阻塞。 |
Linux 传统的进程间通信有很多,如各类管道、消息队列、内存共享、信号量等等。但它们都无法介于内核态与用户态使用,原因如表【2】。
表【2】
通信方法 | 无法介于内核态与用户态的原因 |
---|---|
管道(不包括命名管道) | 局限于父子进程间的通信。 |
消息队列 | 在硬、软中断中无法无阻塞地接收数据。 |
信号量 | 无法介于内核态和用户态使用。 |
内存共享 | 需要信号量辅助,而信号量又无法使用。 |
套接字 | 在硬、软中断中无法无阻塞地接收数据。 |
3 Linux内核态与用户态进程通信方法的提出与实现
3.1 用户上下文环境
运行在用户上下文环境中的代码是可以阻塞的,这样,便可以使用消息队列和 UNIX 域套接字来实现内核态与用户态的通信。但这些方法的数据传输效率较低,Linux 内核提供 copy_from_user()/copy_to_user() 函数来实现内核态与用户态数据的拷贝,但这两个函数会引发阻塞,所以不能用在硬、软中断中。一般将这两个特殊拷贝函数用在类似于系统调用一类的函数中,此类函数在使用中往往"穿梭"于内核态与用户态。此类方法的工作原理路如图【1】。
图【1】
其中相关的系统调用是需要用户自行编写并载入内核。 imp1.tar.gz是一个示例,内核模块注册了一组设置套接字选项的函数使得用户空间进程可以调用此组函数对内核态数据进行读写。源码包含三个文件,imp1.h 是通用头文件,定义了用户态和内核态都要用到的宏。imp1_k.c 是内核模块的源代码。imp1_u.c 是用户态进程的源代码。整个示例演示了由一个用户态进程向用户上下文环境发送一个字符串,内容为"a message from userspace\n"。然后再由用户上下文环境向用户态进程发送一个字符串,内容为"a message from kernel\n"。
3.2 硬、软中断环境
比起用户上下文环境,硬中断和软中断环境与用户态进程无丝毫关系,而且运行过程不能阻塞。
3.2.1 使用一般进程间通信的方法
我们无法直接使用传统的进程间通信的方法实现。但硬、软中断中也有一套同步机制--自旋锁(spinlock),可以通过自旋锁来实现中断环境与中断环境,中断环境与内核线程的同步,而内核线程是运行在有进程上下文环境中的,这样便可以在内核线程中使用套接字或消息队列来取得用户空间的数据,然后再将数据通过临界区传递给中断过程。基本思路如图【2】。
图【2】
因为中断过程不可能无休止地等待用户态进程发送数据,所以要通过一个内核线程来接收用户空间的数据,再通过临界区传给中断过程。中断过程向用户空间的数据发送必须是无阻塞的。这样的通信模型并不令人满意,因为内核线程是和其他用户态进程竞争CPU接收数据的,效率很低,这样中断过程便不能实时地接收来自用户空间的数据。
3.2.2 netlink 套接字
在 Linux 2.4 版以后版本的内核中,几乎全部的中断过程与用户态进程的通信都是使用 netlink 套接字实现的,同时还使用 netlink 实现了 ip queue 工具,但 ip queue 的使用有其局限性,不能自由地用于各种中断过程。内核的帮助文档和其他一些 Linux 相关文章都没有对 netlink 套接字在中断过程和用户空间通信的应用上作详细的说明,使得很多用户对此只有一个模糊的概念。
netlink 套接字的通信依据是一个对应于进程的标识,一般定为该进程的 ID。当通信的一端处于中断过程时,该标识为 0。当使用 netlink 套接字进行通信,通信的双方都是用户态进程,则使用方法类似于消息队列。但通信双方有一端是中断过程,使用方法则不同。netlink 套接字的最大特点是对中断过程的支持,它在内核空间接收用户空间数据时不再需要用户自行启动一个内核线程,而是通过另一个软中断调用用户事先指定的接收函数。工作原理如图【3】。
图【3】
很明显,这里使用了软中断而不是内核线程来接收数据,这样就可以保证数据接收的实时性。
当 netlink 套接字用于内核空间与用户空间的通信时,在用户空间的创建方法和一般套接字使用类似,但内核空间的创建方法则不同。图【4】是 netlink 套接字实现此类通信时创建的过程。
图【4】
以下举一个 netlink 套接字的应用示例。示例实现了从 netfilter 的 NF_IP_PRE_ROUTING 点截获的 ICMP 数据报,在将数据报的相关信息传递到一个用户态进程,由用户态进程将信息打印在终端上。源码在文件 imp2.tar.gz中。 内核模块代码(分段详解):
(一)模块初始化与卸载
static struct sock *nlfd;
struct
{
__u32 pid;
rwlock_t lock;
}user_proc;
/*挂接在 netfilter 框架的 NF_IP_PRE_ROUTING 点上的函数为 get_icmp()*/
static struct nf_hook_ops imp2_ops =
{
.hook = get_icmp, /*netfilter 钩子函数*/
.pf = PF_INET,
.hooknum = NF_IP_PRE_ROUTING,
.priority = NF_IP_PRI_FILTER -1,
};
static int __init init(void)
{
rwlock_init(&user_proc.lock);
/*在内核创建一个 netlink socket,并注明由 kernel_recieve() 函数接收数据
这里协议 NL_IMP2 是自定的*/
nlfd = netlink_kernel_create(NL_IMP2, kernel_receive);
if(!nlfd)
{
printk("can not create a netlink socket\n");
return -1;
}
/*向 netfilter 的 NF_IP_PRE_ROUTING 点挂接函数*/
return nf_register_hook(&imp2_ops);
}
static void __exit fini(void)
{
if(nlfd)
{
sock_release(nlfd->socket);
}
nf_unregister_hook(&imp2_ops);
}
module_init(init);
module_exit(fini);
其实片断(一)的工作很简单,模块加载阶段先在内核空间创建一个 netlink 套接字,再将一个函数挂接在 netfilter 框架的 NF_IP_PRE_ROUTING 钩子点上。卸载时释放套接字所占的资源并注销之前在 netfilter 上挂接的函数。
(二)接收用户空间的数据
DECLARE_MUTEX(receive_sem);
01: static void kernel_receive(struct sock *sk, int len)
02: {
03: do
04: {
05: struct sk_buff *skb;
06: if(down_trylock(&receive_sem))
07: return;
08:
09: while((skb = skb_dequeue(&sk-<receive_queue)) != NULL)
10: {
11: {
12: struct nlmsghdr *nlh = NULL;
13: if(skb-<len <= sizeof(struct nlmsghdr))
14: {
15: nlh = (struct nlmsghdr *)skb-<data;
16: if((nlh-<nlmsg_len <= sizeof(struct nlmsghdr))
17: && (skb-<len <= nlh-<nlmsg_len))
18: {
19: if(nlh-<nlmsg_type == IMP2_U_PID)
20: {
21: write_lock_bh(&user_proc.pid);
22: user_proc.pid = nlh-<nlmsg_pid;
23: write_unlock_bh(&user_proc.pid);
24: }
25: else if(nlh-<nlmsg_type == IMP2_CLOSE)
26: {
27: write_lock_bh(&user_proc.pid);
28: if(nlh-<nlmsg_pid == user_proc.pid) user_proc.pid = 0;
29: write_unlock_bh(&user_proc.pid);
30: }
31: }
32: }
33: }
34: kfree_skb(skb);
35: }
36: up(&receive_sem);
37: }while(nlfd && nlfd-<receive_queue.qlen);
38: }
如果读者看过 ip_queue.c 或 rtnetlink.c中的源码会发现片断(二)中的 03~18 和 31~38 是 netlink socket 在内核空间接收数据的框架。在框架中主要是从套接字缓存中取出全部的数据,然后分析是不是合法的数据报,合法的 netlink 数据报必须有nlmsghdr 结构的报头。在这里笔者使用了自己定义的消息类型:IMP2_U_PID(消息为用户空间进程的ID),IMP2_CLOSE(用户空间进程关闭)。因为考虑到 SMP,所以在这里使用了读写锁来避免不同 CPU 访问临界区的问题。kernel_receive() 函数的运行在软中断环境。
(三)截获 IP 数据报
static unsigned int get_icmp(unsigned int hook,
struct sk_buff **pskb,
const struct net_device *in,
const struct net_device *out,
int (*okfn)(struct sk_buff *))
{
struct iphdr *iph = (*pskb)->nh.iph;
struct packet_info info;
if(iph->protocol == IPPROTO_ICMP) /*若传输层协议为 ICMP*/
{
read_lock_bh(&user_proc.lock);
if(user_proc.pid != 0)
{
read_unlock_bh(&user_proc.lock);
info.src = iph->saddr; /*记录源地址*/
info.dest = iph->daddr; /*记录目的地址*/
send_to_user(&info); /*发送数据*/
}
else
read_unlock_bh(&user_proc.lock);
}
return NF_ACCEPT;
}
(四)发送数据
static int send_to_user(struct packet_info *info)
{
int ret;
int size;
unsigned char *old_tail;
struct sk_buff *skb;
struct nlmsghdr *nlh;
struct packet_info *packet;
size = NLMSG_SPACE(sizeof(*info));
/*开辟一个新的套接字缓存*/
skb = alloc_skb(size, GFP_ATOMIC);
old_tail = skb->tail;
/*填写数据报相关信息*/
nlh = NLMSG_PUT(skb, 0, 0, IMP2_K_MSG, size-sizeof(*nlh));
packet = NLMSG_DATA(nlh);
memset(packet, 0, sizeof(struct packet_info));
/*传输到用户空间的数据*/
packet->src = info->src;
packet->dest = info->dest;
/*计算经过字节对其后的数据实际长度*/
nlh->nlmsg_len = skb->tail - old_tail;
NETLINK_CB(skb).dst_groups = 0;
read_lock_bh(&user_proc.lock);
ret = netlink_unicast(nlfd, skb, user_proc.pid, MSG_DONTWAIT); /*发送数据*/
read_unlock_bh(&user_proc.lock);
return ret;
nlmsg_failure: /*若发送失败,则撤销套接字缓存*/
if(skb)
kfree_skb(skb);
return -1;
}
片断(四)中所使用的宏参考如下:
/*字节对齐*/
#define NLMSG_ALIGN(len) ( ((len)+NLMSG_ALIGNTO-1) & ~(NLMSG_ALIGNTO-1) )
/*计算包含报头的数据报长度*/
#define NLMSG_LENGTH(len) ((len)+NLMSG_ALIGN(sizeof(struct nlmsghdr)))
/*字节对齐后的数据报长度*/
#define NLMSG_SPACE(len) NLMSG_ALIGN(NLMSG_LENGTH(len))
/*填写相关报头信息,这里使用了nlmsg_failure标签,所以在程序中要定义*/
#define NLMSG_PUT(skb, pid, seq, type, len) \
({ if (skb_tailroom(skb) < (int)NLMSG_SPACE(len)) goto nlmsg_failure; \
__nlmsg_put(skb, pid, seq, type, len); })
static __inline__ struct nlmsghdr *
__nlmsg_put(struct sk_buff *skb, u32 pid, u32 seq, int type, int len)
{
struct nlmsghdr *nlh;
int size = NLMSG_LENGTH(len);
nlh = (struct nlmsghdr*)skb_put(skb, NLMSG_ALIGN(size));
nlh->nlmsg_type = type;
nlh->nlmsg_len = size;
nlh->nlmsg_flags = 0;
nlh->nlmsg_pid = pid;
nlh->nlmsg_seq = seq;
return nlh;
}
/*跳过报头取实际数据*/
#define NLMSG_DATA(nlh) ((void*)(((char*)nlh) + NLMSG_LENGTH(0)))
/*取 netlink 控制字段*/
#define NETLINK_CB(skb) (*(struct netlink_skb_parms*)&((skb)->cb))
运行示例时,先编译 imp2_k.c 模块,然后使用 insmod 将模块加载入内核。再运行编译好的 imp2_u 命令,此时就会显示出本机当前接收的 ICMP 数据报的源地址和目的地址。用户可以使用 Ctrl+C 来终止用户空间的进程,再次启动也不会带来问题。
4 总结
本文从内核态代码的不同运行环境来实现不同方法的内核空间与用户空间的通信,并分析了它们的实际效果。最后推荐使用 netlink 套接字实现中断环境与用户态进程通信,因为 netlink 套接字是专为此类通信定制的。
Linux 系统内核空间与用户空间通信的实现与分析的更多相关文章
- linux 系统内核空间与用户空间通信的实现与分析<转>
linux 系统内核空间与用户空间通信的实现与分析 2 评论: 陈鑫 (chen.shin@hotmail.com), 自由软件爱好者, 南京邮电学院电子工程系 2004 年 7 月 01 日 内容 ...
- Linux 系统内核空间与用户空间通信的实现与分析-NETLINK (转载)
Linux中的进程间通信机制源自于Unix平台上的进程通信机制.Unix的两大分支AT&T Unix和BSD Unix在进程通信实现机制上的各有所不同,前者形成了运行在单个计算机上的Syste ...
- linux 内核与用户空间通信之netlink使用方法
转自:http://blog.csdn.net/haomcu/article/details/7371835 Linux中的进程间通信机制源自于Unix平台上的进程通信机制.Unix的两大分支AT&a ...
- linux 内核与用户空间通信机制netlink初探
1.Linux进程概述 Linux中的进程间通信机制源自于Unix平台上的进程通信机制.Unix的两大分支AT&T Unix和BSD Unix在进程通信实现机制上各有所不同,前者形成了运行 ...
- linux内核空间与用户空间信息交互方法
linux内核空间与用户空间信息交互方法 本文作者: 康华:计算机硕士,主要从事Linux操作系统内核.Linux技术标准.计算机安全.软件测试等领域的研究与开发工作,现就职于信息产业部软件与 ...
- linux(centos)用户与权限
1.用户管理 2.用户组 3.权限分配 一.查看与用户相关文件命令: 1.cat 2.more 3.head /etc/passwd #查看文件的前十行 4.head -2 /etc/passw ...
- 在linux中添加ftp用户,并设置相应的权限
在linux中添加ftp用户,并设置相应的权限,操作步骤如下: 1.环境:ftp为vsftp.被限制用户名为test.被限制路径为/home/test 2.建用户:在root用户下: useradd ...
- Linux下非root用户如何安装软件
Linux下非root用户如何安装软件 从windows转移到Linux的用户最开始总会有各种不适,因为这种不适最终放弃linux的不在少数.对于这类人只能说可惜,还没有领略到linux的美好就过早放 ...
- linux中添加ftp用户,并设置相应的权限
在linux中添加ftp用户,并设置相应的权限,操作步骤如下: 1.环境:ftp为vsftp.被限制用户名为test.被限制路径为/home/test 2.建用户:在root用户下: useradd ...
随机推荐
- 一道超级复杂的js题目
先看以下代码: function Foo(){ getName = function(){ alert(1); }; return this; } Foo.getName = function(){ ...
- php使用命名空间时自动加载机制
命名空间主要为了解决用户编写的代码与PHP内部的类/函数/常量或第三方类/函数/常量之间的名字冲突.不过并不是你定义了使用命名空间的类,就可以在任何地方随意使用了,需要在程序运行时将定义命名空间的类文 ...
- linux学习笔记 磁盘存储之磁盘的基本组成结构
- 树莓派 - 驱动hello
树莓派上Linux驱动,从hello world 开始 ... hello.c #include <linux/init.h> #include <linux/module.h> ...
- PHP 反射API
出处:http://blog.csdn.net/hguisu/article/details/7357421 PHP5添加了一项新的功能:Reflection.这个功能使得phper可以reverse ...
- 一次偶遇 IOException
今天遇到了个挺有意思的事情,为了实现上传头像的功能,这是需要实现的功能,项目在本地跑起来且在本地 服务器(tomcat)也实现了头像上传,并把头像路径存入数据库,但是却在上传到线上服务器的时候死活存不 ...
- sqlserver同一个局域网内,把服务器数据库备份到客户端
1.客户端主机创建网络共享文件夹 2.远程服务器运行: EXEC sp_configure 'show advanced options', 1;-- 允许配置高级选项--配置选项'show adva ...
- Win8系统下MT4不能添加指标无法找到技术指标
首次使用MT4平台时系统会将MT4所有文件都加载到了Windows用户目录下,导致在MT4安装位置添加了技术指标后,MT4平台也无法找到技术指标,关于这个问题的解决方法如下 MT4是一款享誉盛名的市场 ...
- UVa 1600 Patrol Robot(BFS)
题意: 给定一个n*m的图, 有一个机器人需要从左上角(1,1)到右下角(n,m), 网格中一些格子是空地, 一些格子是障碍, 机器人每次能走4个方向, 但不能连续穿越k(0<= k <= ...
- 使用idea搭建ssh项目
参考: https://www.cnblogs.com/getchen/p/8036709.html 需要自己提前在数据库中建好表 然后连接数据库通过侧边栏的persistence来生成实体类和相应的 ...