分类: LINUX
2016-08-24 15:54:31
原文地址:用户空间和内核空间通讯之【系统调用】 作者:wjlkoorey258
现在,越来越多的应用程序需要编写内核和用户级代码的程序来一起协作完成具体的任务,而用户与空间和内核空间的通讯也就是一个不可回避的话题了。针对于需要和内核空间通信的具体应用而言,其开发模式和套路相对来说比较固定,主要概括起来有两大步骤:
第一步,编写内核服务程序利用内核空间提供的权限和服务来接收、缓存和处理数据;第二步,编写用户程序来和先前的内核服务程序进行交互。
具体来说,可以利用用户程序来配置内核服务程序的参数,获取内核服务程序提供的数据,也可以向内核服务程序输入数据。
我们可以看到,用户程序和内核的信息交换可以是双向的,也就是说既可以由用户主动向内核空间发送消息,也可以由内核空间主动向用户提交数据。当然,用户程序也可以主动从内核提取数据。
针对上述应用场景,Linux提供了几种用户和内核空间通讯的手段,在实际环境中可以根据需求自由选取,常见的几种方式是:系统调用,procfs,sysfs&ioctl,g(s)etsockopt以及netlink等。
今天我们先来谈谈系统调用。
2.6.21内核提供的系统调用,可以在linux-2.6.21\include\
系统调用的实现和工作原理可以概括如下:
应用程序调用适当的值填充寄存器,然后调用一个特殊的指令,跳转到内核某一固定的位置,内核根据应用程序所填充的固定值来找到相应的函数,然后开始执行该函数。
注意上述关键词的描述。
1、适当的值:在unistd.h中我们可以看到,每个系统调用名都对应一个唯一的数字,这个数字我们称之为系统调用号,在整个系统中,这些系统调用号是唯一的,且由内核开发小组统一维护。例如,我们看到fork系统调用号为2,open系统调用号为5等等。
2、特殊的指令:在intel的CPU中,该指令由中断指令INT 0x80来实现,也就是说在Linux中,系统调用的接口是一个中断处理函数的特例。在x86体系中这个处理函数就是system_call()。
3、固定的位置:当我们执行一个系统调用时,我们前面提到的系统调用号会作为参数传递给system_call(),然后system_call()函数通过查询中断向量表找到每个系统调用所对应的实现函数的位置,即内核空间中,哪个内存地址存放哪个系统调用的处理函数是在内核加载时就已经固定了的。
4、相应的函数:系统调用的处理函数都已“sys_”开头,而所有的系统调用的处理函数最后组成了一张表,叫做系统调用表sys_call_table,位于linux-2.6.21\arch\i386\kernel\syscall_table.s文件中,这是一个汇编文件(注意:如果是在arm系统中,系统调用表的是在内核源码包的linux-2.6.21\arch\arm\kernel\calls.s文件里)。
通过系统调用号可以找到其实现函数的所在位置,也就是说系统调用号和其实现函数的逻辑地址是一一对应的。open的系统调用号为5,那么在执行system_call()时会将“5”传递给它,然后system_call()就会去执行sys_open()系统调用了。其整个流程如下所示:
根据上图所示,如果我们要添加一个新的自定义的系统调用,可分为以下三个步骤:
1、在内核中添加系统调用的实现函数;
2、更新头文件unistd.h;
3、更新syscall_table.s文件。
内核中系统调用的实现函数的分类也相当有讲究,例如系统级的函数一般位于kernel/sys.c中;文件操作的系统调用位于linux-2.6.21/fs目录里,其中每个函数对应一个相应的系统调用的实现文件,如fs/open.c、fs/write.c等等;和socket相关的系统调用其实现函数位于linux-2.6.21/net/socket.c中,里面有sys_socket,sys_bind等。为了简单起见,我们新增的系统调用将其放在kernel/sys.c中,当然你也可以仿照内核那样去组织目录结构来存放你的系统调用,但这样就得你自己去写Makefile了。
我要新增的系统调用是一个用于计算加法的函数,其原型如下:
点击(此处)折叠或打开
比较简单,其中的asmlinkage表示我们这个函数要从汇编语言中来调用,这也就是我们所看到的为什么所有的系统调用的实现函数脑袋上都顶了一个这玩意儿的原因,使用asmlinkage的另外一个原因是表示我们这个函数使用栈来传递参数。
我们新增的系统调用的实现函数sys_myadd()位于kernel/sys.c文件的末尾:
然后,在linux-2.6.21\include\asm-i386\unistd.h中修改如下:
1、新增一行#define __NR_myadd 320
2、并将原来的#define NR_syscalls 320改为#define NR_syscalls 321。
由此可见,x86其实是不鼓励我们新增自定义的系统调用,新增一个系统调用要改两个地方,不像arm那样简单,只需要在arm架构的unistd.h中按系统调用号顺序递增加1即可。修改后的结果如下所示:
最后,修改linux-2.6.21\arch\i386\kernel\syscall_table.s,增加新系统调用的函数入口:
为了使我们新增的系统调用sys_myadd能生效,接下来我们要重新编译内核,然后将其加载。用户空间的调用方式如下:
点击(此处)折叠或打开
当然,这可能和我们常见的系统调用比起来有些另类,但是没关系,你完全可以自定义一个函数,比如add(),然后对其syscall(320,1,2)进行一层封装,然后就可以像下面这样子调用了:
点击(此处)折叠或打开
根据博文“从头构建自己的linux系统”里我们介绍的方法,重新编译内核镜像,然后用如下的命令:
gcc -static -o addtest test.c
来编译用户空间的应用程序,然后将其放到initrd的bin目录下。之所以gcc要用static是因为我们的整个系统都是以静态链接的形式存在的,没有动态依赖库,所以如果不加static那么编译出来的可执行程序在我们的Mini系统上是跑不起来的,不信你可以试一下。
VMWare固然强大,但是在我们目前这种学习环境里显得有些臃肿,每次都要先进到一个标准linux系统,然后将我们编译生成的initrd和bzImage分别拷贝到/boot目录然,然后重启系统选择加载我们自己的Mini系统,不论initrd或bzImage任何一个有改变时都需要重复这个繁琐的过程,今天我们介绍另一款当下比较流程Qemu模拟器,用它来模拟我们加载我们自己定制的Mini。
最新的Qemu Manager 7.0已经推出,功能之强大毫无逊色于VMware,但解压后只有40M多。可以下载最新的绿色版来用。为了支持网络功能还需要安装openvpn-2.0.9-install.exe,可以从“ openvpn-2.0.9-install.rar ”下载。当openvpn-2.0.9-install.exe安装完成后会生成一个新的本地连接,如下:
将QemuManager_7.0.rar解压到本地目录,注意路径中不能包含中文,否则要报错。我将其放在D:\linux目录下,最后的截图如下:
紧接着在D:\linux\qman70里建立system\mylinux目录,将我们编译生成的initrd和bzImage拷贝到mylinux中,然后开始创建Qemu虚拟机。
1、运行QemuManager.exe,什么也不用改,都用默认配置即可。
2、建立虚拟系统。
3、为新系统分配内存和硬盘大小。
4、显示方式选择为Qemu窗口显示。
完成后的效果如下:
为了使用网络,我们还需要配置Network Card1选项,VLAN TYPE一定要选择Tap Netowrking类型,适配器选择我们openvp所生成的那个网络连接,我这里是本地连接5,如下所示:
然后在“Advanced”标签页配置内核镜像和ramdisk文件所在的路径,而且还可以配置内核启动参数,即加载内核时传递什么样的参数如给他,目前我们用不到这个。最后的配置结果如下所示:
最后,将openvpn生成的本地连接的IP地址配置成和我们的Mini系统在一个网段,就可以了,默认情况下Mini Linux的网络地址是192.168.1.1。运行我们的系统:
一切OK,我们自己开发的系统调用的应用程序也工作的很愉快。
小结:我们可以看到系统调用这种用户-内核空间的通信方式确实比较麻烦,所以一般情况下我们都不用这种方法。原因已经不厌其烦的解释过了,但是通过今天的学习相信大家对Linux系统调用的认识和理解都有了一个全新的认识,同时,也掌握了如何开发系统调用的方法,对自我能力的提升都是很有帮助。