分类:
2011-12-19 12:57:34
原文地址:system call 的实现原理 作者:bob_zhang2004
转载的文章, 出处忘记了。这哥们分析的很透彻,
转到这里, 与大家共享。
有实际开发经验的人都知道,在操作系统上运行的某个应用程序,如果想完成一些实际有用的功能,必然会用到操作系统提供的接口,这些接口被称为系统调用(System Call)。
由操作系统提供的功能,通常应用程序本身是无法实现的。例如对文件进行操作,应用程序必需通过系统调用才能做到,因为只有操作系统才具有直接管理外围设备的权限。又如进程或线程间的同步互斥操作,也必需经由操作系统对内核变量进行维护才能完成。
应用程序的进程通常在user模式下运行,当它调用一个系统调用时,进程进入kernel模式,执行的是kernel内部的代码,从而具有执行特权指令的权限,完成特定的功能。换句话说,系统调用是应用程序主动进入操作系统内核的入口。
(一)系统调用和库函数的区别
在不同的操作系统上,系统调用可能以不同的编程语言暴露给开发人员。例如,早期操作系统的系统调用以汇编语言表达,而现代操作系统通常以C语言的形式表达系统调用接口,还有可能封装成其它的高级语言。
实 际上,以某种高级语言(如C)表示的系统调用是对真正系统调用的一个封装,开发人员看到的是库函数的形式,例如Linux的API在libc库中实现。虽 然调用一个系统调用与调用一个普通库函数看起来区别不大,但实质上是不同的。例如,在Linux操作系统下,C语言的库函数printf,实际上使用了 write系统调用;而库函数strcpy(字符串拷贝)却没有使用任何系统调用。另外,一个系统的系统调用接口通常是能够完成所有必需功能的最小集合, 可能存在多个库函数对同一个系统调用进行封装。例如,在Linux中,malloc、calloc和free三个库函数底层都是调用brk系统调用完成 的。
应用程序、库函数和系统调用的关系如下图所示:
(二)系统调用和中断的联系
中断(Interrupt)通常是指在CPU内部或外部发生了某个待处理的事件,从而CPU必需改变当前指令的执行顺序去处理这类事件。在介绍中断和系统调用的关系之前,下面先把中断做一个分类。
中断可以大体分为两大类:
* Synchronous interrupts:在CPU内部产生,说这类中断是同步的,意思是中断信号的发射时间一定在当前指令执行结束之后。
* Asynchronous interrupts: 由CPU外部的其它硬件产生,说这类中断是异步的,意思是中断信号可以在任意时间发射,与CPU本身的时钟节拍没有关系。
有些技术资料中会将上述两种中断分别称为exceptions和interrupts。
Asynchronous interrupts又可分为以下两类:
* Maskable interrupts:大部分中断都可以被屏蔽。
* Nonmaskable interrupts:一些紧急事件,如硬件故障,是不可屏蔽的中断。
Synchronous interrupts (又称为exceptions)又分为以下若干类:
* Processor-detected exceptions:处理器在执行指令时检测到的中断,如除零操作。
* Faults:发生了某个异常条件,但异常条件被消除后,原来的程序流程可以继续执行而不受任何影响,如缺页中断。
* Traps:由陷入指令引起的中断,通常用于程序调试。
* Aborts:CPU内部有重要错误发生,例如硬件错误或系统表值出现错误。一旦这种中断发生,错误将不可恢复,只能将当前进程终止。
* Programmed exceptions:也称为software interrupts,由程序员的代码主动发起的中断。有两种用法:(1)用来实现系统调用;(2)通知调试器某个特殊事件。
至此,我们发现了中断与系统调用的关系:系统调用是一种特殊的中断类型。
在x86的机器中,用一个8bit的数字(0~255)来区分各种中断,这个数字被称为中断向量(vector)。其中一个中断向量,即128,专门被用于执行系统调用。
在 Linux系统中,存有一个系统表,叫做Interrupt Descriptor Table,简称IDT。IDT表共有256项,存放了从中断向量到相应处理例程(interrupt or exception handler)的映射关系。当某个中断发生时,CPU从IDT表中查找到相应的处理例程的地址来执行。
(三)内核对于系统调用的处理
在上面一节我们提到,系统调用的处理例程在IDT表中占有一项。这一项是在trap_init函数中被初始化的,如下:
set_system_gate(SYSCALL_VECTOR,&system_call);
如前所述,上面代码中的SYSCALL_VECTOR的值是128。
当系统调用发生时,通过中断机制,系统调用例程system_call被调用。system_call由汇编语言和C的代码构成,它的执行过程大概分为4个步骤(注意参数的传入和返回值的传出过程):
1. 从寄存器中取出系统调用号(system call number)和输入参数,然后将这些寄存器的值压入kernel栈中。这一部分的代码用汇编写成。
2. 根据系统调用号(system call number)查找系统调用分派表(system call dispatch table),找到系统调用服务例程(system call service routine )。汇编语言。
3. 调用查到的系统调用服务例程。这一部分用C语言写成,因为第1步中已经将输入参数保存在kernel栈中,所以在C函数的参数表中能够拿到输入参数,使得系统调用服务例程在表面上看与一个普通的C函数没有区别。
4. 将系统调用服务例程的返回值出栈,重新保存在寄存器中。汇编语言。
上 面描述的系统调用例程system_call在kernel空间中执行。在执行前,系统调用号和输入参数已经存入了寄存器,这个存入过程由user空间的 代码完成。实际上,如同第一节所讲,每个真正的系统调用基本上都有一个封装它的库函数,一般是在这个库函数中完成系统调用号和输入参数的保存动作。当系统 调用例程system_call执行完毕后,返回值通过寄存器再传回user空间的库函数。
下面详细地介绍上面所讲的4个步骤(涉及代码以2.6.9版的kernel为准)。
在第1步之前,user空间的封装函数已经将对应的系统调用号保存在eax寄存器中,将输入参数保存在ebx, ecx, edx, esi,以及edi寄存器中(因此最多传6个参数,包括系统调用号)。
第1步中将输入参数寄存器的值压入kernel栈的操作由汇编代码__SAVE_ALL宏完成。如下:
#define __SAVE_ALL \
cld; \
pushl %es; \
pushl %ds; \
pushl %eax; \
pushl %ebp; \
pushl %edi; \
pushl %esi; \
pushl %edx; \
pushl %ecx; \
pushl %ebx; \
movl $(__USER_DS), %edx; \
movl %edx, %ds; \
movl %edx, %es;
第2步中的系统分派表在kernel代码中以变量sys_call_table表示。查找系统调用服务例程的动作就是从sys_call_table里找系统调用号(存在eax寄存器中)指向的那一项,如下:
syscall_call:
call *sys_call_table(,%eax,4)
sys_call_table中的项在sys_call_table.c文件中定义:
syscall_handler_t *sys_call_table[] = {
......
[ __NR_exit ] (syscall_handler_t *) sys_exit,
[ __NR_fork ] (syscall_handler_t *) sys_fork,
[ __NR_read ] = (syscall_handler_t *) sys_read,
[ __NR_write ] = (syscall_handler_t *) sys_write,
......
[ __NR_socketcall ] (syscall_handler_t *) sys_socketcall,
......
};
在 这里我们注意到一些常用的系统调用号,如,exit系统调用号为__NR_exit = 1,fork系统调用号为__NR_fork = 2,read系统调用号为__NR_read = 3,write系统调用号为__NR_write = 4,所有socket相关的API的系统调用号都是__NR_socketcall= 102。
第3步,执行C函数实现的系统调用例程。该例程最多接受6个参数(包括系统调用号),返回值是一个整型。返回值为非负,表示执行成功;返回值为负,表示执行出错,该错误码的绝对值会最后存在user空间的errno全局变量中。
第4步,调用syscall_exit_work退出系统调用,并从kernel模式回到user模式。第3步的C函数执行return err的时候,编译后的代码已经将返回值存在了eax寄存器中。
最后,回到user模式的封装函数中,对返回值eax进行检查。如果eax小于0,则将eax的相反数(即绝对值)存到errno全局变量中,同时将eax值置为-1,这时封装函数返回-1;如果eax大于等于0,则封装函数返回eax的值。
参考资料:
* Advanced Programming in the UNIX Environment: Second Edition. By W. Richard Stevens, Stephen A. Rago.
* Understanding the Linux Kernel. By Daniel P. Bovet, Marco Cesati.
* Kernel 2.6.9源码。