编程语言和操作系统之间的桥梁,也就是我们所说的系统调用。今天来聊聊,系统调用的基础概念。

什么是系统调用

我们在了解系统调用之前,需要对操作系统有个简单的了解。操作系统(operating system,OS)是资源的管理器,也是计算机系统的内核与基石,其管理的资源都是经过了抽象。而对计算机来说,资源是硬件信息:CPU、RAM 内存、I/O 设备,以及进一步抽象的软件资源,如进程。

为什么说操作系统是对资源进行了抽象呢?因为操作不方便、操作不安全,我们平时接触到的不是直接的硬件,比如磁盘操作,不会去操作扇区(嵌入式系统除外)。而我们所面对的都是这些:

  • 磁盘抽象:文件夹
  • 内存抽象:虚拟内存
  • CPU 抽象:时间片

有了操作系统,我们对计算机的调度还是不够的。说白了,操作系统也是一个应用程序,底层还是一堆代码和汇编指令。这时候,我们需要由硬件提供支持,在应用和操作系统之间进行一层或多层隔离。

image-20210906210117219

CPU 已经为操作系统提供了特殊的安全支持——分级保护域(protection ring)。操作系统内核运行在特殊模式下,即图中的 ring-0 ,而应用运行在 ring-3,但权限被严格限制。因此,在代码中我们没办法直接去调用系统资源,就需要操作系统帮助我们去调用,并把相应的操作抽象成 API 来供我们使用。

Intel64 有四个特权级别,不过实际上只用到了其中两个 ring-0 和 ring-3。ring-1 和 ring-2 本来计划是为了驱动程序和 OS 服务用,不过流行的 OS 们都没有接受这个方案。

说到这里,答案已经揭晓。系统调用,是操作系统内核为应用提供的 API。可以理解为内核为应用提供服务,操作系统就位我们的上层应用程序提供了一系列“标准库”。比如我们常见的后端服务:APP 发起请求 request → 操作系统 Operating System 接收、处理并响应 → APP 接收 response。

对于应用来说,系统调用可以实现超出自己能力以外的事情。

那么 Go 语言中的系统调用是怎样的呢?在此之前,还需要提及 Go 语言调用规约。我们在做函数调用的时候没有使用寄存器,而是将参数都放在栈上。但在其他编程语言中做参数传递和函数调用都是用到了寄存器。举个例子:

1func hello() {
2    x, y, z := 1, 3, 3, 3
3    a, b, c := multi(x, y)
4}
5
6func multi(x, y int) (a, b, c int) {
7    r, s, t := 1, 2, 3
8    return x+1, x+2, x+3
9}

这段代码的调用规约如下:

未命名文件 (5)

这里也说明一下什么是寄存器。寄存器,是CPU 内部的特殊存储单元。一般是用二极管做的,价格比较高,数量比较少。又因为寄存器少,所以有具体的名字。

CPU 与内存 (1)

CPU 包括了:Control Unit 控制单元,ALU 算术逻辑单元,Registers 寄存器。这些都封装在 CPU 内部的,而内存一般是在外面。寄存器里面可以防止一些地址,通过地址找到位置。

大概知道了寄存器的概念之后,我们再聊了解一下系统调用的调用规约。系统调用有一套自己的调用规约,需要使用寄存器。和 C 语言的调用规约相类似:

arch syscall NR return arg0 arg1 arg2 arg3 arg4 arg5
arm r7 r0 r0 r1 r2 r3 r4 r5
arm64 x8 x0 x0 x1 x2 x3 x4 x5
x86 eaX eaX ebx ecx edx esi edi ebp
x86_64 rax rax rdi rsi rdx r10 r8 r9

来源:https://chromium.googlesource.com/chromiumos/docs/+/master/constants/syscalls.md#x86_64-64_bit

我们大多都是在 x86_64 位平台去做开发、写代码,所以我们只需要看最后一行,看它对应的函数传递规则就好。我们每次做具体的系统调用时,都需要传参,arg0 是第一个参数,arg5 是第六个参数。也就是说做系统调用时只能传递 6 个参数。Linux 平台中有 300 多个系统调用,也不没有让传递参数超过 6 个。

往 rax 寄存器中存放具体系统调用的编号,内核再通过计算 6 个参数就能知道我们调用的是哪个系统调用,最后把返回值也放在 rax 寄存器中。