系统调用

在现代的操作系统中,程序运行的时候,本身是没有权利访问系统资源的。而且,有些行为,应用程序不借助操作系统是无法办到的。

为此,操作系统会提供一套接口,以供应用程序使用,这些接口往往通过中断实现。

Linux使用0x80号中断作为系统调用的入口

Linux系统调用

在x86体系下,系统调用通过0x80中断完成,各个寄存器用户传递参数。

eax = 1;		//退出进程
eax = 2;		//创建进程
eax = 3;		//读取文件或I/O
eax = 4;		//写文件或I/O

每个系统调用都对应内核源代码的一个函数,都以sys_开头。下表是一些库函数在Linux下对应的系统调用。

系统调用的弊端及解决

大家想一些,既然有了系统调用,为什么我们还需要库函数呢?

其实,大部分操作系统的系统调用都有两个特点:

  1. 使用不便:系统调用接口过于原始,没有进行良好的包装,使用不便
  2. 各个系统之间的系统调用不兼容

这时,万能法则就可发挥它的作用了。
增加一个中间抽象层!

万能法则:解决计算机的问题可以通过增加层来实现

这样的抽象层可以保持这样的特点:

  • 使用简便
  • 形式统一

系统调用原理

特权级与中断

我们知道,现代的CPU可以在多种截然不同的特权级别下执行命令。而在现代操作系统中,有两种特权级别:用户模式内核模式,也称为用户态、内核态

一般来说,运行在高特权级别的代码是可以将自己降至低特权级别的,但反过来就不可以了。

系统调用是运行在内核态的,而应用程序基本都是运行在用户态的

这样就会带来一个问题:用户态程序是如何运行内核态代码的呢?

中断!
中断是一个硬件或软件发出的请求,要求CPU暂停当前的工作转手去处理更加重要的事情。

中断一般具有两个属性:

  • 中断号
  • 中断处理程序

不同的中断具有不同的中断号,每个中断号对应一个中断处理程序。在内核中,有一个数组称为中断向量表,这个数组的第n项包含了指向第n个中断处理程序的指针。

当一个中断到来时,CPU会暂停当前执行的代码,根据中断号,找到中断处理程序并执行。当执行完后,CPU会将继续执行之前的代码。

由于中断号是有限的操作系统会用一个或少数几个中断号来对应所有的系统调用

Linux通过0x80中断来触发所有的系统调用
系统调用号通过eax传入,用户将系统调用号放入eax,然后使用0x80中断。

系统调用的实现

当用户调用某个系统调用时,会执行一段汇编代码保存现场以便恢复。接着才会切换到内核态。

在实际执行中断所对应的函数之前,CPU还会进行栈的切换

在Linux中,内核态和用户态是不同的栈,两者各自负责各自的函数调用

当由用户栈切换到内核栈的时候会发生以下行为:

  1. 保存当前用户栈esp及页ss的值
  2. 切换到内核态
  3. 恢复esp和页ss的值

当然除了切换到内核态,还会自动完成以下事件:

  1. 找到当前进程的内核栈
  2. 在内核栈中压入用户态的寄存器SS、ESP、EFLAGS、CS、EIP

总结

所以当调用一个库函数的时候,会进行下图的行为:

参考文献

[1] 俞甲子 石凡 潘爱明.程序员的自我修养.电子工业出版社,2009.4.