第五章 系统调用
5.1 与内核通信
系统调用在用户空间进程和硬件设备之间添加了一个中间层。该层主要作用有三个。
- 它为用户空间提供了一种硬件的抽象接口。
- 系统调用保 证了系统的毡定和安全。
- 在第 3 章中曾经提到过,每个进程都运行在虚拟系统中,而在用户空间和系统的其余部分提供这样一层公共接口,也是出于这种考虑。
在 Linux 中,系统调用是用户空间访问 内核的唯一手段:除异常和陷入外,它们是内核唯一的合法入口。
5.2 API, POSIX 和 C 库
应用程序通过在用户空间实现的应用编程接口(API)而非直接通过系统调用来编程。
POSIX是应用编程接口的一个国际标准,C库提供了POSIX的绝大部分API。
POSIX、API、C库和系统调用之间的关系图
5.3 系统调用
要访问系统调用(在 Linux 中常称作 syscall),通常通过 C 库中定义的函数调用来进行。
5.3.1 系统调用号
- 在 Linux 中,每个系统调用被赋予一个系统调用号。这样,通过这个独一无二的号就可以关 联系统调用。当用户空间的进程执行一个系统调用的时候, 这个系统调用号就用来指明到底是要 执行哪个系统调用:进程不会提及系统调用的名称。
- 系统调用号相当重要, 一旦分配就不能再有任何变更,否则编译好的应用程序就会崩溃。
- 内核记录了系统调用表中的所有已注册过的系统调用的列表,存储在 sys_call_ table 中。
- 这个表为每一个有放的系统调用指定了唯一的系统调用号。
5.3.2 系统调用的性能
Linux 系统调用比其他许多操作系统执行得要快。 Linux很短的土下文切换时间是一个重要原因,迸出内核都被优化得简洁高效。另外一个原因是系统调用处理程序和每个系统调用本身也 都非常简洁。
5.4 系统调用处理程序
- 因为用户控件不能直接执行内核代码,需要切换内核态,它需要用某种方式告知内核,自己需要执行一个系统调用,请求切换到内核态,这个通知内核的机制是软中断。
- 在 x86 系统上预定义的软中断是 中断号 128,通过 int$0x80 指令触发该中断。
- 最近, x86 处理器增加了一条叫做 sysenter 的指令。与int中断指令相比,这条指令提供了更快、 更专业的陷入内核执行系统调用的方式。
5.4.1 指定恰当的系统调用
因为所有的系统调用陆人内核的方式都一样,所以仅仅是陷入内核空间是不够的。因此必须 把系统调用号一并传给内核。在 x86 上,系统调用号是通过 eax 寄存器传递给内核的。
5.4.2 参数传递
参数传递最简单的办法是像传递系统调用号一样,把这些参数也放在寄存器里。在X86-32系统上,ebx、ecx、edx、esi、edi按顺序存放前五个参数。需要六个或六个以上参数时应用一个单独的寄存器存放指向所有这些参数在用户空间地址的指针。
5.5 系统调用的实现
决定系统调用的用途
- 明确新系统调用的参数、返回值和错误码,系统调用的接口力求简洁
- 很多系统调用提供了标志参数以确保向前兼容。标志并不是用来让单个系统调用具有多个不同的行别行为,为了即使增加新的功能和选项,也不破坏向后兼容或不需要增加新的系统调用
- 确保系统调用的可移植性和健壮性
5.5.2 参数验证
- 与进程相关的函数必须检查提供的PID是否有效
- 最重要的一种检查就是检查用户提供的指针是否有效。内核必须保证:
-
- 指针指向的内存区域属于用户空间
- 指针指向的内存区域在进程的地址空间里,进程决不能绕过内存访问限制
- 进程不能绕过内存访问限制
5.6 系统调用上下文
- 内核在执行系统调用的时候处于进程上下文
- current指针指向当前任务,即引发系统调用的那个进程
- 在进程上下文中,内核可以休眠并且可以被抢占
- 当系统调用返回的时候,控制权仍在system_call()中,它最终会负责切换到用户空间,并让用户进程继续执行下去
5.6.1 绑定一个系统调用的最后步骤
- 首先在系统调用表的最后加入一个表项
- 对于所支持的各种体系结构,系统调用号都必须定义于<asm/unistd.h>中(系统调用号是专属于体系结构ABI(应用程序二进制接口)的部分)
- 系统调用必须被编译进内核映象(不能被编译成模块)。比如sys.c,它包含了各种各样的系统调用
5.6.2 从用户空间访问系统调用
- 用户程序通过包含标准头文件并和C库链接,就可以使用系统调用
- 对于每个宏来说,都有2+2*n个参数
-
- 第一个参数对应着系统调用的返回值类型
- 第二个参数是系统调用的名称
- 在以后是按照系统调用参数的顺序排列的每个参数的类型和名称
5.7 小结
在本章,我们描述了系统调用到底是什么,它们与库函数和应用程序接口(API)有怎样的 关系。然后,我们考察了Linux内核如何实现系统调用,以及执行系统调用的连锁反应 陷入内核,传递系统调用号和参数,执行正确的系统调用函数,并把返回值带回用户空间。