编程语言和操作系统之间的桥梁,也就是我们所说的系统调用。今天来聊聊,系统调用的基础概念。
什么是系统调用
我们在了解系统调用之前,需要对操作系统有个简单的了解。操作系统(operating system,OS)是资源的管理器,也是计算机系统的内核与基石,其管理的资源都是经过了抽象。而对计算机来说,资源是硬件信息:CPU、RAM 内存、I/O 设备,以及进一步抽象的软件资源,如进程。
为什么说操作系统是对资源进行了抽象呢?因为操作不方便、操作不安全,我们平时接触到的不是直接的硬件,比如磁盘操作,不会去操作扇区(嵌入式系统除外)。而我们所面对的都是这些:
- 磁盘抽象:文件夹
- 内存抽象:虚拟内存
- CPU 抽象:时间片
有了操作系统,我们对计算机的调度还是不够的。说白了,操作系统也是一个应用程序,底层还是一堆代码和汇编指令。这时候,我们需要由硬件提供支持,在应用和操作系统之间进行一层或多层隔离。
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}
这段代码的调用规约如下:
这里也说明一下什么是寄存器。寄存器,是CPU 内部的特殊存储单元。一般是用二极管做的,价格比较高,数量比较少。又因为寄存器少,所以有具体的名字。
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 寄存器中。