引言

一台计算机的 CPU 在某一时刻只能执行有限数量的指令,但操作系统需要同时管理许多进程。用户通常不会感知到某个进程何时获得 CPU、何时暂时停止,也不需要手动保存和恢复程序状态。操作系统通过 CPU 虚拟化(CPU Virtualization)提供了“每个进程都拥有自己的 CPU”的抽象。

这里的“虚拟化”不是给每个进程分配一颗独立的虚拟处理器。进程的指令仍然直接运行在真实 CPU 上,操作系统只是通过时间共享(Time Sharing)让不同进程交替运行,并在切换时保存和恢复它们的执行状态。

要实现这个抽象,需要回答三个问题。

  1. 应用程序怎样以受控方式请求操作系统服务?
  2. 操作系统怎样从一个进程切换到另一个进程?
  3. 如果进程不主动让出 CPU,操作系统怎样重新取得控制权?

这三个问题分别对应系统调用、上下文切换和中断机制。

系统调用:受控地请求操作系统服务

应用程序运行在用户态(User Mode)时,只能执行受限制的操作。访问文件、创建进程、申请更多内存和操作设备等服务通常需要操作系统代表应用完成。应用通过系统调用(System Call)进入内核,请求内核执行这些受保护的操作。

flowchart LR A[应用程序] --> B[API] B --> C[系统调用封装] C --> D[陷阱指令] D --> E[内核态] E --> F[系统调用处理函数] F --> G[返回用户态] G --> A

多数程序不会直接写出具体的系统调用指令,而是通过较高层的应用程序编程接口(Application Programming Interface,API)完成。例如,C 运行时库可以提供 malloc()、fopen()、fread() 和 fclose() 等函数。函数内部是否需要系统调用,以及具体使用哪个系统调用,由运行时库和操作系统实现决定。

API 的价值在于隐藏了操作系统接口的细节。调用者只需要知道函数的参数、返回值和语义,不必了解当前系统怎样编号系统调用、怎样切换到内核态,也不必为每种处理器分别编写底层指令。Windows 常见的接口包括 Win32 API;Unix、Linux 和 macOS 等系统通常提供符合 POSIX 风格的接口。

需要区分 API 函数和系统调用本身。一个名称看起来像系统服务的函数,可能只是用户态的普通函数。例如,程序可以自行定义一个名为 open() 的函数,它并不会因此自动进入内核。只有当函数最终执行了系统调用指令,或者通过其他机制请求内核服务时,才会发生真正的用户态到内核态转移。

陷阱和系统调用处理

系统调用通常通过陷阱(Trap)进入内核。陷阱是一种由软件主动触发的控制转移机制。不同处理器架构提供的指令名称可能不同,例如 x86 中的 INT、SYSCALL 和 SYSENTER。

一次系统调用的大致流程如下。

  1. 用户态程序准备系统调用编号和参数。
  2. 库函数执行陷阱指令。
  3. 处理器保存足够的寄存器状态,并切换到内核态。
  4. 处理器根据陷阱入口跳转到内核的处理函数。
  5. 内核读取系统调用编号,查找对应的实现函数。
  6. 内核执行服务,将结果或错误状态写入规定的位置。
  7. 内核执行返回陷阱指令,恢复用户态执行。

系统调用编号用于告诉内核需要执行哪一种服务。内核通常维护一张由编号索引的系统调用表,表中的每一项指向对应的内核函数。

sequenceDiagram participant App as 用户态程序 participant CPU as 处理器 participant Kernel as 内核 App->>CPU: 执行系统调用指令 CPU->>CPU: 保存必要的寄存器状态 CPU->>Kernel: 切换到内核态并跳转到处理入口 Kernel->>Kernel: 根据编号查找处理函数 Kernel->>Kernel: 执行服务并保存返回值 Kernel->>CPU: 执行 return-from-trap CPU->>App: 恢复状态并返回用户态

返回时,内核需要使用与进入时匹配的返回机制,例如 x86 中的 IRET、SYSRET 或 SYSEXIT。如果进入内核前没有保存足够的执行现场,程序就无法准确回到系统调用之后的位置。

直接执行与 CPU 虚拟化

为了让应用获得较高性能,操作系统通常允许用户程序直接在 CPU 上执行,而不是让内核逐条解释每条指令。这样做带来了效率,但也提出了控制问题:进程可能执行很长时间,甚至进入无限循环。如果进程从不进行系统调用,操作系统就不能只依靠系统调用重新获得控制权。

CPU 虚拟化的基本机制是时间共享。操作系统运行一个进程一段时间,然后停止它,保存它的状态,再运行另一个进程。对每个进程而言,自己的执行会被暂时中断;从整个系统看,多个进程在有限 CPU 上交替推进。

flowchart LR A[进程 A 运行] --> B[保存 A 的执行现场] B --> C[恢复 B 的执行现场] C --> D[进程 B 运行] D --> E[保存 B 的执行现场] E --> F[恢复 A 的执行现场] F --> A

要让这个过程对应用透明,操作系统必须保证进程恢复后仍然从正确的位置继续执行。保存和恢复寄存器上下文,就是实现这一点的关键。

上下文切换

上下文(Context)是进程继续执行所需的处理器状态。它至少包括程序计数器(Program Counter,PC)、通用寄存器和内核栈指针等信息。不同处理器架构需要保存的具体寄存器集合可能不同,但核心要求是一致的:恢复后的 CPU 状态必须与进程暂停前保持一致。

上下文切换(Context Switch)是从一个进程切换到另一个进程的过程。操作系统通常需要完成以下步骤。

  1. 保存当前运行进程的寄存器上下文。
  2. 更新当前进程在进程控制块(Process Control Block,PCB)中的状态。
  3. 按照调度策略把当前进程放入合适的队列。
  4. 选择另一个就绪进程。
  5. 从新进程的 PCB 中读取寄存器上下文。
  6. 更新新进程的状态,并切换到它的内核栈。
  7. 通过返回机制恢复新进程的用户态执行。

上下文切换的低层实现通常依赖一小段汇编代码。它要保存通用寄存器、程序计数器和内核栈指针,再恢复下一个进程的对应状态。切换内核栈后,内核代码会从“为旧进程服务”的执行上下文转入“为新进程服务”的执行上下文。

上下文切换的开销

上下文切换本身不直接完成用户程序的有效计算,因此操作系统需要尽量降低切换成本。开销不仅来自保存和恢复寄存器,还可能来自间接影响。

  • 新进程的指令和数据可能不在物理内存中。
  • 新进程的地址空间可能没有有效的缓存内容。
  • 处理器缓存中的数据可能更适合旧进程。
  • 频繁切换会减少每个进程连续执行的时间。

因此,调度器不能只追求切换次数少,也要在响应速度、公平性、吞吐量和缓存局部性之间进行权衡。

模式切换和上下文切换

模式切换(Mode Switch)与上下文切换不是同一个概念。

概念变化进程是否改变
模式切换用户态和内核态之间切换通常不改变
上下文切换保存一个进程并恢复另一个进程会改变

例如,进程执行 read() 请求文件数据时,处理器会从用户态进入内核态,内核处理完请求后再回到同一进程的用户态。这是模式切换,但不一定发生上下文切换。

如果定时器中断触发调度,内核可能先从当前进程进入内核态,再保存当前进程的上下文并恢复另一个进程的上下文。此时既发生了模式切换,也发生了上下文切换。

所以,模式切换关注的是权限级别和执行环境;上下文切换关注的是当前由哪个进程使用 CPU。一次模式切换可以只改变运行模式,而一次上下文切换通常需要在内核态中完成进程状态的保存与恢复。

中断:重新获得 CPU 控制权

如果进程不主动进行系统调用,操作系统仍然需要有办法打断它。为此,硬件提供了中断(Interrupt)机制。定时器设备可以按照固定间隔产生中断,使处理器进入内核的中断处理路径。

定时器中断让操作系统获得一种主动控制方式。

sequenceDiagram participant Timer as 定时器 participant CPU as 处理器 participant OS as 操作系统 participant A as 进程 A participant B as 进程 B A->>CPU: 在用户态执行 Timer->>CPU: 产生定时器中断 CPU->>OS: 保存必要状态并进入内核态 OS->>OS: 执行中断处理函数 OS->>OS: 调度器决定继续 A 或切换 OS->>B: 恢复 B 的上下文 B->>CPU: 在用户态继续执行

没有定时器中断时,非协作进程可能通过无限循环长期占用 CPU。操作系统如果只能等待进程主动进行系统调用,就无法保证其他进程及时获得运行机会。

中断的三种来源

从产生方式看,中断相关事件可以分成三类。

异常

异常(Exception)通常由当前进程正在执行的指令直接引起,因此与当前执行流同步。例如,除零、执行非法指令或访问受保护内存都可能触发异常。处理器会保存必要状态,然后让操作系统判断如何处理。

外部硬件中断

外部硬件中断来自处理器之外的设备事件,通常与当前进程正在执行的指令没有直接关系。例如,键盘输入、鼠标移动和定时器到期都可能触发异步中断。

软件生成的中断

软件也可以通过特定指令主动产生中断。系统调用使用的陷阱指令就属于这类控制转移机制。不同系统会根据处理器架构和系统设计选择不同的指令与入口。

中断处理流程

处理器收到中断或异常后,通常会先完成当前指令,再根据硬件和系统设置保存部分寄存器状态。操作系统随后需要完成以下工作。

  1. 根据中断编号找到对应的处理函数。
  2. 保存后续处理所需的进程状态。
  3. 执行中断处理逻辑。
  4. 判断恢复原进程,还是调用调度器切换到另一个进程。
  5. 通过返回机制恢复选中的进程。

中断处理函数的入口通常保存在中断向量表(Interrupt Vector)中。系统启动时会设置这张表;中断控制器向处理器提供中断编号后,处理器以编号为索引找到相应处理函数。

flowchart TB A[设备或处理器事件] --> B[产生中断编号] B --> C[处理器保存必要状态] C --> D[查找中断向量表] D --> E[执行中断处理函数] E --> F{是否需要调度} F -->|否| G[恢复被中断进程] F -->|是| H[保存当前进程上下文] H --> I[恢复被选中进程上下文] I --> J[返回用户态执行] G --> J

中断处理完成后,调度器需要决定是恢复刚才被中断的进程,还是切换到另一个就绪进程。定时器中断通常为调度器提供重新评估运行进程的机会。

系统调用、上下文切换与中断的关系

三种机制解决的是不同层次的问题。

机制触发方式主要作用
系统调用应用主动执行陷阱或调用封装函数请求内核提供受保护的服务
上下文切换内核决定更换运行进程保存当前进程并恢复另一个进程
中断处理硬件、处理器或软件产生事件让内核及时响应事件并重新获得控制权

它们可以组合出现,但不能互相替代。系统调用可能只导致模式切换,也可能因为进程阻塞而进一步触发上下文切换。定时器中断可能只让内核检查状态并恢复原进程,也可能导致调度器选择另一个进程。上下文切换则是 CPU 虚拟化中真正改变当前运行进程的核心动作。

小结

CPU 虚拟化依赖三类机制协同工作。系统调用为应用提供受控的内核服务入口;上下文切换保存并恢复进程的寄存器状态,让多个进程能够交替使用 CPU;中断,尤其是定时器中断,让操作系统即使面对非协作进程也能重新取得控制权。

理解这三者的边界很重要。模式切换改变的是处理器当前所处的权限级别,上下文切换改变的是当前运行的进程,中断则提供了从外部或当前执行事件进入内核的机会。

返回目录