Chinaunix首页 | 论坛 | 博客
  • 博客访问: 1638593
  • 博文数量: 197
  • 博客积分: 10046
  • 博客等级: 上将
  • 技术积分: 1983
  • 用 户 组: 普通用户
  • 注册时间: 2006-08-07 12:36
个人简介

在外企做服务器开发, 目前是项目经理, 管理两个server开发的项目。不做嵌入式好久了。

文章分类
文章存档

2011年(2)

2010年(6)

2009年(18)

2008年(30)

2007年(100)

2006年(41)

分类: LINUX

2010-11-01 18:20:53

转载的文章, 出处忘记了。这哥们分析的很透彻,

转到这里, 与大家共享。

 

 



有实际开发经验的人都知道,在操作系统上运行的某个应用程序,如果想完成一些实际有用的功能,必然会用到操作系统提供的接口,这些接口被称为系统调用(System Call)。

由操作系统提供的功能,通常应用程序本身是无法实现的。例如对文件进行操作,应用程序必需通过系统调用才能做到,因为只有操作系统才具有直接管理外围设备的权限。又如进程或线程间的同步互斥操作,也必需经由操作系统对内核变量进行维护才能完成。

应用程序的进程通常在user模式下运行,当它调用一个系统调用时,进程进入kernel模式,执行的是kernel内部的代码,从而具有执行特权指令的权限,完成特定的功能。换句话说,系统调用是应用程序主动进入操作系统内核的入口。

(一)系统调用和库函数的区别

在不同的操作系统上,系统调用可能以不同的编程语言暴露给开发人员。例如,早期操作系统的系统调用以汇编语言表达,而现代操作系统通常以C语言的形式表达系统调用接口,还有可能封装成其它的高级语言。

际上,以某种高级语言(如C)表示的系统调用是对真正系统调用的一个封装,开发人员看到的是库函数的形式,例如LinuxAPIlibc库中实现。虽 然调用一个系统调用与调用一个普通库函数看起来区别不大,但实质上是不同的。例如,在Linux操作系统下,C语言的库函数printf,实际上使用了 write系统调用;而库函数strcpy(字符串拷贝)却没有使用任何系统调用。另外,一个系统的系统调用接口通常是能够完成所有必需功能的最小集合, 可能存在多个库函数对同一个系统调用进行封装。例如,在Linux中,malloccallocfree三个库函数底层都是调用brk系统调用完成 的。

应用程序、库函数和系统调用的关系如下图所示:

(二)系统调用和中断的联系


中断(Interrupt)通常是指在CPU内部或外部发生了某个待处理的事件,从而CPU必需改变当前指令的执行顺序去处理这类事件。在介绍中断和系统调用的关系之前,下面先把中断做一个分类。

中断可以大体分为两大类:

  * Synchronous interrupts
:在CPU内部产生,说这类中断是同步的,意思是中断信号的发射时间一定在当前指令执行结束之后。
  * Asynchronous interrupts
CPU外部的其它硬件产生,说这类中断是异步的,意思是中断信号可以在任意时间发射,与CPU本身的时钟节拍没有关系。

有些技术资料中会将上述两种中断分别称为exceptionsinterrupts

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,简称IDTIDT表共有256项,存放了从中断向量到相应处理例程(interrupt or exception handler)的映射关系。当某个中断发生时,CPUIDT表中查找到相应的处理例程的地址来执行。

(三)内核对于系统调用的处理

在上面一节我们提到,系统调用的处理例程在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_callkernel空间中执行。在执行前,系统调用号和输入参数已经存入了寄存器,这个存入过程由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 = 1fork系统调用号为__NR_fork = 2read系统调用号为__NR_read = 3write系统调用号为__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
源码。

阅读(3719) | 评论(1) | 转发(1) |
给主人留下些什么吧!~~

chinaunix网友2010-11-02 17:13:31

很好的, 收藏了 推荐一个博客,提供很多免费软件编程电子书下载: http://free-ebooks.appspot.com