# 6.4软件中断与系统调用

## 阅读入口

* [系统调用](/knowledge/cs/operating-systems/system-call/)

## 特权级与系统调用

x86在硬件层面把代码的运行权限分成4级，编号0到3，通常称为保护环。当前代码所在的环记在段寄存器的CPL（当前特权级）字段中，CPU执行指令和访问内存时都按它检查权限。

| 特权级           | 常称  | 可执行的指令        | 内存访问       | 典型代码                |
| :------------ | :-- | :------------ | :--------- | :------------------ |
| Ring 0        | 内核态 | 包括特权指令在内的全部指令 | 由页表及硬件配置控制 | 内核核心代码：调度、内存管理、驱动   |
| Ring 3        | 用户态 | 只有非特权指令       | 由映射和权限控制   | 浏览器、编辑器、Python解释器   |
| Ring 1、Ring 2 | —   | 介于两者之间        | 介于两者之间     | 原本为驱动等中间层预留，主流系统不使用 |

运行在Ring 0的代码处于**内核态**，运行在Ring 3的代码处于**用户态**。Ring 1和Ring 2的设计初衷是给设备驱动一类中间层使用，但现代Linux与Windows基本只用Ring 0和Ring 3，把需要特权的操作全部收进内核。

部分需要较高特权级才能执行的指令称为**特权指令**。修改页表、用`cli`关中断、用`in`和`out`访问I/O端口都属于这一类。用户态执行特权指令不是被忽略，而是由CPU报错，交给内核处理。

[来源：用户态与内核态](/knowledge/cs/operating-systems/cpu-privilege/#definition)

应用通常通过系统调用请求内核提供文件、网络等服务。具体接口和进入内核的机制取决于操作系统与体系结构。这种“用户态程序主动请求内核执行特权操作”的机制就是**系统调用**。内核给每项服务编一个**系统调用号**，调用方把号和参数放进约定的寄存器，内核据此分发到对应的处理函数。

系统调用是同步事件：它在执行到陷入指令的那一刻发生，与外部设备是否就绪无关。32位x86 Linux用`int 0x80`陷入，x86-64用`syscall`陷入。

[来源：系统调用](/knowledge/cs/operating-systems/system-call/#definition)

教材用32位x86 Linux的`int 0x80`讲这段内容，保护环的划分与知识点一致。读定义时先分清“内核态能做什么”和“用户态被禁了什么”，后面五步流程才有落点。

## 一次系统调用的流程

以32位x86 Linux的`int 0x80`为例，寄存器的分工是固定的：

| 寄存器                                 | 用途                         |
| :---------------------------------- | :------------------------- |
| `EAX`                               | 系统调用号，例如`exit`为1，`write`为4 |
| `EBX`、`ECX`、`EDX`、`ESI`、`EDI`、`EBP` | 依次存放最多6个参数                 |
| `EAX`（返回时）                          | 返回值；失败时是负的错误码              |

1. **准备参数**

   应用程序或C库把系统调用号写入`EAX`，把参数写入`EBX`、`ECX`等寄存器。以`write`为例，三个参数分别是文件描述符、缓冲区地址和要写入的字节数。

2. **触发陷入**

   执行`int 0x80`，产生第128号中断（十六进制`0x80`等于十进制128）。这个中断在IDT（中断描述符表）中的门允许Ring 3的代码进入，所以用户态可以合法发起。

3. **切换特权级并跳转**

   CPU按IDT找到0x80对应的处理程序，保存用户态的`CS`、`EIP`、`EFLAGS`和栈指针，把CPL从3改为0，跳转到内核预定义的`system_call`入口。

4. **内核分发并执行**

   内核按`EAX`中的调用号查系统调用表，进入`sys_write`等具体实现。执行期间内核拥有完整的硬件控制权，可以访问设备寄存器和内核数据结构。

5. **返回用户态**

   内核把返回值或错误码写回`EAX`，执行`iret`恢复之前保存的`CS`、`EIP`、`EFLAGS`和栈，CPL回到3。程序从`int 0x80`的下一条指令继续执行。

### 现代指令

`int 0x80`要走完整的中断门流程：查IDT、检查门权限、保存现场，通常需要更多通用中断门处理步骤。AMD64的`syscall`和Intel的`sysenter`通过专门配置的寄存器直接进入内核入口，采用不同的专用入口机制。它们做的仍然是同一件事：用户态按约定放好调用号和参数，一条指令陷入，内核执行，再返回。

x86-64的约定与32位不同：调用号放在`RAX`，参数依次放在`RDI`、`RSI`、`RDX`、`R10`、`R8`、`R9`，返回值放在`RAX`，可使用`sysret`等返回路径，实际返回方式取决于内核实现。

> [!NOTE]
> 系统调用进入内核与进程或线程切换是不同概念。调用过程中若需要等待，调度器可以运行其他线程；之后调用线程恢复并返回用户态。

[来源：系统调用](/knowledge/cs/operating-systems/system-call/#flow)

这五步与6.3的中断处理共用“保存现场、恢复现场”的骨架，区别在于入口由程序主动执行指令触发，而不是设备发信号。

## 学习安排

[6.3硬件中断与异常](/courses/cs/csintro/6.3/) 把外部设备的I/O中断、CPU内部的时钟中断归为异步事件，把除零、缺页和系统调用归为同步的异常。这一节接着“陷阱 / 软中断”这一类往下讲，看用户态程序怎样主动进入内核请求服务；[6.5进程](/courses/cs/csintro/6.5/) 中`Running → Blocked`的状态转换，很多就是由系统调用触发的。

读的时候抓住一条主线：权限被切成Ring 0与Ring 3，用户态代码通过陷入指令把受限请求交给内核，内核执行完再返回。32位x86 Linux用`int 0x80`完成这次进入，x86-64使用`syscall`。

## 课程约定

* 32位x86使用`EAX`、`EBX`、`ECX`，x86-64使用`RAX`、`RDI`、`RSI`等；答题时按题目给出的架构选择寄存器名。
* `0x80`是中断向量号，十六进制`0x80`等于十进制128。
* `iret`是从中断处理程序返回用户态的通用指令；`syscall`的返回可使用`sysret`等路径，取决于内核实现。
* “用户态 → 内核态 → 用户态”称为模式切换，它改变CPU的权限级别；进程上下文切换保存和恢复的是执行现场。

## 易错点

* 系统调用不是函数调用。C库的`printf`、`fopen`是用户态函数，真正陷入内核的是它们内部调用的`write`、`open`。
* `int 0x80`是陷入指令，不是系统调用本身；系统调用是这次陷入所请求的服务。
* 用户态执行受限特权指令会触发硬件异常，后续由操作系统处理。判断一条指令是否只能在内核态执行，看它是否修改机器全局状态或直接访问硬件。
* 系统调用进入内核不等于进程切换；调用期间若发生等待，调度器可能切换线程，之后调用线程才恢复。
* 不要把中断都当成系统调用：时钟中断是异步的硬件事件，系统调用是同步的、由指令主动触发的陷入。
