%%
# 纲要
> 主干纲要、Hint/线索/路标
# Q&A
#### 已明确
#### 待明确
> 当下仍存有的疑惑
**❓<font color="#c0504d"> 有什么问题?</font>**
# Buffer
## 闪念
> sudden idea
## 候选资料
> Read it later
%%
# 内核态与用户态
## CPU 特权级别
CPU 硬件本身具有两种或多种不同的 "**特权级别**" 运行模式(由指令集架构 ISA 规定,通过**控制寄存器中的一个模式位**来控制) [^1]
- **==内核模式==**(Kernel Mode)
- CPU 具有对所有**硬件**的 **==完全访问权==**:可访问任意内存地址,访问任意 CPU 寄存器和外部设备。
- CPU 可执行 ISA 指令集中的 **==所有指令==**。
- **==用户模式==**(User Mode)
- CPU 只能访问 "**受限的内存区域**"——仅可访问**用户空间**,**不能访问内核地址空间**,否则**触发"异常"**。
- CPU 不允许执行 "**特权指令**",
> [!example] x86-64 架构中,CPU 有四个特权级(0~3): CPL =0 是内核模式,CPL = 3 是用户模式
> [!info] 基于 CPU 的不同特权级, OS 对应实现了 "**内核态**" 与 "**用户态**" ——将"**OS 内核"** 与 "**普通应用程序**" 进行隔离
>
> - **操作系统内核**运行在 "**内核态**"(CPU 处于特权模式),**可访问机器的全部资源**,其通过提供「**==系统调用==**」来向用户程序提供服务。
> - **普通应用程序**运行在 "**用户态**"(CPU 处于用户模式),其需要通过 "**系统调用**" 来**向内核请求特权服务**(如从磁盘读取)。
>
> 由此,可有效防止程序错误或恶意代码直接影响整个系统的稳定性和安全性。
<br>
## "用户态->内核态" 之间的切换
触发 "**用户态->内核态**" 切换的情况只包括:
1. **同步中断**(软件中断):
- **trap**:CPU 执行 trap 指令 **`int 0x80` 或 `syscall`**,陷入内核态;
- **异常/故障**:CPU 执行指令时**触发异常**(如缺页故障、段错误、除 0 等),陷入内核态
2. **异步中断**(硬件中断):
- CPU 每执行完一条指令后,检查 **中断请求线 IRL** 是否有需响应的中断。
> [!info] `syscall` 指令不涉及 "中断向量表" 查询
>
> x86-64 下通过 `syscall` 指令陷入内核态后,会**直接跳转到 CPU 的 `IA32_LSTAR` 寄存器中存放的 "==系统调用处理程序==" 入口地址**,不涉及 "中断向量表" 的查询。
> [!NOTE] 同步中断示例
>
> ![[_attachment/02-开发笔记/05-操作系统/os-中断机制.assets/IMG-os-中断机制-1A113E1D8AD88F33D2E1A2594BD84F57.png|622]]
<br>
#### "用户态 -> 内核态" 切换过程中执行的操作
- (1)**CPU 自动完成的==硬件操作==**:
1. CPU 特权级由 CPL=3(用户模式)切换到 **CPL=0(内核模式)**;
2. CPU 修改 **==栈指针==寄存器 `%rsp`** 值:
- 暂存 `%rsp` 旧值 "**用户栈指针**" (例如存到其它**临时寄存器**中)
- 从全局描述符表 GDT 中**获取 TSS 描述符**,获取 "**==内核栈指针==**" `TSS.rsp0`,设置 `%rsp = TSS.rsp0`。
3. CPU 将 "**==用户态==的五个相关寄存器值**" 压入到 "**==内核栈==**" 上
- 即 `struct pt_regs` 中**末尾五项**,依次为: `RIP`(用户态 PC)、`CS`、`RFLAGS`、`RSP`(用户态栈指针) 、`SS`。
- (2)**内核执行的操作**:
- 通过汇编指令 push 将**其余的 "==用户态的寄存器值=="**(通用寄存器) 压入内核栈中,**构建成==完整的 `pt_regs` 结构体==**。
- 例如,通过 "通用寄存器" 传递给 "**系统调用函数**" 的参数。
<br>
#### "内核态 -> 用户态" 切换过程中执行的操作
- (1)**内核执行的操作**:
1. 通过汇编指令 pop 从 "**内核栈**"上弹出 `pt_regs` 结构体中保存的 ==**用户态的寄存器值**==(除末尾午休),恢复到对应寄存器中。
2. **执行 `sysretq`(或 `iretq`)指令**,令 CPU 自动完成下列硬件操作,返回用户态
- (2)**CPU 自动完成的硬件操作**
1. CPU 从 "**内核栈**" 弹出 `pt_regs` 中末尾五项 "**用户态的寄存器值**":`RIP`、`CS`、`RFLAGS`、`RSP` 、`SS`,恢复到对应寄存器中。
2. CPU 特权级由 CPL=0(内核模式)切换到 **CPL=3(用户模式)**;
> [!info] `struct pt_regs` 结构体
>
> `pt_regs` 结构体中保存了 "**用户态->内核态**" 切换时,"**==用户态的寄存器值==**"(按照寄存器值在 "==**内核栈**==" 上的存储顺序)[^2] [^3]
>
> - 末尾五项由 **CPU 硬件**自动压入 "内核栈";
> - 其余项由**内核代码通过汇编指令**压入 "内核栈"。
>
> `task_struct` 中的 `thread_struct` 结构体中,具有 `struct pt_regs* regs` 指针,即**指向 "==内核栈==" 上存储 "用户态寄存器值" 的内存地址**。
> "**内核态->用户态**" 恢复时,可通过该 `regs` 指针读取这些寄存器值,恢复到寄存器中。
>
> ![[_attachment/02-开发笔记/05-操作系统/os-中断机制.assets/IMG-os-中断机制-6E7370127F78DC5DA199B6FBF0DE3BA2.png|440]]
>
> ^41h6l9
<br><br>
# 内核栈
内核为 **每个进程或线程** 维护了一个**独立的 "==内核栈=="**,位于 "**内核地址空间**" 中。
内核栈是进程或线程**在内核态下执行==内核代码==时专用的栈空间**,
诸如**系统调用函数**(例如 `sys_read()`)、**中断服务程序** 等内核代码,都是**在内核栈上展开栈帧**,存储返回地址、具备变量。
当进程发生 "**用户态->内核态**" 切换时,CPU 会自动将 **`%rsp` 寄存器** 的值由原先的 "**用户栈指针**" 修改为 "**==内核栈指针==**"。
> [!info] Linux 下,每个进程/线程的 "**用户栈指针**" 与 "**==内核栈指针==**" 记录在其 `task_struct` 中的`struct thread_struct thread` 成员中
>
> - 内核栈指针:`task_struct.thread.sp0` ,指向 "**内核栈的栈顶**"(低地址)
> - 用户栈指针:`task_struct.thread.rsp0`,指向 "**用户栈的栈顶**"(低地址)
>
> [!info] x86-64 Linux 下内核栈的大小为 ==16KB==(2 个 8KB 的虚拟页)
> [!NOTE] 内核栈示意
>
> - Linux 2.6 以前,各个进程/线程的 `task_struct` 存放在 "内核栈的尾段",即内核栈与 `task_struct` 共用 2 页(8KB)的内存。
> - Linux 2.6 以后,`task_struct` 由 slab 分配器动态分配,栈顶存放的是 **`struct thread_info` 结构体**,其中**具有一个指向 `task_struct` 的指针** [^4] 。
>
> 下图参见[^5]
>
> ![[_attachment/02-开发笔记/05-操作系统/os-中断机制.assets/IMG-os-中断机制-7AA99B05D425CAABB51FF349C9F3FAE5.png|650]]
>
>
> ![[_attachment/02-开发笔记/05-操作系统/os-中断机制.assets/IMG-os-中断机制-1D9EAE56D83EF6162CABD00D9D88120F.png|442]]
>
<br><br><br>
# 系统调用
> ❓<font color="#c00000">什么是系统调用?</font>
"**系统调用**" 确切来说是 **一段 ==OS 内核代码==**,只会在 "**内核态**" 下执行。
OS 以 "**系统调用**" 的形式向用户程序提供 "**内核服务**"——用户程序运行在用户态,当**需要访问硬件或系统资源**时,用户程序必须通过 **系统调用相关的库函数** 来请求 "**==内核服务==**",陷入到**内核态**,由 **内核** 完成对应的**系统调用处理**,**执行特定的特权操作**,而后切换回用户态。
系统调用相关的**库函数** 示例:
- **文件操作**:如读写文件、设备控制——`open`,`read`, `write` 等;
- **进程管理**:如创建、终止进程——`fork`, `exec` 等;
- **内存分配**:如 `brk`,`sbrk` 等;
这些库函数背后会执行 "系统调用" ,涉及到 **CPU 特权级切换、用户态寄存器值保存** 的过程,因此时延会比普通的函数调用高 1~2 个数量级。
> [!NOTE] 系统调用均为 "原子操作"
>
> 内核保证了 "系统调用" 均以 "**原子**" 操作的方式进行,期间**不会为其他进程或线程所中断**。
> [!faq] 系统调用被封装为 C 库中的过程调用
>
> 从 C 语言编程的角度来看,"**系统调用**" 被封装在 **C 库函数的过程调用中**,用户进程通过调用这些库函数来请求系统调用。[^6]
>
> 例如:
>
> - `mmap()` 库函数是 `<sys/mman.h>` 头文件提供的,其背后执行了 **同名的系统调用 mmap**;[^7]
> - `malloc()` 库函数是 C 标准库头文件 `<stdlib.h>` 提供的,其背后则执行了**系统调用 sbrk**。
>
> ![[_attachment/02-开发笔记/05-操作系统/os-中断机制.assets/IMG-os-中断机制-4624045589F49069336473523468029F.png|630]]
>
> 上图参见 [^8]
<br><br>
## 系统调用的具体过程
OS 内核以 C 标准库中 "**库函数**" 的形式**向用户程序提供系统调用相关的编程接口**,部分库函数(例如 `open`、`read`、`write`) 的背后即会执行**系统调用**。
> [!example] `read()` 库函数背后的过程[^9] [^10]
>
> ![[_attachment/02-开发笔记/05-操作系统/os-中断机制.assets/IMG-os-中断机制-6BDC85FE49B986C76FFEAB541113B526.png|580]]
>
> ```text
> --> libc 中的 read()
> --> syscall(SYS_read, fd, buf, 100)
> --> 执行 syscall 指令,陷入内核
> --> 内核根据 SYS_read 查找 sys_call_table[]
> --> 调用 sys_read() 内核函数
> ```
>
<br>
### syscall 函数
C 标准库提供了一个 **`syscall` 函数**,可供 **==直接调用任何系统调用==**。 `read()`/ `write()` 等**涉及系统调用的库函数**即是对 **`syscall()` 函数** 的封装。
> [!example] syscall 函数调用示例
>
> ```c
> // 等价于 `read(fd, buf, sizeof(buf))` ;syscall 函数背后会执行 syscall 汇编指令
> ssize_t ret = syscall(SYS_read, fd, buf, sizeof(buf));
> ```
>
`syscall()` 函数背后的实现会嵌入 "**汇编语言**" 来执行 "**==trap 指令==**",**陷入内核态**并跳转到内核中的 "**系统调用处理程序**":
1. 将对应的 "**==系统调用号==**" 写入寄存器(例如 `%rax`);
2. **执行 trap 指令**,**陷入==内核态==**。
- x86 中——通过 **`int 0x80` 指令** 触发 trap,其中参数 `0x80` 为 **中断向量号**,对应内核 "**==系统调用入口程序==**",查询 **中断向量表** 后跳转执行。
- x86-64 中——通过 **`syscall` 指令**触发 trap,无参数,不查中断向量表,**直接跳转至内核 "==系统调用入口程序=="**。
- 专用寄存器 `IA32_LSTAR` 存放着**系统调用入口程序的地址**,省略了中断向量表查询这一步。
内核中维护着 "**系统调用表**"(`sys_call_table[]`),存放各个具体系统调用函数的**函数指针**。
"**系统调用入口程序**" 会根据 "**系统调用号**" 执行对应的**系统调用处理函数**。
"**==系统调用号==**" 以及 "==**系统调用的参数**==" 通过 **"==通用寄存器=="** 传递,而不是栈 [^11]。
- `%rax`:存放**系统调用号**;系统调用返回时,则用以存放 "**返回值**"。
- `%rdi`,`%rsi`,`%rdx`,`%r10`,`%r8`,`%r9`:包含最多 6 个参数。
> [!example] 示例:"系统调用" 相关的库函数背后的汇编 [^11]
>
> ![[_attachment/02-开发笔记/05-操作系统/os-中断机制.assets/IMG-os-中断机制-33155EBFCE57B2C36718EB3C426FE95D.png|526]]
>
> [!NOTE] 系统调用过程说明[^6]
>
> x86 下的系统调用过程说明,通过 `int 0x80` 触发 trap,涉及 "**中断向量表**" 查询,`0x80` 即 "**系统调用处理程序**" 入口地址在中断向量表中的索引。
>
> ![[_attachment/02-开发笔记/05-操作系统/os-中断机制.assets/IMG-os-中断机制-3E1118A0B1F5CA0FE4C76EF3FF6962C7.png|572]]
>
<br>
## 系统调用表
各个具体 "**系统调用**" 内核代码入口地址位于 "**==系统调用表==**" 中,与 "**同步中断**" 里的其他 "**异常**" 不同,后者的中断处理程序位于 "**中断向量表**" 中。
> [!caution] 注意区分 "**系统调用表**" 与 "**中断向量表**",二者不在同一层面。
- "**异常**":如缺页异常、除 0、段错误等,各自对应不同的 "**==异常号==**",也即作为 "**中断向量号**" 用以查询 "**中断向量表**",获取对应的 **异常处理程序** 入口地址。
- "**系统调用**":
- "**系统调用入口程序**" 负责根据通过寄存器传递的 "**==系统调用号==**" 查询 "**系统调用表**(`sys_call_table[]`)",执行对应的系统调用处理。
- 例如, Linux 下的 `entry_SYSCALL_64()`;
- "**系统调用入口程序**" 的入口地址位于:
- x86 下,存放在 "**中断向量表**" 中 `0x80` 表项—— `int 0x80` 中的 `0x80` 即是 "**中断向量号**",据此查询 "**中断向量表**"。
- x86-64 下,**直接存放在 `IS32_LSTAR` 寄存器** 中,`syscall` 指令会直接读取该寄存器值,跳转至 ""**系统调用处理程序**""
> [!NOTE] x86-64 中系统调用号示例[^11]
> ![[_attachment/02-开发笔记/05-操作系统/os-中断机制.assets/IMG-os-中断机制-E37628DF8836C46FE4A6C8507FAE825A.png|642]]
%%
## 系统调用的优化
系统调用的过程复用了 "**异常机制**",需要执行 **==特权级切换、用户态寄存器值保存==** 等操作,因此时延会比普通的函数调用高 1~2 个数量级。
优化系统调用的一种思路是,**在用户态和内核态之间共享一小块内存**,具体有两种实现方式:
1. 内核将一部分数据通过只读的形式共享给应用
2. 允许应用以 "向某一块内存页写入请求" 的方式发起系统调用,并通过轮询来等待系统调用完成。内核同样通过轮询来等待用户的请求。关键点在于,**内核独占一个 CPU 核心,而其它核心则一直在用户态运行**。
参见 [^12]
%%
<br><br><br>
# 中断机制
> 中断机制也称 "**异常机制**"。
中断(interrupt)定义为一个 "**改变 CPU 顺序执行的指令流**" 的事件,中断机制即进行 "**控制流转换**" 的机制 [^13] [^14]。
现代操作系统是 "**中断驱动**"的,其依赖 **中断机制** 来响应 **来自外部 I/O 设备的异步事件** 和 **来自 CPU 内部的同步事件**。
即在 CPU 运行期间,由于**外部硬件设备**或**特定的内部条件**触发中断信号,使 **CPU 中断当前执行的指令**,**由用户态转变为==内核态==**,转而执行 "**中断服务程序**" 来响应处理中断,处理完成后再回到用户态,继续运行**回到先前被中断的指令流**。
> [!info] 程序计数器 PC 中的值序列称之为 "**逻辑控制流**",简称 "**逻辑流**"[^15]
<br>
## "同步中断" 与 "异步中断"
中断分为两类:
- **==同步中断==**,也称 "**==软件中断==**":在 **CPU 执行指令时由 CPU 控制单元产生**,**来自 CPU 内部**。
- 包括三类:
- **陷阱***(trap):由 **trap 指令**(`int 0x80` 或 `syscall`) 主动触发的中断,见于执行 "**系统调用**" 时。
- **故障**(fault):除 0、缺页故障、非法内存访问、算术溢出等 CPU 运行时异常,或可能由 "**异常处理程序**" 修正。
- **终止**(abort):"**硬件错误**" 层面的致命错误,无法修复,CPU 无法继续当前指令,**导致直接终止程序**。
- **==异步中断==**,也称 "**==硬件中断==**":由 **外部 I/O 设备** (如键盘、网卡等)在任意时间的触发 "**中断请求**" 信号,故相较于 CPU 而言是异步的。
- 外设通过硬件层面的 "**中断控制器**" 向 CPU 发送中断信号,CPU 在每**次执行完一条指令时**检查其 **"中断请求线"** (IRL) 是否有中断信号。
每种具体中断类型都使用一个 **唯一的 8 位无符号整数** 表示(`[0, 255]`)称之为 "**中断号**"。
> [!caution] 在不同资料中,对于上述两类中断会采用**不同的等价术语**,例如称之为 "**异常**" 等[^15]。
<br>
## 关于 "中断" 的不同术语
##### 说法一
将所有触发 CPU 切换控制流的事件统称为 "**==中断==**",再进一步划分 "**硬件中断**" 与 "**软件中断**"(见维基百科[^16])。
###### 说法二 5
在 Intel 手册中[^13]:
- 将 "**异步中断**" 称之为 "**中断**"(interrupt)。
- 将 "**同步中断**" 称之为 "**==异常==**"(exception);
##### 说法三
《CSAPP》中将 "CPU 控制流" 的变化统称为 "**==异常==**",并细分了四种类型[^17]:
![[_attachment/02-开发笔记/05-操作系统/os-中断机制.assets/IMG-os-中断机制-D109260FDC28B2A2FC8F4EBC354C2E6C.png|624]]
根据该书的定义:
- 只将 "**异步中断**" 称之为 "**中断**",也称 "**==异步异常==**";
- 而将 "**同步中断**" 称之为 "**==同步异常==**",并细分为三类:
- **陷阱***(trap):"**系统调用**" 这类进程主动触发的中断,涉及对 **trap 指令**的主动调用。
- **故障**(fault):通常可由 "**异常处理程序**" 修正的异常情况,**纠正后可重新执行 "触发故障" 的指令**。
- 例如 "**==缺页故障==**"、"**==除 0 错误==**"、"**==非法内存访问==**"(段错误)、"**==算术溢出==**" 等。
- 触发缺页异常后,缺页处理程序会将缺失物理页从磁盘中重新调入内存,不会导致进程终止;
- 非法内存访问时报告段错误,进程终止。
- **终止**(abort):例如 "**硬件错误**" 这种不可修复的致命错误,通常导致直接终止程序的。
- 例如存储器校验出错,DRAM 或 SRAM 位被破坏时发生的奇偶错误。
> [!summary] 术语总结
>
> - 在广义的 "**中断**" 说法上,使用的术语是 "**中断处理机制**"、 "**中断向量表**"、"**中断处理程序**";
> - 在《CSAPP》的定义中,使用的术语是 "**异常处理机制**"、 "**异常(向量)表**"、"**异常处理程序**"
>
> | 说明 | 大众使用的术语 | CSAPP 中使用术语 |
> | ------------------------------- | -------------------- | --------------------------------- |
> | 统称 | 中断 | **==异常==** |
> | 由**外部硬件设备**发出的信号产生,为异步事件 | 硬件中断 \| 异步中断 \| 外部中断 | **==中断==** \| 异步异常 |
> | 由**程序指令执行时产生的、来自 CPU 内部**,为同步事件 | 软件中断 \| 同步中断 \| 内部中断 | **==陷阱==、==故障==、==终止==** \| 同步异常 |
>
<br><br><br>
# 同步中断 | 软件中断
同步中断包括:
- **陷阱***(trap):由 **trap 指令**(`int 0x80` 或 `syscall`) 主动触发的中断,常见于执行 "**系统调用**" 时。
- **故障**(fault):"**缺页故障**"、"**除 0 错误**"、"**非法内存访问**"、"**算术溢出**" 等CPU 运行时异常,或可能由 "**异常处理程序**" 修正。
- **终止**(abort):"**硬件错误**" 层面的致命错误,无法修复,CPU 无法继续当前指令,**导致直接终止程序**。
> [!info] "下陷 | 陷入 | trap"
>
> 泛指通过 "trap" 指令使得 **CPU 中断当前执行**,由**用户态下陷至==内核态==**,跳转到**预定义地址**开始执行的**指令或事件** 的过程。
<br>
### trap 指令
> trap 指令也称 "**陷阱指令**"、**"==软件中断==指令"**。
"**trap 指令**" 泛指一类特殊指令,用以触发 "**同步中断**",使 CPU 从用户态切换至内核态,通常用于 "**系统调用**" 的场景。
在不同的 CPU 体系架构下,其对应的**具体指令**不同:
| 指令集架构 | trap 指令 | 说明 |
| --------- | ------------------------------ | --------------------------------------------------------------------------------------------------------------------------------- |
| x86(32 位) | `int 0x80` | - `n` 为单字节大小的值(`[0, 255]`),指定了**中断向量号**; <br>- `int 0x80` 在 32 位 Unix-like 系统上被约定为 **系统调用的入口点**,`0x80` 为中断向量表(IDT)中第 128 项的索引下标。 |
| x86-64 | `syscall` | 无参数,专供系统调用使用,用以触发 trap。 |
| ARM | - 旧版本:`SWI n` <br>- 新版本:`SVC` | - 用于旧版本的ARM架构。其中 `n` 是一个可选的立即数,通常用于指定所需的系统调用。<br>- 在新版本的ARM中,替换为 **`SVC`**(超级访问调用)。 |
| MIPS | `syscall` | |
指令集架构 ISA 中规定了 CPU 所支持的"**==中断指令==**" 以及 "**==中断返回指令==**":
- **中断指令**(也称 **==trap 指令==**)——当进程执行 "中断指令 "时,**CPU 将触发一个软中断**,并 **自动切换其特权模式为==内核态==**。
- 例如 x86 架构中的 `int 0x80`、**x86-64 架构中的 `syscall` 指令**。
- **中断返回指令**(也称 **==trap 返回指令==**)——用于**从中断处理程序中返回**,CPU 将**自动切换至==用户态==**,恢复之前被中断进程的上下文。
- 例如 `iret` 指令(interrupt return)
> [!example]
>
> 例如 x86 架构下,CPU 有 0~3 共四个特权级,其中 CPL= 0 是内核模式,CPL=3 是用户模式。
>
> - 调用 `syscall` 触发软中断后,CPU 会自动从 CPL=3 切换到 CPL= 0,**进入内核模式**;
> - 调用 `iret` 中断返回指令后,CPU 会自动从 CPL=0 切换回 CPL=3,**回到用户模式**。
<br><br><br>
# 异步中断 | 硬件中断
"**硬件中断**" 是 I/O 硬件设备(键盘、鼠标、网卡、硬盘等)用以**通知 CPU** 的机制。
CPU 有一个**专用引脚**,称之为 "**==中断请求线==**"(==IRL==)。
CPU 在**执行完每条指令后**,都会**检查 IRL 是否有中断请求**,若有,则保存当前上下文后**跳转至内核中对应的中断处理程序**,进行中断处理。
硬件设备向 "**==中断控制器==**" 发送 "**中断请求**",由 "**中断控制器**" 再传送给 CPU,即过程为"**外设 --> 中断控制器 --> CPU**"。
> [!NOTE] 概念
>
> - **中断请求线**(Interrupte Request Line,IRL):**CPU 的一个引脚**;
> - **中断请求**(Interrupte Request,IRQ):I/O 设备向 "**中断控制器**" 发出的信号。
>
<br>
### 中断控制器
中断控制器负责**接收/管理来自不同 I/O 设备的中断请求,协调中断优先级,屏蔽某些中断** 等。
- 早期 PC 上,**中断控制器集成于主板上**,称为 **==PIC==**(Programmable Interrupt Controller)
- 例如 Intel 8259A。
- 现代 PC 上,中断控制器分成两部分,称为 **==APIC==**(Advanced Programmable Interrupt Controller)
- **==Local APIC==**:**集成在 CPU 内部**,每个 CPU 核心具有**一个独立的 Local APIC**,支持 **优先级控制、中断屏蔽**,通过总线与 IO APIC 协作。
- **==IO APIC==**:**集成于主板上**,负责接收/管理**来自外设的中断**,转发至**某个 CPU 核心的 Local APIC**。
<br>
### 早期硬件中断的机制
线路连接:
- **外设连接到==中断控制器==的不同引脚**,每个引脚对应一个 "**==IRQ 编号==**"(例如 `IRQ0:定时器`,`IRQ1:键盘` 等)
- **中断控制器**连接到 CPU 的 "**中断请求线**"(即 INTR 引脚)。
中断请求过程:
1. 外设向 **中断控制器的引脚** 发起 **==中断请求==**(例如键盘对应的 `IRQ1` 引脚);
2. **中断控制器 PIC** 接收来自不同外设的中断,**结合优先级、屏蔽性选出当前需响应的中断**:
1. PIC 将来自不同引脚的中断请求,映射为 "**==中断向量号==**"(例如键盘 `IRQ1` 对应的中断向量号 `0x21`)
2. PIC 向 **CPU 的==中断请求线==** 发送中断信号;
3. CPU 向中断控制器发送 "**中断确认信号**"(nterrupt Acknowledge,INTA)
4. PIC 收到 INTA 信号后,将 **中断号** 发送至总线上,**推送给 CPU** [^18];
3. CPU 读总线,获取中断向量号,查询 "**中断向量表**",跳转执行 "**中断服务例程**"。
### 现代硬件中断机制
采用 **APIC 系统**(Advanced Programmable Interrupt Controller)—— IO APIC + Local APIC + MSI 中断
**IO APIC** 具有一个 **==中断路由表==**,由 OS 内核启动时配置,负责将 IRQ 号转化
- 外设通过 PCIe 总线向 IO APIC **发送中断请求 IRQ**;
- IO APIC 根据 "**中断路由表**",将 IRQ 设备号转换为 "**中断向量号**",**转发到某个 CPU 核心的 Local APIC**
- Local APIC 通知 CPU 核心响应中断.
- CPU 获取中断向量号,查询 "**中断向量表**",跳转执行 "**中断服务例程**"。
示例:网卡收到数据,触发 MSI 中断 → IO APIC 收到后 → 派发到 CPU 1 的 Local APIC → Local APIC 通知 CPU 响应
### 时钟中断
"时钟中断" 由 **硬件定时器** 周期性触发(每毫秒或每几毫秒一次),**OS 内核据此机会获取 CPU**,执行**任务调度**。
> [!NOTE] OS 内核通过硬件时钟产生的中断来定期获取 CPU 控制权,防止一个进程死循环
>
> 例如,进程无任何系统调用,也不会运行出错,但独占 CPU 而使内核永远无机会获取 CPU。
>
<br><br><br>
# 中断处理相关概念
## 中断号 | 异常号
每种**具体的中断**都被分配有一个 **唯一的 8 位无符号整数** 表示(`[0, 255]`),统称为 "**==中断号==**" or "**==异常号==**"(也称 "**中断向量**") 。
中断号用作为 **中断向量表中条目的索引**,从而获取该中断对应的 **中断处理程序** 入口地址。
- 部分号码由 **CPU 设计者** 分配(ISA 规定),范围在 `[0, 31]`,包括 **除 0、缺页、非法内存访问、算术运算溢出** 等同步中断。
- 部分号码由 **OS 内核** 设计者分配,范围通常在 `[32, 255]`,包括各种**外部 I/O 设备对应的中断号**、**x86 下 "系统调用入口程序" 对应的中断号 `0x80`**。
> [!info] x86-64 中的 "中断号" [^11]
>
> ![[_attachment/02-开发笔记/05-操作系统/os-中断机制.assets/IMG-os-中断机制-C8583A16B8A7D6B5C99B5AA8D5DDBC58.png|484]]
>
>
> ![[_attachment/02-开发笔记/05-操作系统/os-中断机制.assets/IMG-os-中断机制-4E36094DD8ECDE6480EFFCCAE94E2C0C.png|440]]
>
<br>
## 中断向量表
> 也称 "中断描述符表"(Interrupt Descripter Table,**==IDT==**),"**异常表**"
中断向量表中**存储了==所有中断/异常处理程序==的入口地址**,表中每个条目称之为 "**中断描述符**"。
"**中断号**" 用作该表中条目的索引下标,用以获取对应的中断/异常处理程序地址。
**中断向量表** 由 OS 启动时创建&初始化,存放于至 **==特定内存地址==**(内核地址空间),
并**通过 `lidt` 指令** 将其 **起始地址** 加载到CPU 中的 "**==中断描述符表基址寄存器==**" (IDTR 寄存器)中。
> [!NOTE] 中断描述符表的初始化
>
> 内核启动时:
>
> 1. 创建一个空白 "**中断描述符表**"(IDT),例如一个 `idt_table` 数组。
> 2. 注册 **中断号-中断处理函数**,例如通过调用 `set_intr_gate(14, page_fault)` 进行注册。
> 3. 加载 IDT 到 CPU 中:通过 `lidt` 指令**将内存中的==中断描述符表地址== 存入 CPU 的 IDTR 寄存器**。
>
<br>
## 中断处理程序
> 也称 "中断服务例程"(Interrupt Service Routing,**==ISR==**),**异常处理程序** (Exception Handler)
中断处理程序本质上是 "**内核代码**",只运行在 "**内核态**" 下,专用于**完成对应中断的处理**。
例如包括**读取或清除设备状态**、**传输数据**、**发送信号或通知**、**终止进程**等。
各类中断处理程序的函数地址均存放在 "**同一张中断向量表**"(IDT)中。
> [!NOTE] 中断处理程序的注册
>
> - **系统内置中断**:内核初始化阶段,随中断向量表的初始化而注册,例如,定时器中断、系统调用中断、缺页异常
> - **外设设备中断**:驱动程序加载时通过系统调用 `request_irq()` 动态注册。
<br><br><br>
# 中断处理流程
大致流程如下:
1. **触发中断**:
- 同步中断(软件中断):
- CPU 执行 **`syscall` 或 `int 0x80` 指令** 直接触发;
- CPU 执行指令时**触发异常**(如缺页故障、除 0)
- 异步中断(硬件中断):
- CPU 每执行完一条指令后,检查 **==中断请求线 IRL==** 是否有需响应的中断。
2. **中断响应**
1. **==用户态->内核态==切换**:CPU 特权级切换,修改`%rsp` 为 "**内核栈指针**",将 "**用户态寄存器值**" 压入到 "**==内核栈==**" 中。
2. **执行内核代码**
- 根据 "**中断向量号**" 查找 **==中断向量表==**,获取中断处理程序地址。
- 跳转执行 "**中断处理程序**";
- 执行中断返回指令 `sysretq` 或 `iretq`
3. **中断返回**
1. **==内核态->用户态==切换**:从 "**内核栈**" 弹出 "**用户态寄存器值**" 并恢复到对应寄存器,切换 CPU 特权级;
2. 控制权回到用户**进程 or 线程**
当中断发生时,CPU 暂停执行当前的指令流,特权级别由 "**用户模式**" 切换为 "**内核模式**",**保存当前 CPU 上下文**(寄存器值)。
随后根据 "**中断号**" 查找 "**中断向量表**",跳转至对应的 "**==中断处理程序==**" 执行。
中断处理完成后,其**恢复此前暂存的 CPU 上下文**,切换回 "**用户模式**",返回此前的中断地址。
> [!NOTE] 中断处理流程[^19]
>
> ![[_attachment/02-开发笔记/05-操作系统/os-中断机制.assets/IMG-os-中断机制-820CE4DB6AEBA6C659E84E2929E9CAA5.png|451]]
>
> [!caution] 系统调用/中断响应过程中,不存在 "用户进程->内核进程"的上下文切换。
>
> 所谓 "**系统调用**" 与 "**中断服务程序/例程**" 本质上就是 **"内核代码",即内核中的一个函数**。
> 这一内核函数的执行,是在 "**==用户进程的内核态上下文==**" ,即用户进程的 "**内核栈**" 上完成的,而不是说从 "用户进程" 切换执行 "内核进程"。
>
<br><br><br>
# ❓ FAQ:操作系统如何重获 CPU 的控制权?
> ❓<font color="#c00000">当一个进程在 CPU 上运行时,意味着操作系统没有运行,操作系统如何重获 CPU 的控制权,从而实现 "进程间切换" ?</font>
##### 早期系统——等待进程执行系统调用或非法操作
在早期系统中采用 "协作调度"机制,操作系统 **"==等待=="进程执行系统调用或非法操作而陷入内核态,此时 OS 重获 CPU 控制权**,从而可以进行进程调度 [^4]。
然而,**如果进程陷入死循环,且不进行任何系统调用,则 OS 将什么也做不了,只能重启机器**。
##### 现代系统——利用"时钟中断"
现代操作系统通过 "**时钟中断**"(timer interrupt) 来获取 CPU 控制权。
时钟设备**每隔几毫秒**产生一次中断,中断**使得当前正在运行的进程停止**,跳转至 **中断处理程序**,OS 重获 CPU 控制权。
###### 时钟中断机制 & 上下文切换说明
![[_attachment/02-开发笔记/05-操作系统/os-中断机制.assets/IMG-os-中断机制-8D155A25769CFED3F997B892E3876738.png|724]]
<br><br>
# 参考资料
- [4.陷阱](https://zhuanlan.zhihu.com/p/409537825)
# Footnotes
[^1]: 《深入理解计算机系统》P510
[^2]: 《深入 Linux 内核架构》(P52)
[^3]: 《深入理解 Linux 内核》(P171)
[^4]: 《Linux 内核设计与实现》(P22)
[^5]: 《深入 Linux 内核架构》(P57)
[^6]: 《操作系统导论-OSTEP》(P40)
[^7]: 《操作系统:原理与实现》P109
[^8]: 《Unix 环境高级编程》(P17)
[^9]: 《Linux 内核设计与实现》(P61)
[^10]: 《现代操作系统》(P29)
[^11]: 《深入理解计算机系统》P506
[^12]: 《操作系统:原理与实现》P66
[^13]: 《深入理解 Linux 内核》(P135)
[^14]: 《深入理解计算机系统 CSAPP》(P502,第八章)
[^15]: 《深入理解计算机系统 CSAPP》(P508)
[^16]: [中断 - 维基百科,自由的百科全书](https://zh.wikipedia.org/wiki/%E4%B8%AD%E6%96%B7)
[^17]: 《深入理解计算机系统 CSAPP》(P504)
[^18]: 《现代操作系统》(P17)
[^19]: 《操作系统概念》(P394)