LKD(Linux内核设计与实现) Chapter 5 System Calls

序言

在远古的时候,所有的程序都是对等的,大家运行在相同的CPU权限下。那时候CPU计算能力很弱,也没有多个核可以同时运行多个程序。程序之间基本上平等,大家都排队等待获取CPU执行时间。程序的天性都很纯良,大家只埋头干好两件事,一是自己的手头活,二是应对发生的异常。那是一个美好的世界,即使有某个程序不守规矩,做坏事破坏了整个系统,大不了断电重启,世界可以重来,美好也就恢复了。

但是后来,CPU变得强大了,计算能力突飞猛进,可以有很多核同时运行成百上千的程序。于是乎CPU吼道,“凭什么老是对它掉电重启?”。这个呼声直冲云霄,惊动了人类。人类响应的CPU的呼应,创造出了一种怪物程序,这个怪物非常野蛮,它获得了CPU的特许,运行在最高的权限下,以便帮助CPU控制的所有的资源。其他程序成了奴仆,想要从怪物程序那获取资源,就必须放低姿态,卑躬屈膝得中断自己的执行,把辛苦获得的执行时间交给怪物,换取一点可怜的资源。

若干年后,所有的程序都习惯了这个怪物的存在。怪物也有了新的名字,叫操作系统。有了操作系统,CPU再也不担心运行的程序会作怪了,于是乎CPU可以专心地去发展自己的计算能力,把自己的核数目扩大一倍又一倍。

x86系统调用

以x86系统调用为例,操作系统运行在权限最高的ring 0级别,普通程序运行在ring 3级别。操作系统提供一张表,列出它可以提供的服务。64位系统,这张表在arch/x86/entry/syscalls/syscall_64.tbl,内容如下:

所有的系统调用都提供了一个编号,如read的编号是``,write1等等。

那么内核如何定义一个系统调用呢,以read为例(代码在fs/read_write.c):

SYSCALL_DEFINE3include/linux/syscalls.h定义,最终,宏会被替换成:

asmlinkage修饰符告诉编译器当前函数的所有参数均放在栈上。为什么这么做呢?从用户空间切换到内核的时候,内核本来就是要保存用户空间寄存器的内容保存到栈上,以便稍后还原的,所以那些通过寄存器传递的系统调用参数自然就在栈上了。


  • 文章中涉及的任何代码都参考Linux v4.4
  • Sysenter Based System Call Mechanism in Linux 2.6
  • Anatomy of a system call, part 1
  • Anatomy of a system call, part 2
  • kernel/sys.c可以找到许多系统调用的实现。
  • 一条军规是不要随便添加系统调用,实在不行,把需要用系统调用使用的功能,改用文件系统调用实现,比如
    • 可以添加一个设备节点,然后进行readwriteioctl
    • 如果只是为了吐露信息的话,可以放到sysfs
    • 有些借口,例如信号量,是可以用文件描述符表示的。