系统调用的原理和实现+添加一个新的系统调用

统调用概述

        计算机系统的各种硬件资源是有限的,在现代多任务操作系统上同时运行的多个进程都需要访问这些资源,为了更好的管理这些资源进程是不允许直接操作的,所有对这些资源的访问都必须有操作系统控制。也就是说操作系统是使用这些资源的唯一入口,而这个入口就是操作系统提供的系统调用(System Call)。

        系统调用是属于操作系统内核的一部分的,必须以某种方式提供给进程让它们去调用。CPU可以在不同的特权级别下运行,而相应的操作系统也有不同的运行级别,用户态和内核态。运行在内核态的进程可以毫无限制的访问各种资源,而在用户态下的用户进程的各种操作都有着限制,比如不能随意的访问内存、不能开闭中断以及切换运行的特权级别。显然,属于内核的系统调用一定是运行在内核态下,但是如何切换到内核态呢?

        答案是中断。操作系统一般是通过中断从用户态切换到内核态。中断就是一个硬件或软件请求,要求CPU暂停当前的工作,去处理更重要的事情。比如,在x86机器上可以通过int指令进行软件中断,而在磁盘完成读写操作后会向CPU发起硬件中断。

        中断有两个重要的属性,中断号和中断处理程序。中断号用来标识不同的中断,不同的中断具有不同的中断处理程序。在操作系统内核中维护着一个中断向量表(Interrupt Vector Table),这个数组存储了所有中断处理程序的地址,而中断号就是相应中断在中断向量表中的偏移量。

        一般地,系统调用都是通过中断实现的,比如,Linux下中断号0x80就是进行系统调用的。接下来就来看一下Linux下系统调用具体的实现过程。

Linux下系统调用的实现

        前文已经提到了Linux下的系统调用是通过0x80实现的,但是我们知道操作系统会有多个系统调用(Linux下有319个系统调用),而对于同一个中断号是如何处理多个不同的系统调用的?最简单的方式是对于不同的系统调用采用不同的中断号,但是中断号明显是一种稀缺资源,Linux显然不会这么做;还有一个问题就是系统调用是需要提供参数,并且具有返回值的,这些参数又是怎么传递的?也就是说,对于系统调用我们要搞清楚两点:

        1. 系统调用的函数名称转换。

        2. 系统调用的参数传递。

        首先看第一个问题。实际上,Linux中处理系统调用的方式与中断类似。每个系统调用都有相应的系统调用号作为唯一的标识,内核维护一张系统调用表,表中的元素是系统调用函数的起始地址,而系统调用号就是系统调用在调用表的偏移量。在进行系统调用是只要指定对应的系统调用号,就可以明确的要调用哪个系统调用,这就完成了系统调用的函数名称的转换。举例来说,Linux中fork的调用号是2(具体定义,在我的计算机上是在/usr/include/asm/unistd_32.h,可以通过find / -name unistd_32.h -print查找)

  1. #ifndef _ASM_X86_UNISTD_32_H  
  2. #define _ASM_X86_UNISTD_32_H  
  3.   
  4. /* 
  5.  * This file contains the system call numbers. 
  6.  */  
  7.   
  8. #define __NR_restart_syscall      0  
  9. #define __NR_exit                 1  
  10. #define __NR_fork                 2  
  11. #define __NR_read                 3  
  12. #define __NR_write                4  
  13. #define __NR_open                 5  
        Linux中是通过寄存器%eax传递系统调用号,所以具体调用fork的过程是:将2存入%eax中,然后进行系统调用,伪代码:
  1. mov     eax, 2  
  2. int     0x80  
        对于参数传递,Linux是通过寄存器完成的。Linux最多允许向系统调用传递6个参数,分别依次由%ebx,%ecx,%edx,%esi,%edi和%ebp这个6个寄存器完成。比如,调用exit(1),伪代码是:
  1. mov    eax, 2  
  2. mov    ebx, 1  
  3. int    0x80  
        因为exit需要一个参数1,所以这里只需要使用ebx。这6个寄存器可能已经被使用,所以在传参前必须把当前寄存器的状态保存下来,待系统调用返回后再恢复,这个在后面栈切换再具体讲。
        Linux中,在用户态和内核态运行的进程使用的栈是不同的,分别叫做用户栈和内核栈,两者各自负责相应特权级别状态下的函数调用。当进行系统调用时,进程不仅要从用户态切换到内核态,同时也要完成栈切换,这样处于内核态的系统调用才能在内核栈上完成调用。系统调用返回时,还要切换回用户栈,继续完成用户态下的函数调用。

        寄存器%esp(栈指针,指向栈顶)所在的内存空间叫做当前栈,比如%esp在用户空间则当前栈就是用户栈,否则是内核栈。栈切换主要就是%esp在用户空间和内核空间间的来回赋值。在Linux中,每个进程都有一个私有的内核栈,当从用户栈切换到内核栈时,需完成保存%esp以及相关寄存器的值(%ebx,%ecx...)并将%esp设置成内核栈的相应值。而从内核栈切换会用户栈时,需要恢复用户栈的%esp及相关寄存器的值以及保存内核栈的信息。一个问题就是用户栈的%esp和寄存器的值保存到什么地方,以便于恢复呢?答案就是内核栈,在调用int指令机型系统调用后会把用户栈的%esp的值及相关寄存器压入内核栈中,系统调用通过iret指令返回,在返回之前会从内核栈弹出用户栈的%esp和寄存器的状态,然后进行恢复。

        相信大家一定听过说,系统调用很耗时,要尽量少用。通过上面描述系统调用的实现原理,大家也应该知道这其中的原因了。第一,系统调用通过中断实现,需要完成栈切换。第二,使用寄存器传参,这需要额外的保存和恢复的过程。

        上面关于系统调用的阐述,如有错误欢迎指正。。




5.4.1 如何实现一个新的系统调用

2010-07-23 12:38 任桥伟 人民邮电出版社 字号: T | T
一键收藏,随时查看,分享好友!

《Linux内核修炼之道》第5章讲解系统调用,它是应用程序和内核间的桥梁,学习并理解它是我们走向内核的一个很好的过渡。本节为大家介绍如何实现一个新的系统调用。

AD:WOT2014:用户标签系统与用户数据化运营培训专场

5.4 系统调用的实现

一个系统调用的实现并不需要去关心如何从用户空间转换到内核空间,以及系统调用处理程序如何去执行,你需要做的只是遵循几个固定的步骤。

5.4.1  如何实现一个新的系统调用

为Linux添加新的系统调用是件相对容易的事情,主要包括有4个步骤:编写系统调用服务例程;添加系统调用号;修改系统调用表;重新编译内核并测试新添加的系统调用。

下面以一个并无实际用处的hello系统调用为例,来演示上述几个步骤。

(1)编写系统调用服务例程。

遵循前面所述的几个原则,hello系统调用的服务例程实现为:

 
 
  1. 01 asmlinkage long sys_hello(void)  
  2. 02 {  
  3. 03      printk("Hello!\n");  
  4. 04      return 0;  
  5. 05 } 

通常,应该为新的系统调用服务例程创建一个新的文件进行存放,但也可以将其定义在其他文件之中并加上注释做必要说明。同时,还要在include/linux/syscalls.h文件中添加原型声明:

 
 
  1. asmlinkage long sys_hello(void); 

sys_hello函数非常简单,仅仅打印一条语句,并没有使用任何参数。如果我们希望hello系统调用不仅能打印"hello!"欢迎信息,还能够打印出我们传递过去的名称,或者其他的一些描述信息,则sys_hello函数可以实现为:

 
 
  1. 01 asmlinkage long sys_hello(const char __user *_name)  
  2. 02 {  
  3. 03      char *name;  
  4. 04      long ret;  
  5. 05  
  6. 06      name = strndup_user(_name, PAGE_SIZE);  
  7. 07      if (IS_ERR(name)) {  
  8. 08          ret = PTR_ERR(name);  
  9. 09          goto error;  
  10. 10      }  
  11. 11  
  12. 12      printk("Hello, %s!\n", name);  
  13. 13      return 0;  
  14. 14  error:  
  15. 15      return ret;  
  16. 16 } 

第二个sys_hello函数使用了一个参数,在这种有参数传递发生的情况下,编写系统调用服务例程时必须仔细检查所有的参数是否合法有效。因为系统调用在内核空间执行,如果不加限制任由用户应用传递输入进入内核,则系统的安全与稳定将受到影响。

参数检查中最重要的一项就是检查用户应用提供的用户空间指针是否有效。比如上述sys_hello函数参数为char类型指针,并且使用了__user标记进行修饰。__user标记表示所修饰的指针为用户空间指针,不能在内核空间直接引用,原因主要如下。

用户空间指针在内核空间可能是无效的。

用户空间的内存是分页的,可能引起页错误。

如果直接引用能够成功,就相当于用户空间可以直接访问内核空间,产生安全问题。

因此,为了能够完成必须的检查,以及在用户空间和内核空间之间安全地传送数据,就需要使用内核提供的函数。比如在sys_hello函数的第6行,就使用了内核提供的strndup_user函数(在mm/util.c文件中定义)从用户空间复制字符串name的内容。

(2)添加系统调用号。

每个系统调用都会拥有一个独一无二的系统调用号,所以接下来需要更新include/asm-i386/unistd.h文件,为hello系统调用添加一个系统调用号。

 
 
  1. 328 #define __NR_utimensat      320  
  2. 329 #define __NR_signalfd       321  
  3. 330 #define __NR_timerfd        322  
  4. 331 #define __NR_eventfd        323  
  5. 332 #define __NR_fallocate      324  
  6. 333 #define __NR_hello          325 /*分配hello系统调用号为325*/  
  7. 334  
  8. 335 #ifdef __KERNEL__  
  9. 336  
  10. 337 #define NR_syscalls 326 /*将系统调用数目加1修改为326*/ 

(3)修改系统调用表。

为了让系统调用处理程序system_call函数能够找到hello系统调用,我们还需要修改系统调用表sys_call_table,放入服务例程sys_hello函数的地址。

 
 
  1. 322 .long sys_utimensat     /* 320 */  
  2. 323 .long sys_signalfd  
  3. 324 .long sys_timerfd  
  4. 325 .long sys_eventfd  
  5. 326 .long sys_fallocate  
  6. 327 .long sys_hello /*hello系统调用服务例程*/ 

新的系统调用hello的服务例程被添加到了sys_call_table的末尾。我们可以注意到,sys_call_table每隔5个表项就会有一个注释,表明该项的系统调用号,这个好习惯可以在查找系统调用对应的系统调用号时提供方便。

(4)重新编译内核并测试。

为了能够使用新添加的系统调用,需要重新编译内核,并使用新内核重新引导系统。然后,我们还需要编写测试程序对新的系统调用进行测试。针对hello系统调用的测试程序如下:

 
 
  1. 00 #include <unistd.h> 
  2. 01 #include <sys/syscall.h> 
  3. 02 #include <sys/types.h> 
  4. 03  
  5. 04 #define __NR_hello       325  
  6. 05  
  7. 06 int main(int argc, char *argv[])  
  8. 07 {  
  9. 08      syscall(__NR_hello);  
  10. 09      return 0;  
  11. 10 } 

然后使用gcc编译并执行:

 
 
  1. $gcc -o hello hello.c  
  2. $./hello  
  3. Hello! 

由执行结果可见,系统调用添加成功。


  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值