引言
一台计算机的 CPU 在某一时刻只能执行有限数量的指令,但操作系统需要同时管理许多进程。用户通常不会感知到某个进程何时获得 CPU、何时暂时停止,也不需要手动保存和恢复程序状态。操作系统通过 CPU 虚拟化(CPU Virtualization)提供了“每个进程都拥有自己的 CPU”的抽象。
这里的“虚拟化”不是给每个进程分配一颗独立的虚拟处理器。进程的指令仍然直接运行在真实 CPU 上,操作系统只是通过时间共享(Time Sharing)让不同进程交替运行,并在切换时保存和恢复它们的执行状态。
要实现这个抽象,需要回答三个问题。
- 应用程序怎样以受控方式请求操作系统服务?
- 操作系统怎样从一个进程切换到另一个进程?
- 如果进程不主动让出 CPU,操作系统怎样重新取得控制权?
这三个问题分别对应系统调用、上下文切换和中断机制。
系统调用:受控地请求操作系统服务
应用程序运行在用户态(User Mode)时,只能执行受限制的操作。访问文件、创建进程、申请更多内存和操作设备等服务通常需要操作系统代表应用完成。应用通过系统调用(System Call)进入内核,请求内核执行这些受保护的操作。
多数程序不会直接写出具体的系统调用指令,而是通过较高层的应用程序编程接口(Application Programming Interface,API)完成。例如,C 运行时库可以提供 malloc()、fopen()、fread() 和 fclose() 等函数。函数内部是否需要系统调用,以及具体使用哪个系统调用,由运行时库和操作系统实现决定。
API 的价值在于隐藏了操作系统接口的细节。调用者只需要知道函数的参数、返回值和语义,不必了解当前系统怎样编号系统调用、怎样切换到内核态,也不必为每种处理器分别编写底层指令。Windows 常见的接口包括 Win32 API;Unix、Linux 和 macOS 等系统通常提供符合 POSIX 风格的接口。
需要区分 API 函数和系统调用本身。一个名称看起来像系统服务的函数,可能只是用户态的普通函数。例如,程序可以自行定义一个名为 open() 的函数,它并不会因此自动进入内核。只有当函数最终执行了系统调用指令,或者通过其他机制请求内核服务时,才会发生真正的用户态到内核态转移。
陷阱和系统调用处理
系统调用通常通过陷阱(Trap)进入内核。陷阱是一种由软件主动触发的控制转移机制。不同处理器架构提供的指令名称可能不同,例如 x86 中的 INT、SYSCALL 和 SYSENTER。
一次系统调用的大致流程如下。
- 用户态程序准备系统调用编号和参数。
- 库函数执行陷阱指令。
- 处理器保存足够的寄存器状态,并切换到内核态。
- 处理器根据陷阱入口跳转到内核的处理函数。
- 内核读取系统调用编号,查找对应的实现函数。
- 内核执行服务,将结果或错误状态写入规定的位置。
- 内核执行返回陷阱指令,恢复用户态执行。
系统调用编号用于告诉内核需要执行哪一种服务。内核通常维护一张由编号索引的系统调用表,表中的每一项指向对应的内核函数。
返回时,内核需要使用与进入时匹配的返回机制,例如 x86 中的 IRET、SYSRET 或 SYSEXIT。如果进入内核前没有保存足够的执行现场,程序就无法准确回到系统调用之后的位置。
直接执行与 CPU 虚拟化
为了让应用获得较高性能,操作系统通常允许用户程序直接在 CPU 上执行,而不是让内核逐条解释每条指令。这样做带来了效率,但也提出了控制问题:进程可能执行很长时间,甚至进入无限循环。如果进程从不进行系统调用,操作系统就不能只依靠系统调用重新获得控制权。
CPU 虚拟化的基本机制是时间共享。操作系统运行一个进程一段时间,然后停止它,保存它的状态,再运行另一个进程。对每个进程而言,自己的执行会被暂时中断;从整个系统看,多个进程在有限 CPU 上交替推进。
要让这个过程对应用透明,操作系统必须保证进程恢复后仍然从正确的位置继续执行。保存和恢复寄存器上下文,就是实现这一点的关键。
上下文切换
上下文(Context)是进程继续执行所需的处理器状态。它至少包括程序计数器(Program Counter,PC)、通用寄存器和内核栈指针等信息。不同处理器架构需要保存的具体寄存器集合可能不同,但核心要求是一致的:恢复后的 CPU 状态必须与进程暂停前保持一致。
上下文切换(Context Switch)是从一个进程切换到另一个进程的过程。操作系统通常需要完成以下步骤。
- 保存当前运行进程的寄存器上下文。
- 更新当前进程在进程控制块(Process Control Block,PCB)中的状态。
- 按照调度策略把当前进程放入合适的队列。
- 选择另一个就绪进程。
- 从新进程的 PCB 中读取寄存器上下文。
- 更新新进程的状态,并切换到它的内核栈。
- 通过返回机制恢复新进程的用户态执行。
上下文切换的低层实现通常依赖一小段汇编代码。它要保存通用寄存器、程序计数器和内核栈指针,再恢复下一个进程的对应状态。切换内核栈后,内核代码会从“为旧进程服务”的执行上下文转入“为新进程服务”的执行上下文。
上下文切换的开销
上下文切换本身不直接完成用户程序的有效计算,因此操作系统需要尽量降低切换成本。开销不仅来自保存和恢复寄存器,还可能来自间接影响。
- 新进程的指令和数据可能不在物理内存中。
- 新进程的地址空间可能没有有效的缓存内容。
- 处理器缓存中的数据可能更适合旧进程。
- 频繁切换会减少每个进程连续执行的时间。
因此,调度器不能只追求切换次数少,也要在响应速度、公平性、吞吐量和缓存局部性之间进行权衡。
模式切换和上下文切换
模式切换(Mode Switch)与上下文切换不是同一个概念。
| 概念 | 变化 | 进程是否改变 |
|---|---|---|
| 模式切换 | 用户态和内核态之间切换 | 通常不改变 |
| 上下文切换 | 保存一个进程并恢复另一个进程 | 会改变 |
例如,进程执行 read() 请求文件数据时,处理器会从用户态进入内核态,内核处理完请求后再回到同一进程的用户态。这是模式切换,但不一定发生上下文切换。
如果定时器中断触发调度,内核可能先从当前进程进入内核态,再保存当前进程的上下文并恢复另一个进程的上下文。此时既发生了模式切换,也发生了上下文切换。
所以,模式切换关注的是权限级别和执行环境;上下文切换关注的是当前由哪个进程使用 CPU。一次模式切换可以只改变运行模式,而一次上下文切换通常需要在内核态中完成进程状态的保存与恢复。
中断:重新获得 CPU 控制权
如果进程不主动进行系统调用,操作系统仍然需要有办法打断它。为此,硬件提供了中断(Interrupt)机制。定时器设备可以按照固定间隔产生中断,使处理器进入内核的中断处理路径。
定时器中断让操作系统获得一种主动控制方式。
没有定时器中断时,非协作进程可能通过无限循环长期占用 CPU。操作系统如果只能等待进程主动进行系统调用,就无法保证其他进程及时获得运行机会。
中断的三种来源
从产生方式看,中断相关事件可以分成三类。
异常
异常(Exception)通常由当前进程正在执行的指令直接引起,因此与当前执行流同步。例如,除零、执行非法指令或访问受保护内存都可能触发异常。处理器会保存必要状态,然后让操作系统判断如何处理。
外部硬件中断
外部硬件中断来自处理器之外的设备事件,通常与当前进程正在执行的指令没有直接关系。例如,键盘输入、鼠标移动和定时器到期都可能触发异步中断。
软件生成的中断
软件也可以通过特定指令主动产生中断。系统调用使用的陷阱指令就属于这类控制转移机制。不同系统会根据处理器架构和系统设计选择不同的指令与入口。
中断处理流程
处理器收到中断或异常后,通常会先完成当前指令,再根据硬件和系统设置保存部分寄存器状态。操作系统随后需要完成以下工作。
- 根据中断编号找到对应的处理函数。
- 保存后续处理所需的进程状态。
- 执行中断处理逻辑。
- 判断恢复原进程,还是调用调度器切换到另一个进程。
- 通过返回机制恢复选中的进程。
中断处理函数的入口通常保存在中断向量表(Interrupt Vector)中。系统启动时会设置这张表;中断控制器向处理器提供中断编号后,处理器以编号为索引找到相应处理函数。
中断处理完成后,调度器需要决定是恢复刚才被中断的进程,还是切换到另一个就绪进程。定时器中断通常为调度器提供重新评估运行进程的机会。
系统调用、上下文切换与中断的关系
三种机制解决的是不同层次的问题。
| 机制 | 触发方式 | 主要作用 |
|---|---|---|
| 系统调用 | 应用主动执行陷阱或调用封装函数 | 请求内核提供受保护的服务 |
| 上下文切换 | 内核决定更换运行进程 | 保存当前进程并恢复另一个进程 |
| 中断处理 | 硬件、处理器或软件产生事件 | 让内核及时响应事件并重新获得控制权 |
它们可以组合出现,但不能互相替代。系统调用可能只导致模式切换,也可能因为进程阻塞而进一步触发上下文切换。定时器中断可能只让内核检查状态并恢复原进程,也可能导致调度器选择另一个进程。上下文切换则是 CPU 虚拟化中真正改变当前运行进程的核心动作。
小结
CPU 虚拟化依赖三类机制协同工作。系统调用为应用提供受控的内核服务入口;上下文切换保存并恢复进程的寄存器状态,让多个进程能够交替使用 CPU;中断,尤其是定时器中断,让操作系统即使面对非协作进程也能重新取得控制权。
理解这三者的边界很重要。模式切换改变的是处理器当前所处的权限级别,上下文切换改变的是当前运行的进程,中断则提供了从外部或当前执行事件进入内核的机会。