Chapter 5 - 系统调用
Created by : Mr Dk.
2019 / 10 / 18 08:56
Nanjing, Jiangsu, China
5.1 与内核通信
在 用户空间进程 和 硬件设备 之间添加了一个中间层,为用户空间提供了硬件的抽象接口,保证了系统的稳定性和安全。是用户空间访问内核的唯一合法入口。
5.2 API、POSIX 和 C 库
应用程序通过在用户空间实现的 API 而不是直接通过系统调用来编程:
- API 实际上并不需要和内核提供的系统调用对应
- API 可以在各种 OS 上实现,给应用程序提供完全相同的接口
在 Unix 世界中,最流行的 API 是基于 POSIX 标准的,Linux 尽力与 POSIX 和 SUSv3 兼容。Linux 的系统调用像大多数 Unix 系统一样,作为 C 库的一部分提供。
- Mechanism, not policy. (提供机制而不是策略) 是 Unix 文化贯穿的一条设计主线
- What capabilities are to be provided (the mechanism)
- How those capabilities can be used (the policy)
Unix 的系统调用仅抽象用于完成某种确定目的的函数,函数怎么用完全不需要内核关心。
5.3 系统调用
访问系统调用,通常使用 C 库中定义的函数来进行。
- 可能需要 0/1/2 甚至几个参数
- 可能产生一些副作用 (使系统的状态发生改变)
- 通过一个
long
类型的返回值来表示成功或者错误
SYSCALL_DEFINE0(getpid)
{
return task_tgid_vnr(current);
}
SYSCALL_DEFINE0
是一个宏,定义了无参数的系统调用。宏展开后:
asmlinkage long sys_getpid(void)
asmlinkage
是一个编译执行,通知编译器仅从栈中提取函数参数sys_xxx()
是 Linux 中所有系统调用都应该遵守的命名规则
5.3.1 系统调用号
Linux 中每个系统调用都被赋予了一个系统调用号。系统调用号很重要,一旦分配就不能再有变更,不然编译好的程序就会崩溃。系统调用被删除后,调用号也不能被回收,不然编译好的程序可能会调用另一个系统调用。Linux 中有一个 sys_ni_syscall()
,除了返回 -ENOSYS
不做任何其它工作。当系统调用被删除,这个函数负责填补空缺。内核在 sys_call_table
中记录了所有已注册的系统调用。
5.3.2 系统调用的性能
Linux 系统调用比许多其它 OS 执行得快,较短的上下文切换时间是一个重要原因:进出内核都被优化得简洁高效。
5.4 系统调用处理程序
用户空间程序无法直接执行内核代码
- 不能直接调用内核空间中的函数
- 内核驻留在受保护的地址空间上
- 如果进程可以直接读写内核,系统的安全性将不复存在
应用程序应该以某种方式通知系统,促使系统切换到内核态去执行异常处理程序。通知内核是靠软中断实现的:
- 引发一个异常,促使 CPU 进入内核态
- 此时异常处理程序实际上就是系统调用处理程序
x86 系统上系统调用的中断号为 128,通过 INT $0x80
触发中断,导致系统切换到内核态,并执行第 128 号异常处理程序。处理程序名为 system_call()
,与硬件体系结构紧密相关。最近,x86 处理器增加了一条 sysenter
指令,提供更快、更专业的陷入内核执行系统调用的方式。
5.4.1 指定恰当的系统调用
仅陷入内核空间是不够的,需要把系统调用号一并传给内核。在 x86 中,通过 eax
进行传递:在陷入内核空间前,用户空间把相应系统调用的调用号放入 eax
中。system_call()
函数通过将给定的调用号与 NR_syscalls
作比较,检查有效性。由于系统调用表中的表项以 8B 存放,因此:
call *sys_call_table(, %rax, 8)
5.4.2 参数传递
在 x86-32 系统上,ebx、ecx、edx、esi、edi 按顺序存放前五个参数。需要 6 个或以上参数时,应当指定一个寄存器,存放指向所有参数的用户空间地址。给用户空间的返回值也通过寄存器传递(x86 eax)。
5.5 系统调用的实现
5.5.1 实现系统调用
确定每个系统调用的用途。Linux 不提倡多用途的系统调用——ioctl()
。很多系统调用提供了 标志参数 以确保向前兼容,目的并不是让单个系统调用有多个不同行为,而是为了增加新的功能和选项。设计接口尽量多为将来做考虑。
5.5.2 参数验证
系统调用必须仔细检查它们所有的参数是否合法有效。内核需要保证用户空间提供的指针:
- 指向的内存区域属于用户空间
- 指向的内存区域在进程的地址空间内
- 不能绕过内存访问限制
使用 copy_from_user()
和 copy_to_user()
来进行数据传递
5.6 系统调用上下文
内核在执行系统调用的时候处于 进程上下文:
- 内核可以休眠
- 内核可以被抢占
需要保证系统调用是可重入的。系统调用返回时,控制权仍在 system_call()
,由该函数负责切换到用户空间。
5.6.1 绑定一个系统调用的最后步骤
- 在系统调用表的最后加入一项
- 对于所支持的各种体系结构,将系统调用号进行定义
- 系统调用被实现,并被编译进内核映像 (不能被编译为模块)
5.6.2 从用户空间访问系统调用
Linux 提供了一组宏,用于直接对系统调用访问——_syscalln()
- n 从 0 到 6。对于每个宏,都有 2 + 2 × n 个参数:
- 系统调用的返回值类型
- 系统调用名称
- n 个参数的类型和名称
宏会被展开为内嵌汇编的 C 函数。
5.6.3 为什么不通过系统调用的方式实现
建立新的系统调用非常容易,但绝不提倡这么做。