Linux系统调用具体解释(怎样从用户空间进入内核空间)

系统调用概述

        计算机系统的各种硬件资源是有限的,在现代多任务操作系统上同一时候执行的多个进程都须要訪问这些资源,为了更好的管理这些资源进程是不同意直接操作的,全部对这些资源的訪问都必须有操作系统控制。也就是说操作系统是使用这些资源的唯一入口,而这个入口就是操作系统提供的系统调用(System Call)。在linux中系统调用是用户空间訪问内核的唯一手段,除异常和陷入外,他们是内核唯一的合法入口。

       普通情况下应用程序通过应用编程接口API,而不是直接通过系统调用来编程。

在Unix世界,最流行的API是基于POSIX标准的。

       操作系统通常是通过中断从用户态切换到内核态。中断就是一个硬件或软件请求,要求CPU暂停当前的工作,去处理更重要的事情。

比方。在x86机器上能够通过int指令进行软件中断。而在磁盘完毕读写操作后会向CPU发起硬件中断。

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

        一般地,系统调用都是通过软件中断实现的,x86系统上的软件中断由int $0x80指令产生,而128号异常处理程序就是系统调用处理程序system_call()。它与硬件体系有关。在entry.S中用汇编写。

接下来就来看一下Linux下系统调用详细的实现过程。

Linux下系统调用的实现

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

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

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

        首先看第一个问题。实际上,Linux中每一个系统调用都有对应的系统调用号作为唯一的标识,内核维护一张系统调用表,sys_call_table,表中的元素是系统调用函数的起始地址,而系统调用号就是系统调用在调用表的偏移量。在x86上,系统调用号是通过eax寄存器传递给内核的。比方fork()的实现:

在/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  
      所以详细调用fork的过程是:将2存入%eax中,然后进行系统调用,伪代码:

  1. mov     eax, 2  
  2. int     0x80  
        对于參数传递,Linux是通过寄存器完毕的。Linux最多同意向系统调用传递6个參数,分别依次由%ebx,%ecx,%edx,%esi,%edi这5个寄存器完毕,须要6个及以上參数情况不多见。另外应该有一个单独的寄存器存放指向全部这些參数在用户空间的地址的指针。给用户空间的返回值通过eax寄存器传递。比方,调用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和寄存器的状态。然后进行恢复。

        相信大家一定听过说,系统调用非常耗时。要尽量少用。通过上面描写叙述系统调用的实现原理,大家也应该知道这当中的原因了。

第一,系统调用通过中断实现,须要完毕栈切换。

第二,使用寄存器传參,这须要额外的保存和恢复的过程。

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

posted @ 2017-06-01 11:07  wzjhoutai  阅读(1978)  评论(0编辑  收藏  举报