现在的位置: 首页 > 综合 > 正文

linux内核–系统调用(一)

2013年12月13日 ⁄ 综合 ⁄ 共 2224字 ⁄ 字号 评论关闭

    UNIX系统通过向内核发出系统调用(system call)实现了用户态进程和硬件设备之间的大部分接口。

一、POSIX API和系统调用

    从编程者的观点看,API和系统调用之间的差别是没有关系的:唯一相关的事情就是函数名、 参数类型以及返回代码的含义。然而,从内核设计者的观点看,这种差别确实有关系,因为系统调用属于内核,而用户态的库函数不属于内核。

二、系统调用处理程序及服务例程

   当用户态的进程调用一个系统调用时,CPU切换到内核态并开始执行一个内核函数。调用系统调用的最终结果都是跳转到所谓系统调用处理程序的汇编语言函数。因为内核实现了很多不同的系统调用,进程必须传递一个名为系统调用号的参数来识别所需的系统调用,eax寄存器就用此目的。

    系统调用处理程序与其他异常处理程序的结构类似,执行如下操作:

  • 在内核态栈保存大多数寄存器的内容(这个操作队所有系统调用都通用,并用汇编语言编写)
  • 调用名为系统调用服务例程的相应的C函数来处理系统调用。
  • 退出系统调用处理程序:用保存在内核栈中的值加载寄存器,CPU从内核态切换回到用户态(所有的系统调用都要执行这一相同操作,该操作用汇编语言代码实现)

   下图展示了调用系统调用的应用程序、相应的封装例程、系统调用处理程序以及系统调用服务例程之间的关系,箭头表示函数之间的执行流。把CPU从用户态切换到内核态和从内核态切换到用户态。

    为了把系统调用与相应的服务例程关联起来,内核利用了一个系统调用分派表,这个表存放在sys_call_table数组中,有NR_syscalls个表项:第n个表项包含系统调用号为nd的服务例程的地址。

三、进入和退出系统调用

    可以通过两种不同的方式嗲用系统调用:

  • 执行int $0x80汇编语言指令,在linux内核的老版本中,这是从用户态切换到内核态的唯一方式。
  • 执行sysenter汇编语言指令。

    同样内核可以通过两种方式从系统调用退出,从而使CPU切换回到用户态:

  • 执行iret汇编语言指令
  • 执行sysexit汇编语言指令,它和sysenter指令同时在Intel Pentium 2微处理器中引入

    但是进入内核的两种范式并不像看起来那么简单,因为:

  • 内核必须既支持只使用int $0x80指令的旧函数库,同时支持也可以用sysenter指令的新函数库。
  • 使用sysenter指令的标准库必须能处理仅支持int $0x80指令的旧内核
  • 内核和标准库必须既能运行在不包含sysrenter指令的旧处理器上,也能运行在包含它的新处理器上。

  1)通过int $0x80指令发出系统调用

    调用系统调用的传统方法是使用汇编语言指令int。向量128(0x80)对应于内核入口点,在内核初始化期间调用的函数trap_init(),用下面的方式建立对应于向量128的中断描述符表表项:

    set_system_gate(0x80,&system_call);

因此,当用户态进程发出int $0x80指令时,CPU切换到内核态并开始从地址system_call处执行指令。

    system_call()函数首先把系统调用号和这个异常处理程序可以用到的所有的CPU保存到相应的栈中;随后,这个函数在ebx中存放当前进程的thread_info数据结构的地址;system_call()函数检查thread_info结构flag字段的标志位,检查是否有某一调试程序正在跟踪执行程序对系统调用的调用;最后,调用与eax中所包含的系统调用号对应的特定服务例程:

    call *sys_call_table(0,%eax,4)

因为分派表中的每个表项战4个字节,因此首先把系统调用号乘以4,再加上sys_call _table分派表的起始地址,然后从这个地址单元获取指向服务例程的指针,内核就找到了要调用的服务例程。

  2)从系统调用退出

    system_call()函数从eax获得它的返回值,并把这个返回值放在曾保存用户态eax寄存器值的那个栈单元的位置上:

    mov1 %eax, 24(%esp)

    用户态进程将在eax中找到系统调用的返回码。然后,system_call()函数关闭本地中断检查当前进程的thread_info结构中的标志。

  这里指解释了第一种方式的系统调用

四、参数传递

    system_call 和sysenter_entry()函数是linux中所有系统调用的公共入口点,因此最少的参数为通过eax寄存器传递来的系统调用号。例如,如果一个应用程序调用fork()封装例程,那么在执行int $0x80汇编指令之前就把eax寄存器置为2.很多系统调用确实需要由应用程序明确地传递参数,例如mmap()系统调用可能需要多达6个额外参数。(除了系统调用号之外)

    在发出系统调用之前,系统调用的参数被写入CPU寄存器,然后在调用系统调用服务例程之前,内核再把存放在CPU中的参数拷贝到内核堆栈中,这是因为系统调用服务例程是普通的C函数。

    举例说明,处理write()系统调用的sys_write()服务例程的声明如下:

    int sys_write(unsigned int fd,const char *buf,unsigned int count);C编译器产生一个汇编语言函数,该函数期望在栈的顶部找到fd、buf和count参数,而这些参数位于返回地址的下面。

五、内核封装例程

抱歉!评论已关闭.