system call 三个主要目的:

  • 为硬件提供一层抽象,比如为读写不同类型的文件、磁盘、设备提供统一的接口
  • 确保系统的安全性和稳定性,方便控制根据用户和权限控制资源的访问
  • 允许用户空间进程使用 virtualized system,比如使用 virtual memory 和 multitasking 而无需关心 kernel 的实现

我们可以使用 strace 来跟踪系统调用,一个使用例子如下所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
strace -o tmp echo "hello"

tail -n 5 tmp

openat(AT_FDCWD, "/usr/lib/locale/C.UTF-8/LC_CTYPE", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=201272, ...}) = 0
mmap(NULL, 201272, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7ff1992de000
close(3) = 0
fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(0x88, 0), ...}) = 0
write(1, "hello\n", 6) = 6
close(1) = 0
close(2) = 0
exit_group(0) = ?
+++ exited with 0 +++

可以看到上面的例子中列出了 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
2
copy_to_user(dst, src, sz);
copy_from_user(dst, src, sz);

在 xv6 中,调用进入内核态的指令 ecall,他会执行预先写好的汇编程序(在 trampoline 中存放着 uservec 方法),这个方法会保存用户寄存器、加载内核栈指针并从用户页表切换到了内核页表。这些内容都是存储在紧跟着 trampoline 下的一个 trapframe page 中的。保存完所有内容后,就跳转到 usertrap() 函数,根据 trap 的原因,执行相应的内容。如果是系统调用,就会执行 system() 方法,如果是设备中断,就会进行相应的处理。执行完成后会通过 usertrapret 返回用户态

由于中断与 系统调用的机制类似,所以在有的操作系统中,中断与系统调用采用同一机制实现

如何实现一个系统调用?

最后,系统还提供了一个 capable() 方法来校验用户是否拥有调用的权限,比如 capable(CAP_SYS_NICE) 检查是否拥有修改 nice value 的权限

  1. 编写系统调用
  2. 向系统调用表中添加一个 entry
  3. 定义系统调用号(<asm/unistd.h>)
  4. 编译进内核
  5. 可以在用户空间使用 _syscalln(ret_type, name, fir_type, fir_arg, ...) 调用自己定义的系统调用
1
2
3
4
5
6
7
_syscall0(type,name)
_syscall1(type,name,type1,arg1)
_syscall2(type,name,type1,arg1,type2,arg2)
_syscall3(type,name,type1,arg1,type2,arg2,type3,arg3)
_syscall4(type,name,type1,arg1,type2,arg2,type3,arg3,type4,arg4)
_syscall5(type,name,type1,arg1,type2,arg2,type3,arg3,type4,arg4,type5,arg5)
_syscall6(type,name,type1,arg1,type2,arg2,type3,arg3,type4,arg4,type5,arg5,type6,arg6)