内核态、系统调用、中断与异常¶
应用需要文件和网络服务,但不能被允许任意修改页表、关闭中断或直接控制设备。处理器特权级和受控入口共同建立了这条边界。
本页最重要的区分是:进入内核态描述权限变化,进程切换描述执行主体变化;二者可能同时发生,也可能完全不同时发生。
用户态与内核态¶
处理器用特权级限制敏感操作。不同体系结构命名不同,但可以先抽象成两类:
| 模式 | 可执行操作 | 典型代码 |
|---|---|---|
| 用户态 | 普通计算,只能访问获准映射 | 应用、普通库函数 |
| 内核态 | 可配置页表、中断和设备 | 内核、内核驱动 |
用户态进程访问未映射内存时,硬件不会“礼貌地返回错误值”,而是触发异常,把控制权交给内核。内核再判断这是可以处理的缺页,还是应向进程报告非法访问。
仅靠软件约定不够:如果恶意程序可以忽略约定,就仍能破坏系统。真正边界必须由硬件在每次取指、访存或执行特权指令时实施检查。
普通函数调用与系统调用¶
普通函数调用通常只修改程序计数器、栈和少量寄存器,调用者与被调用者处在同一权限级和地址空间。
系统调用则通过体系结构规定的陷入指令进入内核入口。典型步骤是:
- 用户代码把系统调用号和参数放入约定寄存器。
- 执行系统调用指令。
- CPU 切换到内核特权级并跳到受控入口。
- 入口代码保存用户现场,建立可安全运行内核代码的环境。
- 内核根据编号分派处理函数,复制并验证用户参数。
- 处理完成后设置返回值或错误码。
- 返回指令恢复用户现场和用户特权级。
最小例子:直接写标准输出¶
应用通常调用 C 库的 write 包装:
#include <unistd.h>
int main(void) {
const char msg[] = "hello\n";
ssize_t n = write(STDOUT_FILENO, msg, sizeof(msg) - 1);
return n < 0;
}
write 这个名字可能指库函数包装,也可能泛指内核系统调用。包装层可以处理 ABI、线程取消点或错误码转换;真正特权转换发生在体系结构的系统调用指令处。
为什么必须验证用户指针¶
用户传给内核的地址可能:
- 未映射或没有所需读写权限。
- 在检查后被另一个线程修改映射。
- 指向内核地址或跨越合法边界。
- 指向一个会在访问时缺页的页面。
内核不能把用户指针当作可信普通指针。它需要使用安全的用户空间复制机制,并正确处理复制期间的异常。验证也不能只做“地址小于某个值”的简单判断,因为权限由实际映射、范围和访问方向共同决定。
三类进入内核的事件¶
系统调用¶
系统调用由当前程序主动、同步触发。返回后通常继续执行系统调用之后的指令,除非调用导致进程退出、阻塞后被信号终止,或调度发生其他变化。
异常¶
异常由当前指令同步触发。例如:
- 缺页。
- 除零。
- 非法指令。
- 断点和单步调试陷阱。
- 访问权限违反。
有些异常可修复并重试指令,例如按需分页的缺页;有些异常表明程序错误,内核会向进程传递信号或终止进程。
硬件中断¶
硬件中断通常由外部设备异步产生,例如网卡收到数据、磁盘请求完成或定时器到期。它与当前执行指令没有因果上的同步对应。处理器会在允许的边界暂停当前控制流,保存必要状态并跳到中断处理入口。
| 事件 | 触发者 | 与当前指令关系 | 常见用途 |
|---|---|---|---|
| 系统调用 | 当前程序 | 主动、同步 | 请求内核服务 |
| 异常 | 当前指令/CPU | 被动、同步 | 缺页、错误、调试 |
| 中断 | 外部设备/定时器 | 异步 | 完成通知、抢占入口 |
中断处理为什么分层¶
中断到达时,应尽快确认设备状态、记录必要信息并允许后续中断。复杂工作如果全部在高优先级中断上下文中完成,会延迟其他中断并增加系统抖动。
因此常见设计会把处理分为:
- 快速部分:确认来源、读取关键状态、屏蔽或应答设备、把工作放入队列。
- 延后部分:在允许调度或更普通的上下文中处理协议、唤醒等待者和完成较重工作。
不同系统对“上半部、下半部、软中断、工作队列”等名称和约束并不完全相同,核心思想都是缩短不可抢占或高优先级路径。
一次时钟中断会不会切换进程¶
不一定。时钟中断只保证内核获得一次处理机会。内核更新计时和调度状态后,可能有三种结果:
- 当前线程仍最合适,直接返回它。
- 更高优先级线程已就绪,发生线程切换。
- 当前线程时间片耗尽,调度器选择另一个线程。
所以:
反过来,内核线程之间也可能在内核态发生上下文切换,而没有从用户态进入内核态的那一步。
系统调用成本来自哪里¶
系统调用的成本不只是一条陷入指令。可能包括:
- 保存和恢复寄存器。
- 特权级与栈切换。
- 参数复制和权限检查。
- 安全缓解措施带来的间接开销。
- 缓存、分支预测和 TLB 状态变化。
- 服务本身的锁、调度和 I/O 等待。
把大量小操作逐个做成系统调用通常效率低,因此接口会提供批量操作、缓冲、共享内存或异步队列。但批量越大,单次延迟、错误处理和公平性也可能变差。
若每次系统调用固定入口成本为 \(t_s\),每项实际工作成本为 \(t_w\),逐项处理 \(n\) 个对象的时间近似为:
若一个批量调用处理 \(n\) 项,忽略批处理额外开销时:
这解释了批量接口的动机,但没有反映内存占用、等待时间和失败粒度等代价。
异常返回与信号¶
当异常无法由内核透明修复时,内核通常把它转换成用户空间可观察事件。例如非法内存访问可能表现为进程收到信号。用户处理器运行前,内核要构造受控用户态现场;处理完成后,还需要通过安全返回路径恢复原上下文。
信号看似像软件中断,但两者层次不同:硬件中断是 CPU/设备到内核的事件;信号是内核向用户进程传递的异步通知抽象。
安全边界上的典型错误¶
- 未完整检查长度导致整数溢出或越界复制。
- 检查与使用分离,期间共享状态被改变。
- 错误路径忘记释放引用、锁或资源。
- 把用户控制数据当作内核指针或格式字符串。
- 返回路径恢复了不应由用户控制的特权状态。
系统调用入口暴露给所有进程,是内核的重要攻击面。接口越复杂,状态组合和错误路径越多,验证成本越高。
与其他章节的联系¶
fork、exec、wait通过系统调用操作进程对象。- 互斥锁竞争可能让系统调用中的线程睡眠并触发调度。
- 缺页异常进入虚拟内存处理器,修复后重试指令。
- 文件
read可能命中页缓存,也可能提交设备请求并等待中断。 - 容器中的系统调用仍进入宿主内核,因此系统调用过滤是重要隔离层。
自测¶
- 系统调用为什么不能只靠跳转到固定内核地址实现?
- 缺页异常与磁盘中断都是“进入内核”,它们的触发关系有何不同?
- 为什么时钟中断发生后可能仍返回原线程?
- 用户态驱动若能直接访问设备寄存器,会削弱哪些保护?
- 批量系统调用减少了什么成本,又可能引入哪些新权衡?
参考思路
普通跳转不会可靠提升特权级,也无法阻止用户跳入任意内核内部位置。缺页由当前访存同步触发,设备中断由外部完成异步触发。时钟只是调度检查点,调度器可继续选择原线程。直接设备访问可能绕过内存、I/O 和中断隔离。批量减少固定边界成本,但会增大单次工作量、延迟和错误处理复杂度。