来自网址http://www.kerneltravel.net/jiaoliu/005.htm linux
用户程序和内核的信息交换是双向的,也就是说既能够主动从用户空间向内核空间发送信息,也能够从内核空间向用户空间提交数据。固然,用户程序也能够主动地从内核提取数据。下面咱们就针对内核和用户交互数据的方法作一总结、概括。编程
信息交互按信息传输发起方能够分为用户向内核传送/提取数据和内核向用户空间提交请求两大类,先来讲说:由用户级程序主动发起的信息交互。安全
系统调用是用户级程序访问内核最基本的方法。目前linux大体提供了二百多个标准的系统调用(参见内核代码树中的include/ asm-i386/unistd.h和arch/i386/kernel/entry.S文件),而且容许咱们添加本身的系统调用来实现和内核的信息交换。好比咱们但愿创建一个系统调用日志系统,将全部的系统调用动做记录下来,以便进行入侵检测。此时,咱们能够编写一个内核服务程序。该程序负责收集全部的系统调用请求,并将这些调用信息记录到在内核中自建的缓冲里。咱们没法在内核里实现复杂的入侵检测程序,所以必须将该缓冲里的记录提取到用户空间。最直截了当的方法是本身编写一个新系统调用实现这种提取缓冲数据的功能。当内核服务程序和新系统调用都实现后,咱们就能够在用户空间里编写用户程序进行入侵检测任务了,入侵检测程序能够定时、轮训或在须要的时候调用新系统调用从内核提取数据,而后进行入侵检测了。服务器
Linux/UNIX的一个特色就是把全部的东西都看做是文件(every thing is a file)。系统定义了简洁完善的驱动程序界面,客户程序能够用统一的方法透过这个界面和内核驱动程序交互。而大部分系统的使用者和开发者已经很是熟悉这种界面以及相应的开发流程了。网络
驱动程序运行于内核空间,用户空间的应用程序经过文件系统中/dev/目录下的一个文件来和它交互。这就是咱们熟悉的那个文件操做流程:open() —— read() —— write() —— ioctl() —— close()。(须要注意的是也不是全部的内核驱动程序都是这个界面,网络驱动程序和各类协议栈的使用就不大一致,好比说套接口编程虽然也有open()close()等概念,但它的内核实现以及外部使用方式都和普通驱动程序有很大差别。)异步
设备驱动程序在内核中要作的中断响应、设备管理、数据处理等等各类工做这篇文章不去关心,咱们把注意力集中在它与用户级程序交互这一部分。操做系统为此定义了一种统一的交互界面,就是前面所说的open(), read(), write(), ioctl()和close()等等。每一个驱动程序按照本身的须要作独立实现,把本身提供的功能和服务隐藏在这个统一界面下。客户级程序选择须要的驱动程序或服务(其实就是选择/dev/目录下的文件),按照上述界面和文件操做流程,就能够跟内核中的驱动交互了。其实用面向对象的概念会更容易解释,系统定义了一个抽象的界面(abstract interface),每一个具体的驱动程序都是这个界面的实现(implementation)。函数
因此驱动程序也是用户空间和内核信息交互的重要方式之一。其实ioctl, read, write本质上讲也是经过系统调用去完成的,只是这些调用已被内核进行了标准封装,统必定义。所以用户没必要向填加新系统调用那样必须修改内核代码,从新编译新内核,使用虚拟设备只须要经过模块方法将新的虚拟设备安装到内核中(insmod上)就能方便使用。关于此方面设计细节请查阅参考资料5,编程细节请查阅参考资料6。工具
在linux中,设备大体可分为:字符设备,块设备,和网络接口(字符设备包括那些必须以顺序方式,像字节流同样被访问的设备;如字符终端,串口等。块设备是指那些能够用随机方式,以整块数据为单位来访问的设备,如硬盘等;网络接口,就指一般网卡和协议栈等复杂的网络输入输出服务)。若是将咱们的系统调用日志系统用字符型驱动程序的方式实现,也是一件轻松惬意地工做。咱们能够将内核中收集和记录信息的那一部分编写成一个字符设备驱动程序。虽然没有实际对应的物理设备,但这并没什么问题:Linux的设备驱动程序原本就是一个软件抽象,它能够结合硬件提供服务,也彻底能够做为纯软件提供服务(固然,内存的使用咱们是没法避免的)。在驱动程序中,咱们能够用open来启动服务,用read()返回处理好的记录,用ioctl()设置记录格式等,用close()中止服务,write()没有用到,那么咱们能够不去实现它。而后在/dev/目录下创建一个设备文件对应咱们新加入内核的系统调用日志系统驱动程序。性能
proc是Linux提供的一种特殊的文件系统,推出它的目的就是提供一种便捷的用户和内核间的交互方式。它以文件系统做为使用界面,使应用程序能够以文件操做的方式安全、方便的获取系统当前运行的状态和其它一些内核数据信息。spa
proc文件系统多用于监视、管理和调试系统,咱们使用的不少管理工具如ps,top等,都是利用proc来读取内核信息的。除了读取内核信息,proc文件系统还提供了写入功能。因此咱们也就能够利用它来向内核输入信息。好比,经过修改proc文件系统下的系统参数配置文件(/proc/sys),咱们能够直接在运行时动态更改内核参数;再如,经过下面这条指令:
echo 1 > /proc/sys/net/ip_v4/ip_forward
开启内核中控制IP转发的开关,咱们就可让运行中的Linux系统启用路由功能。相似的,还有许多内核选项能够直接经过proc文件系统进行查询和调整。
除了系统已经提供的文件条目,proc还为咱们留有接口,容许咱们在内核中建立新的条目从而与用户程序共享信息数据。好比,咱们能够为系统调用日志程序(无论是做为驱动程序也好,仍是做为单纯的内核模块也好)在proc文件系统中建立新的文件条目,在此条目中显示系统调用的使用次数,每一个单独系统调用的使用频率等等。咱们也能够增长另外的条目,用于设置日志记录规则,好比说不记录open系统调用的使用状况等。关于proc文件系统得使用细节,请查阅参考资料7。
有些内核开发者认为利用ioctl()系统调用每每会似的系统调用意义不明确,并且难控制。而将信息放入到proc文件系统中会使信息组织混乱,所以也不同意过多使用。他们建议实现一种孤立的虚拟文件系统来代替ioctl()和/proc,由于文件系统接口清楚,并且便于用户空间访问,同时利用虚拟文件系统使得利用脚本执行系统管理任务更家方便、有效。
咱们举例来讲如何经过虚拟文件系统修改内核信息。咱们能够实现一个名为sagafs的虚拟文件系统,其中文件log对应内核存储的系统调用日志。咱们能够经过文件访问特广泛方法得到日志信息:如
# cat /sagafs/log
使用虚拟文件系统——VFS实现信息交互使得系统管理更加方便、清晰。但有些编程者也许会说VFS 的API 接口复杂不容易掌握,不要担忧2.5内核开始就提供了一种叫作libfs的例程序帮助不熟悉文件系统的用户封装了实现VFS的通用操做。有关利用VFS实现交互的方法看参考资料。
Linux经过内存映像机制来提供用户程序对内存直接访问的能力。内存映像的意思是把内核中特定部分的内存空间映射到用户级程序的内存空间去。也就是说,用户空间和内核空间共享一块相同的内存。这样作的直观效果显而易见:内核在这块地址内存储变动的任何数据,用户能够当即发现和使用,根本无须数据拷贝。而在使用系统调用交互信息时,在整个操做过程当中必须有一步数据拷贝的工做——或者是把内核数据拷贝到用户缓冲区,或只是把用户数据拷贝到内核缓冲区——这对于许多数据传输量大、时间要求高的应用,这无疑是致命的一击:许多应用根本就没法忍受数据拷贝所耗费的时间和资源。
咱们曾经为一块高速采样设备开发过驱动程序,该设备要求在20兆采样率下以1KHz的重复频率进行16位实时采样,每毫秒须要采样、DMA和处理的数据量惊人,若是要使用数据拷贝的方法,根本没法达成要求。此时,内存映像成为惟一的选择:咱们在内存中保留了一块空间,将其配置成环形队列供采样设备DMA输出数据。再把这块内存空间映射到在用户空间运行的数据处理程序上,因而,采样设备刚刚获得并传送到主机上的数据,立刻就能够被用户空间的程序处理。
实际上,内存影射方式一般也正是应用在那些内核和用户空间须要快速大量交互数据的状况下,特别是那些对实时性要求较强的应用。X window系统的服务器的虚拟内存区域,就能够被看作是内存映像用法的一个典型例子:X服务器须要对视频内存进行大量的数据交换,相对于lseek/write来讲,将图形显示内存直接影射到用户空间能够显著提升效能。
并非任何类型的应用都适合mmap,好比像串口和鼠标这些基于流数据的字符设备,mmap就没有太大的用武之地。而且,这种共享内存的方式存在很差同步的问题。因为没有专门的同步机制可让用户程序和内核程序共享,因此在读取和写入数据时要有很是谨慎的设计以保证不会产生干绕。
mmap彻底是基于共享内存的观念了,也正由于此,它能提供额外的便利,但也特别难以控制。
即便在内核中,咱们有时也须要执行一些在用户级才提供的操做:如打开某个文件以读取特定数据,执行某个用户程序从而完成某个功能。由于许多数据和功能在用户空间是现有的或者已经被实现了,那么没有必要耗费大量的资源去重复。此外,内核在设计时,为了拥有更好的弹性或者性能以支持未知但有可能发生的变化,自己就要求使用用户空间的资源来配合完成任务。好比内核中动态加载模块的部分须要调用kmod。但在编译kmod的时候不可能把全部的内核模块都订下来(要是这样的话动态加载模块就没有存在乎义了),因此它不可能知道在它之后才出现的那些模块的位置和加载方法。所以,模块的动态加载就采用了以下策略:加载任务实际上由位于用户空间的modprobe程序帮助完成——最简单的情形是modprobe用内核传过来的模块名字做为参数调用insmod。用这种方法来加载所须要的模块。
内核中启动用户程序仍是要经过execve这个系统调用原形,只是此时的调用发生在内核空间,而通常的系统调用则在用户空间进行。若是系统调用带参数,那将会碰到一个问题:由于在系统调用的具体实现代码中要检查参数合法性,该检查要求全部的参数必须位于用户空间——地址处于0x0000000——0xC0000000之间,因此若是咱们从内核传递参数(地址大于0xC0000000),那么检查就会拒绝咱们的调用请求。为了解决这个问题,咱们能够利用set_fs宏来修改检查策略,使得容许参数地址为内核地址。这样内核就能够直接使用该系统调用了。
例如:在kmod经过调用execve来执行modprobe的代码前须要有set_fs(KERNEL_DS):
......
set_fs(KERNEL_DS);
/* Go, go, go... */
if (execve(program_path, argv, envp) < 0)
return -errno;
上述代码中program_path 为"/sbin/modprobe",argv为{ modprobe_path, "-s", "-k", "--", (char*)module_name, NULL },envp为{ "HOME=/", "TERM=linux", "PATH=/sbin:/usr/sbin:/bin:/usr/bin", NULL }。
从内核中打开文件一样使用带参数的open系统调用,所需的还是要先调用set_fs宏。
内核和用户空间传递数据主要是用get_user(ptr)和put_user(datum,ptr)例程。因此在大部分须要传递数据的系统调用中均可以找到它们的身影。但是,若是咱们不是经过用户程序发起的系统调用——也就是说,没有明确的提供用户空间内的缓冲区位置——的状况下,如何向用户空间传递内核数据呢?
显然,咱们不能再直接使用put_user()了,由于咱们没有办法给它指定目的缓冲区。因此,咱们要借用brk系统调用和当前进程空间:brk用于给进程设置堆空间的大小。每一个进程拥有一个独立的堆空间,malloc等动态内存分配函数其实就是进程的堆空间中获取内存的。咱们将利用brk在当前进程(current process)的堆空间上扩展一块新的临时缓冲区,再用put_user将内核数据导出到这个肯定的用户空间去。
还记得刚才咱们在内核中调用用户程序的过程吗?在那里,咱们有一个跳过参数检查的操做,如今有了这种方法,能够另辟蹊径了:咱们在当前进程的堆上扩展一块空间,把系统调用要用到的参数经过put_user()拷贝到新扩展获得的用户空间里,而后在调用execve的时候以这个新开辟空间地址做为参数,因而,参数检查的障碍不复存在了。
char * program_path = "/bin/ls" ;
/* 找到当前堆顶的位置*/
mmm=current->mm->brk;
/* 用brk在堆顶上原扩展出一块256字节的新缓冲区*/
ret = brk(*(void)(mmm+256));
/* 把execve须要用到的参数拷贝到新缓冲区上去*/
put_user((void*)2,program_path,strlen(program_path)+1);
/* 成功执行/bin/ls程序!*/
execve((char*)(mmm+2));
/* 恢复现场*/
tmp = brk((void*)mmm);
这种方法没有通常性(具体的说,这种方法有负面效应吗),只能做为一种技巧,但咱们不难发现:若是你熟悉内核结构,就能够作到不少意想不到的事情!
信号在内核里的用途主要集中在通知用户程序出现重大错误,强行杀死当前进程,这时内核经过发送SIGKILL信号通知进程终止,内核发送信号使用send_sign(pid,sig)例程,能够看到信号发送必需要事先知道进程序号(pid),因此要想从内核中经过发信号的方式异步通知用户进程执行某项任务,那么必须事先知道用户进程的进程号才可。而内核运行时搜索到特定进程的进程号是个费事的工做,可能要遍历整个进程控制块链表。因此用信号通知特定用户进程的方法很糟糕,通常在内核不会使用。内核中使用信号的情形只出如今通知当前进程(能够从current变量中方便得到pid)作某些通用操做,如终止操做等。所以对内核开发者该方法用处不大。相似状况还有消息操做。这里不罗嗦了。
总结 由用户级程序主动发起的信息交互,不管是采用标准的调用方式仍是透过驱动程序界面,通常都要用到系统调用。而由内核主动发起信息交互的状况很少。也没有标准的界面,操做大不方便。因此通常状况下,尽量用本文描述的前几种方法进行信息交互。毕竟,在设计的根源上,相对于客户级程序,内核就被定义为一个被动的服务提供者。所以,咱们本身的开发也应该尽可能遵循这种设计原则。