现代的cpu常常可以在多种截然不同的特权级别下执行指令,在现代操作系统中,通常也具有两种特权级别,分别是用户模式和内核模式,也被称为用户态和内核态,普通程序运行在用户态的模式上,收到了许多限制,因为操作系统将可能产生冲突的系统资源保护起来了,阻止应用程序直接访问,所有有了系统调用。系统调用是用户程序与内核之间的接口。
Linux下系统调用采用了0x80号中断作为系统调用的入口,当用户态的程序需要运行内核态的代码之后,操作系统产生中断将用户态切换到内核态(中断是当硬件或者软件发出的请求,请求CPU暂停当前的工作转手处理更重要的事情),在内核中有一个数组称为中断向量表,这个数组的第n项包含了指向第n号中断的中断处理程序的指针。当中断到来时,CPU会暂停当前的代码,根据中断号,在中断向量表中寻找对应的中断处理程序,并调用,等到中断处理程序执行完成过后,CPU继续执行之前的代码。下图可以解释其中的过程。
接着,以fork为例介绍系统调用的执行流程
1.触发中断
fork()函数是一个对系统调用fork的封装,可以用下列宏来定义
_syscall0(pid_t,fork);
_syscall0是一个宏函数,定义如下
asm的第一个参数是一个字符串,即“int $0x80”就是调用80号中断
“=a”(_res)表示用eax寄存器输出返回值并存储在_res里
“0”指示由编译器选择和输出相同的寄存器(即eax)来传递参数。
ps:可见,如果系统调用只有一个参数,那么参数是由eax寄存器传入的。x86下Linux支持的系统调用参数至多6个,分别使用6个寄存器传递,分别是ebx,ecx,edx,esi,edi,ebp。
2.切换堆栈
当CPU执行到int$0x80时,会保存现场以便恢复,接着会切换到内核态。然后CPU会查找中断向量表的第0x80号元素。切换到内核态的时候就牵扯到了第二步,切换堆栈。切换堆栈时候,CPU自动完成保存现场等信息,在这里就不多赘述了。
3.中断处理程序
在int指令合理的切换了栈之后,程序的流程就切换到了中断向量表中记录的0x80号中断处理程序。
以下是源码中的截取部分
可见,用户调用int 0x80之后,最终执行的函数是system_call,一开始使用宏SAVE_ALL将各种寄存器压栈,然后接下来使用cmp1指令比较eax和nr_syscalls的值。如果系统调用好有效,就会在Linux/arch/i386/kernel/syscall_table.S里找到eax的值对应的系统调用表里的函数的地址,从而实现运行内核代码
运行完成后恢复用户现场,从而切换回用户态,完成系统调用过程。
总结:系统调用是用户和内核交互的接口,其中过程包含了用户态切换到内核态,再由内核态切换回到用户态的过程,因此我们说系统调用时的对CPU的使用效率不高,极大的浪费了两次切换的时间和消耗的资源,还需注意系统调用表和中断向量表的区别!!!
(本文仅代表个人理解,有错误有劳指出)