用户空间和内核空间通讯之【系统调用】-wjlkoorey258-ChinaUnix博客
Thursday, August 25, 2016
7:54 AM
现在,越来越多的应用程序需要编写内核和用户级代码的程序来一起协作完成具体的任务,而用户与空间和内核空间的通讯也就是一个不可回避的话题了。针对于需要和内核空间通信的具体应用而言,其开发模式和套路相对来说比较固定,主要概括起来有两大步骤:
第一步,编写内核服务程序利用内核空间提供的权限和服务来接收、缓存和处理数据;第二步,编写用户程序来和先前的内核服务程序进行交互
具体来说,可以利用用户程序来配置内核服务程序的参数,获取内核服务程序提供的数据,也可以向内核服务程序输入数据。
我们可以看到,用户程序和内核的信息交换可以是双向的,也就是说既可以由用户主动向内核空间发送消息,也可以由内核空间主动向用户提交数据。当然,用户程序也可以主动从内核提取数据。
针对上述应用场景,Linux提供了几种用户和内核空间通讯的手段,在实际环境中可以根据需求自由选取,常见的几种方式是:系统调用,procfs,sysfs&ioctl,g(s)etsockopt以及netlink等。
今天我们先来谈谈系统调用。
2.6.21内核提供的系统调用,可以在linux-2.6.21\include\unistd.h头文件中找到它们,这里的指的就是CPU体系架构。对于x86而言,就是linux-2.6.21\include\asm-i386\unistd.h,一共有319个,如下所示:
Project Options View Window Help  *ifndef ASM 1386 UNISTD H  *define  * This file contains the system call numbers.  *define _NR_restart_syscall  *define _NR_read  #define _NR_open  *define _NR_close  *define _NR chdir  *define  NR exit  _NRZfork  # define  *define  NR write  *define  NR_waitpid  *define  NR_creat  *define  _NR_link  *define  NR unlink  # define  _NR_execve  *define  NR time  _NR_mknod  *define  NR chmod  *define  NR_lchown  *define _  NR_break  —define  NR oldstat  # define  NR_lseek  *define  _NR_getpid  *define  NR mount  *define  NR_umount  2  1  5  14  17  22  Chinauntx t*Z-  blog chinaun.x_ net
系统调用的实现和工作原理可以概括如下:
应用程序调用适当的值填充寄存器,然后调用一个特殊的指令,跳转到内核某一固定的位置,内核根据应用程序所填充的固定值来找到相应的函数,然后开始执行该函数。
注意上述关键词的描述。
1、适当的值:在unistd.h中我们可以看到,每个系统调用名都对应一个唯一的数字,这个数字我们称之为系统调用号,在整个系统中,这些系统调用号是唯一的,且由内核开发小组统一维护。例如,我们看到fork系统调用号为2,open系统调用号为5等等。
2、特殊的指令:在intel的CPU中,该指令由中断指令INT 0x80来实现,也就是说在Linux中,系统调用的接口是一个中断处理函数的特例。在x86体系中这个处理函数就是systemcall()。
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文件里)。
Eile Edit Options  ENTRY(sys_call_table)  along sys_waitpidl  , long  along sys_setuid16  along sys_getuid16  long  0 old
通过系统调用号可以找到其实现函数的所在位置,也就是说系统调用号和其实现函数的逻辑地址是一一对应的。open的系统调用号为5,那么在执行system_call()时会将“5”传递给它,然后system_call()就会去执行sys_open()系统调用了。其整个流程如下所示:
sys_exi t  sys_fork  open  sysTern_cal I
根据上图所示,如果我们要添加一个新的自定义的系统调用,可分为以下三个步骤:
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了。
我要新增的系统调用是一个用于计算加法的函数,其原型如下:
点击(此处)折叠或打开

  1. asmlinkage int sys_myadd(int a,int b)
  1. {
  1. return a+b;
  1. }

比较简单,其中的asmlinkage表示我们这个函数要从汇编语言中来调用,这也就是我们所看到的为什么所有的系统调用的实现函数脑袋上都顶了一个这玩意儿的原因,使用asmlinkage的另外一个原因是表示我们这个函数使用栈来传递参数。
我们新增的系统调用的实现函数sysmyadd()位于kernel/sys.c文件的末尾:
[![
Kernel -2, 6, 21 - Eile Edit srhBOL ElPORt srhBOL aoups_sort _StMSOL _stMsot- _stMSOL i _AÆH_WM k_ge •J sys ‘u.sk ht- -2,621 Options Help if (nodep) err I = nodep); if (cache) { The cache is not needed for this implementation, but make sure user programs pass something valid. vsyscall implementations can instead make good use of the cache. Only use to and tl because these are available in both 32bit and 64bit ABI (no need tor a compatgetcpu). 32bit has enough d ding unsigned long to, u; &cache- get_user(tL, &cache- to + + ; put_user(to, &cache- &cache- return err ? -EFAULT : o; end sys_myaddont P) asmlinkage int return a+b; blog chinaunixnet](https://cdn.nlark.com/yuque/0/2021/jpg/3018302/1639559548660-43b08440-78cc-4641-b879-0047110da39b.jpg)](http://blog.chinaunix.net/attachment/201209/18/23069658_1347977397lm3m.jpg)
然后,在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即可。修改后的结果如下所示:
[![g Kernel-2.621 Project - source 1 nux-2. .21 Eile Edit Search Project Help AR
„zplice A.Th_hAFr __kTh_WAxr _AEY_WAM _nry_wm _nry_wm wur — km-wur _ocy-wnrr _AFCY-WArr ATh_hAFr _kTEWAFr _AEILWANT _nry_wm _AEY_wm wur define _NR_fchmodat define NR_faccessat define _NR_pselect6 define NR_ppolI define _NR_unshare define NR set robust list define N st_list define _NR_splice define NR_syn ge define _NR_tee define NR_vmsplice # define _NR_move_pages define NR_getcpu 1’ / my test define _NR_myadd 320 ifdef _KERNEI— define NR_syscalls 3 define define define ARCH WANT OLD Chinaunl blog chanaumxnet STAT](https://cdn.nlark.com/yuque/0/2021/jpg/3018302/1639559548813-6819d5cf-d121-4e17-9c67-31815e996568.jpg)](http://blog.chinaunix.net/attachment/201209/18/23069658_1347977435GMJB.jpg)
最后,修改linux-2.6.21\arch\i386\kernel\syscall_table.s,增加新系统调用的函数入口:
[![g Kernel-2.6.21 Project - Source nstg@- Edit Search Options yie•vv Window -.-lång sys syscall_nbhS .10ng sys_openat long sys_mkdirat .10ng sys_mknodat .long sys_fchownat .long sys_futimesat .long sys_fstatat64 .long sys .long sys: .long sys_linkat .long sys_symlinkat .long sys .long sys:faccessat .long sys_pselect6 .long sys_vmsplice iriotify init .long .long .long sys_migrate_pages unlinkat renameat long sys_readlinkat fchmodat .long sys_ppoll /
295 / / 300 / 305 / .long sys
splice .long sys_tee .long sys_move_pages .long sys_getcpu .•orn s s .long sys_unshare / 310 .long sys_set_robust_list .long .long / 315/ m add / 320 ‘/ ChinaLJn1 blog chinaunix net](https://cdn.nlark.com/yuque/0/2021/jpg/3018302/1639559548952-7ad99b09-2cbd-4751-9695-b59e19d28346.jpg)](http://blog.chinaunix.net/attachment/201209/18/23069658_1347977455t30l.jpg)
为了使我们新增的系统调用sys_myadd能生效,接下来我们要重新编译内核,然后将其加载。用户空间的调用方式如下:
点击(此处)折叠或打开

  1. / my syscall test user-space source file :test.c/
  1. include

  1. include

  1. int main(int argc,char** argv)
  1. {
  1. int result=syscall(320,1,2);
  1. printf(“result=%d\n”,result);
  1. return 0;
  1. }

当然,这可能和我们常见的系统调用比起来有些另类,但是没关系,你完全可以自定义一个函数,比如add(),然后对其syscall(320,1,2)进行一层封装,然后就可以像下面这样子调用了:
点击(此处)折叠或打开

  1. / my syscall test user-space source file :test.c/
  1. include

  1. include

  1. int add(int a,int b)
  1. {

return syscall(320,a,b);

  1. }
  1. int main(int argc,char** argv)
  1. {
  1. int result= add(1,2);

printf(“result=%d\n”,result);

  1. return 0;

}
根据博文“从头构建自己的linux系统”里我们介绍的方法,重新编译内核镜像,然后用如下的命令:
gcc -static -o addtest test.c
来编译用户空间的应用程序,然后将其放到initrd的bin目录下。之所以gcc要用static是因为我们的整个系统都是以静态链接的形式存在的,没有动态依赖库,所以如果不加static那么编译出来的可执行程序在我们的Mini系统上是跑不起来的,不信你可以试一下。
![[root€localhost Is buss,’hÜx-I.14.a initrd test.c rout f-s - i. I •1.4. 1 Irwx-2.6.21 [rootlocalhost cat test.c #incILJde <stdio. h) frinclude <linux/unistd. int main(int argv) int , result); return e; [ rootlocalhost rootlocalhost [rootlocalhost [root610ca1host [ rootlocalhost root@localhost gcc -static -o addtest test.c mount •o loop initrd tmp cp addtest tmp/bin/ sync umount tmp)
VMWare固然强大,但是在我们目前这种学习环境里显得有些臃肿,每次都要先进到一个标准linux系统,然后将我们编译生成的initrd和bzImage分别拷贝到/boot目录然,然后重启系统选择加载我们自己的Mini系统,不论initrd或bzImage任何一个有改变时都需要重复这个繁琐的过程,今天我们介绍另一款当下比较流程Qemu模拟器,用它来模拟我们加载我们自己定制的Mini。
最新的Qemu Manager 7.0已经推出,功能之强大毫无逊色于VMware,但解压后只有40M多。可以下载最新的绿色版来用。为了支持网络功能还需要安装openvpn-2.0.9-install.exe,可以从“
用户空间和内核空间通讯之【系统调用】-wjlkoorey258-ChinaUnix博客 - 图4
openvpn-2.0.9-install.rar ”下载。当openvpn-2.0.9-install.exe安装完成后会生成一个新的本地连接,如下:
M„-vell 88E80S7 PCI-E .  FamilyF .  X TAPWin32  ChinaUn1x  blog chtnaurvx net
将QemuManager_7.0.rar解压到本地目录,注意路径中不能包含中文,否则要报错。我将其放在D:\linux目录下,最后的截图如下:
TAP0rW  Delzip190.dll  t.pdi  m / term  2010/3/31 11:27  18:17  2010/3/21 10:20  2012/90 IE;IJ  2012/0/7 18:17  2012/9/7 18:17  2010/4/11047  2010/3/31 11:43  2010/3/31  2010/4/4  2010/3/21  2010/4/4  1:26  2010/43 11:25  2010/4/3 7;26  2010/3/3  2010/3/31  2010/3/5 7:28  KM;' - MPEG  - MPEG  MANIFEST  - MPEG M..
紧接着在D:\linux\qman70里建立system\mylinux目录,将我们编译生成的initrd和bzImage拷贝到mylinux中,然后开始创建Qemu虚拟机。
1、运行QemuManager.exe,什么也不用改,都用默认配置即可。
Qemu Manager First Run Wizard  'o Q—ru  aerau Manager a Please speciyany settings  fm updates when *tarts  Launch  On system  Advanced Opti*ß Of on & LISB pen  Chinauntx
2、建立虚拟系统。
NCN Virtual Machine Wizard  QEM  ChinatJnlk
3、为新系统分配内存和硬盘大小。
Virual Machire  New Virtual Machine Wizard  Settng.  Hear specfy the arnount ard hard  Size  Please specty amount cl RPM for the vitual  Plirnary Virtue Hard Disk  Create NSA Disk  C) Imge  0 00 not use an Virtual Disk  Disk Inuge  Disk ImsgeSize 1  Dick QEMIJ  2013 MB.  Encrypt  8. (0.001
4、显示方式选择为Qemu窗口显示。
New Virual Machine Wizard  New Virtual Machine Wizard  It prornpted some final below.  Ycu wit be able to make ediions ore the vitual has been created.  Vitual Output
完成后的效果如下:
ChinatJnlx
为了使用网络,我们还需要配置Network Card1选项,VLAN TYPE一定要选择Tap Netowrking类型,适配器选择我们openvp所生成的那个网络连接,我这里是本地连接5,如下所示:
第 を 基 u コ 工 u で 凵  p い 考 動 N  : 辷 A コ 下 当 1  0 当 一 コ こ 「 一
然后在“Advanced”标签页配置内核镜像和ramdisk文件所在的路径,而且还可以配置内核启动参数,即加载内核时传递什么样的参数如给他,目前我们用不到这个。最后的配置结果如下所示:
ChinaUnl*  L QEMu
最后,将openvpn生成的本地连接的IP地址配置成和我们的Mini系统在一个网段,就可以了,默认情况下Mini Linux的网络地址是192.168.1.1。运行我们的系统:
凵 - - … … 09 - , ~ p H 13u 11 凵 u 1  SSBOOns
一切OK,我们自己开发的系统调用的应用程序也工作的很愉快。
小结:我们可以看到系统调用这种用户-内核空间的通信方式确实比较麻烦,所以一般情况下我们都不用这种方法。原因已经不厌其烦的解释过了,但是通过今天的学习相信大家对Linux系统调用的认识和理解都有了一个全新的认识,同时,也掌握了如何开发系统调用的方法,对自我能力的提升都是很有帮助。
已使用 OneNote 创建。