系统调用
在现代的操作系统中,程序运行的时候,本身是没有权利访问系统资源的。而且,有些行为,应用程序不借助操作系统是无法办到的。
为此,操作系统会提供一套接口,以供应用程序使用,这些接口往往通过中断实现。
Linux使用
0x80
号中断作为系统调用的入口
Linux系统调用
在x86体系下,系统调用通过0x80
中断完成,各个寄存器用户传递参数。
eax = 1; //退出进程
eax = 2; //创建进程
eax = 3; //读取文件或I/O
eax = 4; //写文件或I/O
每个系统调用都对应内核源代码的一个函数,都以sys_
开头。下表是一些库函数在Linux下对应的系统调用。
系统调用的弊端及解决
大家想一些,既然有了系统调用,为什么我们还需要库函数呢?
其实,大部分操作系统的系统调用都有两个特点:
- 使用不便:系统调用接口过于原始,没有进行良好的包装,使用不便
- 各个系统之间的系统调用不兼容。
这时,万能法则就可发挥它的作用了。
增加一个中间抽象层!
万能法则:解决计算机的问题可以通过增加层来实现
这样的抽象层可以保持这样的特点:
- 使用简便
- 形式统一
系统调用原理
特权级与中断
我们知道,现代的CPU可以在多种截然不同的特权级别下执行命令。而在现代操作系统中,有两种特权级别:用户模式和内核模式,也称为用户态、内核态。
一般来说,运行在高特权级别的代码是可以将自己降至低特权级别的,但反过来就不可以了。
系统调用是运行在内核态的,而应用程序基本都是运行在用户态的。
这样就会带来一个问题:用户态程序是如何运行内核态代码的呢?
中断!
中断是一个硬件或软件发出的请求,要求CPU暂停当前的工作转手去处理更加重要的事情。
中断一般具有两个属性:
- 中断号
- 中断处理程序
不同的中断具有不同的中断号,每个中断号对应一个中断处理程序。在内核中,有一个数组称为中断向量表,这个数组的第n项包含了指向第n个中断处理程序的指针。
当一个中断到来时,CPU会暂停当前执行的代码,根据中断号,找到中断处理程序并执行。当执行完后,CPU会将继续执行之前的代码。
由于中断号是有限的,操作系统会用一个或少数几个中断号来对应所有的系统调用。
Linux通过
0x80
中断来触发所有的系统调用
系统调用号通过eax传入,用户将系统调用号放入eax,然后使用0x80
中断。
系统调用的实现
当用户调用某个系统调用时,会执行一段汇编代码保存现场以便恢复。接着才会切换到内核态。
在实际执行中断所对应的函数之前,CPU还会进行栈的切换。
在Linux中,内核态和用户态是不同的栈,两者各自负责各自的函数调用
当由用户栈切换到内核栈的时候会发生以下行为:
- 保存当前用户栈
esp
及页ss
的值 - 切换到内核态
- 恢复
esp
和页ss
的值
当然除了切换到内核态,还会自动完成以下事件:
- 找到当前进程的内核栈
- 在内核栈中压入用户态的寄存器
SS、ESP、EFLAGS、CS、EIP
总结
所以当调用一个库函数的时候,会进行下图的行为:
参考文献
[1] 俞甲子 石凡 潘爱明.程序员的自我修养.电子工业出版社,2009.4.