系统调用
在操作系统中,内核提供了用户进程与内核进行交互的一组接口。
与内核通信
系统调用在用户空间与硬件设备之间添加了一个中间层,该层主要有三个作用:
- 提供硬件的抽象接口,这样用户就不需要理会硬件类型;
- 保障系统的稳定和安全,内核可以基于权限等规则去对用户的访问进行裁决;
- 因为每个进程都运行在虚拟内存中,如果允许用户进程随意访问硬件,就无法实现多任务和虚拟内存了;
API, POSIX和C库
API定义了一组应用程序使用的编程接口,它可以是一个系统调用,也可以是通过多个系统调用来实现。C库实现了Unix系统的主要API,包括标准C库函数和系统调用接口。
系统调用
通常我们会通过C库中定义的函数来调用系统调用(syscall)。系统调用往往具有一个明确的操作,例如getpid()系统调用,它会返回当前进程,在内核中的实现为:
1 | SYSCALL_DEFINE0(getpid) |
函数声明中的asmlinkage为一个编译指令,通知编译器仅仅从栈中提取参数,其次,为了保证32位和64位的兼容,应该返回long(调用失败返回负数,返回0通常表明成功)。另外,在调用出现错误的时候,C库会把错误码写入errno这个全局变量。
系统调用号
在Linux中,每个系统调用都被赋予一个系统调用号,而且系统调用号不能变更,也不能删除。另外,为了避免一个系统调用被删除或者出现问题后不可用,Linux用一个“未实现”的系统调用sys_ni_syscall()来填补空缺。
内核在arch/i386/kernel/syscall_64.c中为已经注册过的系统调用表存储系统调用号——sys_call_table。
系统调用的性能
Linux系统调用通常非常快:
- 上下文切换时间较短;
- 操作本身比较简洁;
系统调用处理程序
由于用户空间的程序无法直接执行内核代码,所以需要某种机制保证用户去通知内核执行某个系统调用。
这个机制是用软中断实现的,通过引发一个异常(int $0x80指令)使得系统切换到内核态去执行异常处理程序(第128号异常处理程序),而这个异常处理程序实际上就是系统调用处理程序——system_call()。
指定恰当的系统调用
刚刚的操作只是切换到内核态,但我们仍然需要告诉内核用户进程的系统调用号,在x86里,该系统调用号是通过寄存器eax传递的。在陷入内核态之前将系统调用号存放到eax里,这样就可以在系统调用程序运行的时候拿到数据。
在拿到系统调用号后,system_call()就会将系统调用号和NR_syscalls作比对,检查有效性。
参数传递
多数的系统调用需要一些外部的参数输入,而这些参数的输入通常也是存放在寄存器里传递的,例如前五个参数通过ebx,ecx,edx,esi和edi这五个寄存器传递,如果参数超过5个,就存放一个指向参数所在用户空间的地址。
返回值也是通过寄存器eax传递。
系统调用上下文
内核在执行系统调用的时候处于进程的上下文,并且在进程上下文中,内核可以休眠且可以被抢占。