这是“操作系统系列教程”的第十九篇。前面几篇我们讲了进程调度、时间片轮转、优先级调度,知道了操作系统如何在多个进程之间分配 CPU。但还有一个关键问题没有回答:当一个进程被换下、另一个进程被换上时,CPU 到底做了什么?那些被中断的进程,它们的执行状态是怎么保存的?答案就是——上下文切换。

一个“接电话”的类比

你正在写代码,突然手机响了。你放下代码,接起电话,聊了十分钟,挂断电话,然后回到电脑前,从刚才停下的地方继续写。

这个过程中,你的大脑做了几件事:

  1. 记住当前进度:刚才写到哪一行、光标在哪个位置、下一步想写什么;
  2. 保存思维状态:刚才思考的思路、正在调试的 bug、脑海里的一些临时变量;
  3. 切换到电话模式:开始和对方交流;
  4. 恢复之前的进度:挂断电话后,回到刚才写到的位置,继续思考。

操作系统在切换进程时,做的事情几乎一模一样。它需要保存当前进程的执行状态,恢复下一个进程的执行状态。这个过程,就是上下文切换(Context Switch)。

上下文切换是多任务操作系统的核心机制。没有它,多个进程就无法“同时”运行;但切换太频繁,又会带来巨大的性能开销。理解上下文切换,是理解操作系统并发执行的关键。

这篇文章,我们就来把“上下文切换”这件事讲清楚。

说明:本文涉及寄存器、程序计数器、页表、TLB 等概念。初学者如果遇到暂时看不懂的术语,可以先跳过细节,重点理解“上下文切换就是保存当前进程状态、恢复下一个进程状态的过程”这一核心思想。

一、上下文切换到底是什么?

1.1 从“上下文”说起

要理解上下文切换,先要理解什么是上下文(Context)。

上下文是一个进程在某一时刻的执行状态,包括:

  • 程序执行到了哪里(程序计数器);
  • 正在使用哪些数据(寄存器的值);
  • 栈的情况(栈指针、栈内容);
  • 内存映射(页表基址);
  • 打开的文件(文件描述符表);
  • 信号处理状态等。

简单来说,上下文就是进程当前运行状态的全部信息。有了这些信息,进程就可以在任意时刻被暂停,并在稍后被恢复,就像从未被中断过一样。

1.2 上下文切换的定义

给出一个正式的定义:

上下文切换(Context Switch)是操作系统在多任务环境中,把 CPU 从一个进程(或线程)切换到另一个进程(或线程)时,保存当前进程的上下文、恢复下一个进程的上下文的过程。

这个定义里有三个关键点:

  1. 保存当前进程的上下文:把当前进程的执行状态存储起来;
  2. 恢复下一个进程的上下文:把下一个进程之前保存的执行状态加载回来;
  3. 切换 CPU 的执行流:CPU 从执行一个进程变为执行另一个进程。

1.3 上下文切换在系统中的位置

进程B

内核

进程A

执行中

被中断

保存进程A的上下文

选择进程B

恢复进程B的上下文

被恢复

继续执行

图 1:上下文切换在系统中的位置。 内核负责保存和恢复上下文,调度器负责选择下一个进程。

从图 1 可以看出,上下文切换发生在内核中。进程本身并不知道自己被切换了,它只知道自己被暂停了一下,然后继续执行。

二、上下文切换时到底保存了什么?

这是本文的核心问题。上下文切换时,操作系统需要保存哪些信息?

2.1 CPU 寄存器

CPU 寄存器是进程执行时最核心的状态。包括:

寄存器类型作用
通用寄存器存储临时数据、函数参数、返回值
程序计数器(PC)下一条要执行的指令地址
栈指针(SP)当前栈顶的位置
基址指针(BP)当前栈帧的基址
程序状态字(PSW)标志位、特权级、中断使能等

这些寄存器的值决定了进程执行到哪里、正在处理什么数据。保存了这些寄存器,就等于保存了进程的“执行现场”。

2.2 程序计数器(PC)

程序计数器(Program Counter,PC)是最关键的寄存器之一。它存储着下一条要执行的指令的地址。

当进程被中断时,PC 指向的是下一条尚未执行的指令。恢复时,CPU 从 PC 指向的地址继续执行,就好像从未被中断过。

指令 1

指令 2

指令 3

指令 4
被中断

下一条指令
PC 指向这里

图 2:程序计数器的作用。 进程被中断时,PC 指向下一条未执行的指令。

2.3 栈指针(SP)

栈是进程存放局部变量、函数参数、返回地址的地方。栈指针(Stack Pointer,SP)指向当前栈顶。

当进程被中断时,栈中可能有多层函数调用的信息。保存 SP,就能在恢复时找到正确的栈位置。

2.4 内存管理信息

进程的内存管理信息也需要保存,包括:

  • 页表基址:指向该进程的页表;
  • 内存限制:代码段、数据段、堆、栈的边界。

在切换进程时,操作系统需要切换页表,让 CPU 使用新进程的地址空间。这也是上下文切换开销大的一个重要原因。

2.5 其他状态

除了上述核心状态,还需要保存:

状态说明
文件描述符表进程打开的文件
信号处理状态信号处理函数的地址
进程状态就绪、运行、阻塞
调度信息优先级、时间片剩余

2.6 保存到哪里?

这些上下文信息保存在哪里?答案是进程控制块(PCB)。

每个进程有一个 PCB,操作系统在切换进程时,把当前进程的上下文保存到它的 PCB 中,然后从下一个进程的 PCB 中恢复上下文。

进程B的PCB

进程A的PCB

保存

恢复

寄存器值

程序计数器

栈指针

页表基址

寄存器值

程序计数器

栈指针

页表基址

内核

图 3:上下文保存在 PCB 中。 切换时,从当前进程的 PCB 保存,从下一个进程的 PCB 恢复。

三、上下文切换的类型

上下文切换并不是只有一种。根据切换的对象不同,可以分为几种类型。

3.1 进程上下文切换

进程上下文切换是最完整、开销最大的切换。它不仅需要保存和恢复 CPU 寄存器,还需要切换页表、刷新 TLB、切换内存地址空间。

进程上下文切换的开销包括:

  • 保存和恢复寄存器;
  • 切换页表;
  • 刷新 TLB;
  • 刷新 CPU 缓存(部分架构)。

3.2 线程上下文切换

线程上下文切换发生在同一进程内的两个线程之间。由于同一进程的线程共享地址空间,线程切换不需要切换页表,也不需要刷新 TLB,因此开销比进程切换小得多。

对比项进程切换线程切换
保存寄存器需要需要
切换页表需要不需要
刷新 TLB需要不需要
开销大小

线程切换

保存寄存器

恢复寄存器

进程切换

保存寄存器

切换页表

刷新 TLB

恢复寄存器

图 4:进程切换与线程切换的对比。 进程切换需要切换页表和刷新 TLB,线程切换不需要。

3.3 中断上下文切换

中断上下文切换发生在中断处理时。CPU 从用户态切换到内核态,保存用户程序的上下文,执行中断处理程序,然后恢复。

中断上下文切换和进程上下文切换的区别:

  • 中断上下文切换不涉及进程切换,只是从用户态到内核态的切换;
  • 中断处理程序运行在内核态,使用内核栈;
  • 中断处理完毕后,可能返回原进程,也可能触发调度。

3.4 模式切换

模式切换(Mode Switch)是指 CPU 从用户态切换到内核态,或从内核态切换到用户态。系统调用、中断、异常都会触发模式切换。

模式切换和上下文切换的区别:

对比项模式切换上下文切换
是否切换进程不一定一定
是否保存全部上下文不一定一定
开销较小较大
触发方式系统调用、中断、异常调度器决定

注意:模式切换不一定导致上下文切换。例如,系统调用进入内核后,如果不需要切换进程,处理完直接返回原进程,就没有发生上下文切换。

系统调用

处理完毕, 返回原进程

需要调度

用户态

内核态

保存上下文

选择新进程

恢复上下文

图 5:模式切换与上下文切换的关系。 系统调用进入内核后,可能只发生模式切换,也可能触发上下文切换。

四、上下文切换的完整流程

上下文切换是一个复杂的过程,涉及硬件和操作系统的紧密配合。

4.1 完整流程图

进程BPCB_BPCB_A内核CPU进程A进程BPCB_BPCB_A内核CPU进程A正在执行触发调度(时间片用完/阻塞)保存进程A的寄存器保存程序计数器保存栈指针保存页表基址选择进程B读取进程B的上下文恢复进程B的寄存器恢复进程B的程序计数器切换页表到进程B刷新 TLB从上次中断处继续执行

图 6:上下文切换的完整流程。 从触发调度到恢复新进程,经历保存、选择、恢复三个阶段。

4.2 详细步骤

  1. 触发调度:时间片用完、进程阻塞、更高优先级进程就绪等;
  2. 进入内核态:CPU 从用户态切换到内核态;
  3. 保存进程 A 的上下文:把寄存器、PC、SP、页表基址等保存到 PCB_A;
  4. 选择进程 B:调度器从就绪队列中选择下一个进程;
  5. 恢复进程 B 的上下文:从 PCB_B 中读取寄存器、PC、SP 等;
  6. 切换页表:把 CPU 的页表基址寄存器指向进程 B 的页表;
  7. 刷新 TLB:使旧的 TLB 条目失效(或使用 ASID 避免刷新);
  8. 返回用户态:CPU 从内核态切换回用户态;
  9. 继续执行进程 B:从进程 B 上次中断的地方继续执行。

4.3 一个具体的例子

假设进程 A 正在执行,时间片用完,操作系统切换到进程 B。

进程 A 的上下文:

  • PC = 0x00401000(下一条指令地址);
  • SP = 0x7FFF0000(栈顶);
  • 通用寄存器:RAX = 10, RBX = 20, RCX = 30;
  • 页表基址 = 0x1000。

进程 B 的上下文:

  • PC = 0x00402000;
  • SP = 0x7FFE0000;
  • 通用寄存器:RAX = 100, RBX = 200, RCX = 300;
  • 页表基址 = 0x2000。

切换过程:

  1. 保存 A 的 PC、SP、寄存器到 PCB_A;
  2. 从 PCB_B 读取 B 的 PC、SP、寄存器;
  3. 把 CPU 的页表基址寄存器改为 0x2000;
  4. 刷新 TLB;
  5. CPU 从 0x00402000 继续执行进程 B。

进程B

进程A

保存

恢复

PC=0x401000

SP=0x7FFF0000

页表=0x1000

PC=0x402000

SP=0x7FFE0000

页表=0x2000

内核

图 7:上下文切换的具体例子。 从进程 A 的上下文切换到进程 B 的上下文。

五、上下文切换的开销

上下文切换不是免费的。它的开销来自多个方面。

5.1 直接开销

直接开销是上下文切换本身消耗的时间:

开销来源说明
保存寄存器把寄存器写入 PCB
恢复寄存器从 PCB 读取寄存器
切换页表修改页表基址寄存器
刷新 TLB使旧 TLB 条目失效
内核代码执行调度器选择进程

5.2 间接开销

间接开销是上下文切换对系统性能的间接影响:

开销来源说明
CPU 缓存失效新进程的代码和数据可能不在缓存中
TLB 失效地址转换需要重新查询页表
分支预测失效CPU 的分支预测器需要重新学习
流水线冲刷CPU 流水线被清空

上下文切换开销

直接开销

间接开销

保存/恢复寄存器

切换页表

刷新 TLB

CPU 缓存失效

TLB 失效

分支预测失效

图 8:上下文切换的开销来源。 直接开销和间接开销共同影响性能。

5.3 开销有多大?

上下文切换的开销因硬件和操作系统而异,通常在 几微秒 左右。听起来不多,但如果系统每秒切换上千次,累积起来就很可观。

假设一次上下文切换需要 5 微秒,每秒切换 1000 次,那么每秒有 5 毫秒花在切换上,占 CPU 时间的 0.5%。如果每秒切换 10000 次,就有 5% 的 CPU 时间花在切换上。

5.4 如何测量上下文切换次数?

在 Linux 中,可以使用以下命令查看上下文切换次数:

# 查看系统整体的上下文切换统计
vmstat 1

# 查看特定进程的上下文切换
pidstat -w -p PID 1

vmstat 输出中的 cs 列表示每秒上下文切换次数。pidstat 可以查看特定进程的自愿和非自愿上下文切换。

六、上下文切换的触发时机

上下文切换不是随时发生的,而是在特定时机触发。

6.1 触发时机

触发时机说明是否自愿
时间片用完分时系统中,进程用完时间片非自愿
进程阻塞进程请求 I/O 或等待事件自愿
进程终止进程执行完毕自愿
更高优先级进程就绪抢占式调度非自愿
系统调用进程主动让出 CPU自愿
中断处理中断处理完后可能触发调度非自愿

6.2 自愿切换与非自愿切换

自愿上下文切换(Voluntary Context Switch):进程主动放弃 CPU,如等待 I/O、调用 sleep()、等待锁。

非自愿上下文切换(Involuntary Context Switch):进程被迫放弃 CPU,如时间片用完、被更高优先级进程抢占。

在 Linux 中,可以用 pidstat -w 查看这两种切换:

pidstat -w -p PID 1

输出中的 cswch/s 是自愿切换,nvcswch/s 是非自愿切换。

上下文切换触发时机

自愿切换

非自愿切换

等待 I/O

调用 sleep

等待锁

时间片用完

被抢占

中断触发调度

图 9:自愿切换与非自愿切换。 自愿切换是进程主动放弃 CPU,非自愿切换是被迫放弃。

七、如何减少上下文切换

上下文切换是必要的,但过多的切换会降低系统性能。以下是一些减少上下文切换的方法。

7.1 调整时间片大小

时间片太小,切换频繁;时间片太大,响应变差。需要根据系统负载调整时间片。

7.2 使用线程而非进程

线程切换比进程切换开销小得多。如果任务之间需要共享数据,使用多线程比多进程更高效。

7.3 减少锁竞争

锁竞争会导致线程频繁阻塞和唤醒,增加上下文切换。可以通过减小锁粒度、使用无锁数据结构等方法减少锁竞争。

7.4 使用 I/O 多路复用

select、poll、epoll 等 I/O 多路复用技术可以让一个线程管理多个 I/O 连接,减少线程数量和上下文切换。

7.5 使用协程

协程是用户态的轻量级线程,切换在用户态完成,不需要进入内核,开销远小于线程切换。

减少上下文切换

调整时间片

使用线程而非进程

减少锁竞争

I/O 多路复用

使用协程

图 10:减少上下文切换的方法。

八、上下文切换的实际应用

8.1 Linux 中的上下文切换

Linux 使用 switch_to 宏完成进程上下文切换。它保存当前进程的寄存器,恢复下一个进程的寄存器,并切换栈和页表。

8.2 容器与上下文切换

容器共享宿主机内核,容器内的进程切换仍然是普通的进程切换。但容器启动比虚拟机快,因为不需要启动完整的内核。

8.3 高性能计算中的上下文切换

在高性能计算中,上下文切换是性能杀手。例如,高频交易系统要求极低的延迟,通常会:

  • 使用 CPU 亲和性,把进程绑定到固定 CPU;
  • 使用轮询而非中断,避免中断上下文切换;
  • 使用用户态网络协议栈,绕过内核。

上下文切换应用

Linux: switch_to 宏

容器: 普通进程切换

高性能计算: 减少切换

图 11:上下文切换的实际应用。

九、常见误区澄清

误区真相
“上下文切换就是模式切换”模式切换是用户态到内核态,上下文切换是进程切换,两者不同
“线程切换和进程切换开销一样”线程切换不需要切换页表,开销小得多
“上下文切换没有开销”上下文切换涉及保存恢复寄存器、切换页表、刷新 TLB,开销较大
“系统调用一定会导致上下文切换”不一定,如果不需要切换进程,只发生模式切换
“上下文切换越多越好”过多切换会降低性能,需要平衡
“所有操作系统的上下文切换机制相同”Linux、Windows 的实现细节不同,但核心思想一致
“上下文切换只发生在进程之间”线程之间、中断处理也会发生上下文切换
Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐