《Linux 内核设计与实现》读书笔记-系统调用
system call 三个主要目的:
- 为硬件提供一层抽象,比如为读写不同类型的文件、磁盘、设备提供统一的接口
- 确保系统的安全性和稳定性,方便控制根据用户和权限控制资源的访问
- 允许用户空间进程使用 virtualized system,比如使用 virtual memory 和 multitasking 而无需关心 kernel 的实现
我们可以使用 strace
来跟踪系统调用,一个使用例子如下所示
1 | strace -o tmp echo "hello" |
可以看到上面的例子中列出了 echo "hello"
命令调用了 openat
, fstat
, mmap
, write
, close
等系统调用。
执行流程
用户进程想要使用系统调用时,会引发 trap(int 80),切换到内核态,然后根据其 exception vector 找到相应的 exception handler 执行。如果是系统调用,会调用 syscall()
,然后根据系统调用号,到系统调用表中找到相应的系统调用执行。
每个系统调用都有一个系统调用号,用于确定特定的系统调用,系统调用号存储在 eax
寄存器。系统还维护了一张系统调用表(system call table) sys_call_table
,里面注册了一系列的系统调用函数,可以通过系统调用号来引用某个系统调用
在系统调用中,前五个参数分别存储在 ebx
, ecx
, edx
, esi
, edi
,超过部分会有一个寄存器存储一个指针,指向用户空间存储参数的内存地址。
在使用系统调用参数前,我们要确保参数是有效、合法且正确的,不然恶意用户可能传递恶意参数来控制内核,因此在使用前需要对参数进行校验,特别是对指针的校验,校验其是否属于该进程的用户空间的地址空间(防止读取 kernel data 或者其他进程的 data)、是否拥有读、写或执行的权限。同时,kernel 不能盲目地访问用户空间地址,为此 Linux 提供了两个方法用于从用户空间复制数据和复制数据到用户空间
1 | copy_to_user(dst, src, sz); |
在 xv6 中,调用进入内核态的指令 ecall
,他会执行预先写好的汇编程序(在 trampoline
中存放着 uservec
方法),这个方法会保存用户寄存器、加载内核栈指针并从用户页表切换到了内核页表。这些内容都是存储在紧跟着 trampoline
下的一个 trapframe page
中的。保存完所有内容后,就跳转到 usertrap()
函数,根据 trap 的原因,执行相应的内容。如果是系统调用,就会执行 system()
方法,如果是设备中断,就会进行相应的处理。执行完成后会通过 usertrapret
返回用户态
如何实现一个系统调用?由于中断与 系统调用的机制类似,所以在有的操作系统中,中断与系统调用采用同一机制实现
最后,系统还提供了一个 capable()
方法来校验用户是否拥有调用的权限,比如 capable(CAP_SYS_NICE)
检查是否拥有修改 nice value 的权限
- 编写系统调用
- 向系统调用表中添加一个 entry
- 定义系统调用号(
<asm/unistd.h>
) - 编译进内核
- 可以在用户空间使用
_syscalln(ret_type, name, fir_type, fir_arg, ...)
调用自己定义的系统调用
1 | _syscall0(type,name) |