这篇文章介绍什么是 kernel space 和 user space,以及系统调用(System call,以下会用 syscall表示)。

为什么要分成 kernel space 和 user space 呢?

这要对系统对进程的抽象说起,每一个进程看到的内存空间都从固定的位置开始,main() 总是从 0x4005db 开始,stack 也总是从特定的位置开始。那么不同进程使用的内存都是重叠的吗?

当然不是。这就是系统对进程的抽象。每个进程只能看到自己的内存,MMU(内存管理单元)将解决每个进程看到的虚拟内存和实际物理内存的对应(内存管理是一个很大的话题,这里只是简单的描述,有关内存管理有很多资料可以参考,这里提供一个有趣的文章:Virtual Memory With 256 Bytes of RAM – Interactive Demo)。不仅仅是内存,每个进程只能使用有限的资源,而 kernel 可以使用所有的资源。当 user space 需要使用硬件资源时,user space 通过 syscall 告诉 kernel space,kernel space 来完成调用。

这样做的目的是将资源隔离开,确保 user space 崩溃不会影响 kernel space;以及限制 user space 所能做的事情,不能让程序可以随意进入 kernel space 进行任意的操作。怎么做到的呢?当计算机启动的时候,CPU 会进入 ring0,即特权状态,在一个地址设置好 syscall 的对应关系(这个对应关系可以在这里查),比如 1 对应的就是 write() syscall,然后 CPU 退出到 ring3,即 user space。再次进入 ring0 的唯一方法就是通过 syscall。syscall 要求传入一个 int 表示要调用哪一个 syscall,这样,kernel 就可以控制所能执行的动作,即只能执行事先定义好的 syscall mapping。

Syscall 和一般的函数调用是不一样的,这一点读者应该已经明白了。因为在程序内部,可以使用 stack 或内存在调用之间传值,但是 user space 和 kernel space 是隔离的(当然也可以通过共享的内存传值),stack 不是共用的。如果写一个简单的 write() 程序反编译,可以看到 syscall 的汇编代码是 mov exa, 1 syscall ,其中1就是前面我们说的 syscall mapping,即 write(),然后 syscall 是 user space 向 kernel space 发出的软中断,进入 kernel space,kernel 会检查 exa 寄存器的值,找到对应的函数执行。(不同的架构实现会有所不同)

简单来说,kernel space 和 user space 是两个世界,syscall 就是连接这两个世界的桥梁。虽然这个桥梁我们一般不会直接使用,而是通过 glibc,glibc 是 syscall 的一个 wrapper,让我们 call 起来更加简单方便。比如说 printf 函数,其实就是 write() 的一个 wrapper。

通过 man 2 write 可以看到 write() 的原型如下:

C

1

2

3

       #include <unistd.h>

       ssize_t write(int fd, const void *buf, size_t count);

所以我们可以这么调用,向屏幕打印字符:

C

1

2

3

4

void main(){

  // 1是stdout

  write(1, "hello syscall!", 20);

}

有关 syscall 大体就是这样。以上是我的理解,如果有错误欢迎交流。下面再介绍一些相关的工具。

上面那张截图 radare2 的界面,一个反编译工具。使用方式是 radare2 -d ./a.out ,然后在s sym.main ,设置断点,可以看到 syscall 是如何执行的。

通过 man syscalls 可以查看系统提供的 syscalls. strace 工具可以将一个进程使用的 syscall 输出到 stderr,包括调用的参数,和返回值。比如查看 ls 使用的系统调用:

Shell

1

2

3

4

5

6

7

8

[vagrant@centos7 ~]$ strace ls                                                                                                                                               execve("/usr/bin/ls", ["ls"], [/* 25 vars */]) = 0

brk(NULL)                               = 0x1574000                                                                                                                          mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fde7d373000

access("/etc/ld.so.preload", R_OK)      = -1 ENOENT (No such file or directory)

open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3

fstat(3, {st_mode=S_IFREG|0644, st_size=25207, ...}) = 0

mmap(NULL, 25207, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7fde7d36c000

close(3)                                = 0

# 以下省略

-c 参数很有用,可以显示每次 call 花费的时间,call 的次数,占用的总时间等,是一个 perf 的好工具。

Shell

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

[vagrant@centos7 /]$ strace -c ls

bin  boot  dev  etc  home  lib  lib64  media  mnt  opt  proc  root  run  sbin  srv  sys  tmp  usr  var

% time     seconds  usecs/call     calls    errors syscall

------ ----------- ----------- --------- --------- ----------------

24.29    0.000034          17         2           statfs

  9.29    0.000013           7         2           munmap

  7.86    0.000011           1        13           close

  6.43    0.000009           9         1           write

  6.43    0.000009           0        27           mmap

  5.71    0.000008           4         2           getdents

  5.00    0.000007           1        10           open

  5.00    0.000007           4         2           ioctl

  4.29    0.000006           6         1           stat

  3.57    0.000005           0        11           fstat

  3.57    0.000005           0        18           mprotect

  3.57    0.000005           2         3           brk

  3.57    0.000005           3         2         1 access

  2.86    0.000004           2         2           rt_sigaction

  2.86    0.000004           4         1           openat

  1.43    0.000002           2         1           rt_sigprocmask

  1.43    0.000002           2         1           getrlimit

  1.43    0.000002           2         1           set_tid_address

  1.43    0.000002           2         1           set_robust_list

  0.00    0.000000           0         8           read

  0.00    0.000000           0         1           execve

  0.00    0.000000           0         1           arch_prctl

------ ----------- ----------- --------- --------- ----------------

100.00    0.000140                   111         1 total

参考资料:

  1. Linux System Programming
  2. Youtube: Syscalls, Kernel vs. User Mode and Linux Kernel Source Code – bin 0x09
  3. jvns 的这篇漫画:https://drawings.jvns.ca/userspace/