什么是上下文切换?进程切换时 CPU 到底在保存什么
这是“操作系统系列教程”的第十九篇。前面几篇我们讲了进程调度、时间片轮转、优先级调度,知道了操作系统如何在多个进程之间分配 CPU。但还有一个关键问题没有回答:当一个进程被换下、另一个进程被换上时,CPU 到底做了什么?那些被中断的进程,它们的执行状态是怎么保存的?答案就是——上下文切换。
一个“接电话”的类比
你正在写代码,突然手机响了。你放下代码,接起电话,聊了十分钟,挂断电话,然后回到电脑前,从刚才停下的地方继续写。
这个过程中,你的大脑做了几件事:
- 记住当前进度:刚才写到哪一行、光标在哪个位置、下一步想写什么;
- 保存思维状态:刚才思考的思路、正在调试的 bug、脑海里的一些临时变量;
- 切换到电话模式:开始和对方交流;
- 恢复之前的进度:挂断电话后,回到刚才写到的位置,继续思考。
操作系统在切换进程时,做的事情几乎一模一样。它需要保存当前进程的执行状态,恢复下一个进程的执行状态。这个过程,就是上下文切换(Context Switch)。
上下文切换是多任务操作系统的核心机制。没有它,多个进程就无法“同时”运行;但切换太频繁,又会带来巨大的性能开销。理解上下文切换,是理解操作系统并发执行的关键。
这篇文章,我们就来把“上下文切换”这件事讲清楚。
说明:本文涉及寄存器、程序计数器、页表、TLB 等概念。初学者如果遇到暂时看不懂的术语,可以先跳过细节,重点理解“上下文切换就是保存当前进程状态、恢复下一个进程状态的过程”这一核心思想。
一、上下文切换到底是什么?
1.1 从“上下文”说起
要理解上下文切换,先要理解什么是上下文(Context)。
上下文是一个进程在某一时刻的执行状态,包括:
- 程序执行到了哪里(程序计数器);
- 正在使用哪些数据(寄存器的值);
- 栈的情况(栈指针、栈内容);
- 内存映射(页表基址);
- 打开的文件(文件描述符表);
- 信号处理状态等。
简单来说,上下文就是进程当前运行状态的全部信息。有了这些信息,进程就可以在任意时刻被暂停,并在稍后被恢复,就像从未被中断过一样。
1.2 上下文切换的定义
给出一个正式的定义:
上下文切换(Context Switch)是操作系统在多任务环境中,把 CPU 从一个进程(或线程)切换到另一个进程(或线程)时,保存当前进程的上下文、恢复下一个进程的上下文的过程。
这个定义里有三个关键点:
- 保存当前进程的上下文:把当前进程的执行状态存储起来;
- 恢复下一个进程的上下文:把下一个进程之前保存的执行状态加载回来;
- 切换 CPU 的执行流:CPU 从执行一个进程变为执行另一个进程。
1.3 上下文切换在系统中的位置
图 1:上下文切换在系统中的位置。 内核负责保存和恢复上下文,调度器负责选择下一个进程。
从图 1 可以看出,上下文切换发生在内核中。进程本身并不知道自己被切换了,它只知道自己被暂停了一下,然后继续执行。
二、上下文切换时到底保存了什么?
这是本文的核心问题。上下文切换时,操作系统需要保存哪些信息?
2.1 CPU 寄存器
CPU 寄存器是进程执行时最核心的状态。包括:
| 寄存器类型 | 作用 |
|---|---|
| 通用寄存器 | 存储临时数据、函数参数、返回值 |
| 程序计数器(PC) | 下一条要执行的指令地址 |
| 栈指针(SP) | 当前栈顶的位置 |
| 基址指针(BP) | 当前栈帧的基址 |
| 程序状态字(PSW) | 标志位、特权级、中断使能等 |
这些寄存器的值决定了进程执行到哪里、正在处理什么数据。保存了这些寄存器,就等于保存了进程的“执行现场”。
2.2 程序计数器(PC)
程序计数器(Program Counter,PC)是最关键的寄存器之一。它存储着下一条要执行的指令的地址。
当进程被中断时,PC 指向的是下一条尚未执行的指令。恢复时,CPU 从 PC 指向的地址继续执行,就好像从未被中断过。
图 2:程序计数器的作用。 进程被中断时,PC 指向下一条未执行的指令。
2.3 栈指针(SP)
栈是进程存放局部变量、函数参数、返回地址的地方。栈指针(Stack Pointer,SP)指向当前栈顶。
当进程被中断时,栈中可能有多层函数调用的信息。保存 SP,就能在恢复时找到正确的栈位置。
2.4 内存管理信息
进程的内存管理信息也需要保存,包括:
- 页表基址:指向该进程的页表;
- 内存限制:代码段、数据段、堆、栈的边界。
在切换进程时,操作系统需要切换页表,让 CPU 使用新进程的地址空间。这也是上下文切换开销大的一个重要原因。
2.5 其他状态
除了上述核心状态,还需要保存:
| 状态 | 说明 |
|---|---|
| 文件描述符表 | 进程打开的文件 |
| 信号处理状态 | 信号处理函数的地址 |
| 进程状态 | 就绪、运行、阻塞 |
| 调度信息 | 优先级、时间片剩余 |
2.6 保存到哪里?
这些上下文信息保存在哪里?答案是进程控制块(PCB)。
每个进程有一个 PCB,操作系统在切换进程时,把当前进程的上下文保存到它的 PCB 中,然后从下一个进程的 PCB 中恢复上下文。
图 3:上下文保存在 PCB 中。 切换时,从当前进程的 PCB 保存,从下一个进程的 PCB 恢复。
三、上下文切换的类型
上下文切换并不是只有一种。根据切换的对象不同,可以分为几种类型。
3.1 进程上下文切换
进程上下文切换是最完整、开销最大的切换。它不仅需要保存和恢复 CPU 寄存器,还需要切换页表、刷新 TLB、切换内存地址空间。
进程上下文切换的开销包括:
- 保存和恢复寄存器;
- 切换页表;
- 刷新 TLB;
- 刷新 CPU 缓存(部分架构)。
3.2 线程上下文切换
线程上下文切换发生在同一进程内的两个线程之间。由于同一进程的线程共享地址空间,线程切换不需要切换页表,也不需要刷新 TLB,因此开销比进程切换小得多。
| 对比项 | 进程切换 | 线程切换 |
|---|---|---|
| 保存寄存器 | 需要 | 需要 |
| 切换页表 | 需要 | 不需要 |
| 刷新 TLB | 需要 | 不需要 |
| 开销 | 大 | 小 |
图 4:进程切换与线程切换的对比。 进程切换需要切换页表和刷新 TLB,线程切换不需要。
3.3 中断上下文切换
中断上下文切换发生在中断处理时。CPU 从用户态切换到内核态,保存用户程序的上下文,执行中断处理程序,然后恢复。
中断上下文切换和进程上下文切换的区别:
- 中断上下文切换不涉及进程切换,只是从用户态到内核态的切换;
- 中断处理程序运行在内核态,使用内核栈;
- 中断处理完毕后,可能返回原进程,也可能触发调度。
3.4 模式切换
模式切换(Mode Switch)是指 CPU 从用户态切换到内核态,或从内核态切换到用户态。系统调用、中断、异常都会触发模式切换。
模式切换和上下文切换的区别:
| 对比项 | 模式切换 | 上下文切换 |
|---|---|---|
| 是否切换进程 | 不一定 | 一定 |
| 是否保存全部上下文 | 不一定 | 一定 |
| 开销 | 较小 | 较大 |
| 触发方式 | 系统调用、中断、异常 | 调度器决定 |
注意:模式切换不一定导致上下文切换。例如,系统调用进入内核后,如果不需要切换进程,处理完直接返回原进程,就没有发生上下文切换。
图 5:模式切换与上下文切换的关系。 系统调用进入内核后,可能只发生模式切换,也可能触发上下文切换。
四、上下文切换的完整流程
上下文切换是一个复杂的过程,涉及硬件和操作系统的紧密配合。
4.1 完整流程图
图 6:上下文切换的完整流程。 从触发调度到恢复新进程,经历保存、选择、恢复三个阶段。
4.2 详细步骤
- 触发调度:时间片用完、进程阻塞、更高优先级进程就绪等;
- 进入内核态:CPU 从用户态切换到内核态;
- 保存进程 A 的上下文:把寄存器、PC、SP、页表基址等保存到 PCB_A;
- 选择进程 B:调度器从就绪队列中选择下一个进程;
- 恢复进程 B 的上下文:从 PCB_B 中读取寄存器、PC、SP 等;
- 切换页表:把 CPU 的页表基址寄存器指向进程 B 的页表;
- 刷新 TLB:使旧的 TLB 条目失效(或使用 ASID 避免刷新);
- 返回用户态:CPU 从内核态切换回用户态;
- 继续执行进程 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。
切换过程:
- 保存 A 的 PC、SP、寄存器到 PCB_A;
- 从 PCB_B 读取 B 的 PC、SP、寄存器;
- 把 CPU 的页表基址寄存器改为 0x2000;
- 刷新 TLB;
- CPU 从 0x00402000 继续执行进程 B。
图 7:上下文切换的具体例子。 从进程 A 的上下文切换到进程 B 的上下文。
五、上下文切换的开销
上下文切换不是免费的。它的开销来自多个方面。
5.1 直接开销
直接开销是上下文切换本身消耗的时间:
| 开销来源 | 说明 |
|---|---|
| 保存寄存器 | 把寄存器写入 PCB |
| 恢复寄存器 | 从 PCB 读取寄存器 |
| 切换页表 | 修改页表基址寄存器 |
| 刷新 TLB | 使旧 TLB 条目失效 |
| 内核代码执行 | 调度器选择进程 |
5.2 间接开销
间接开销是上下文切换对系统性能的间接影响:
| 开销来源 | 说明 |
|---|---|
| CPU 缓存失效 | 新进程的代码和数据可能不在缓存中 |
| TLB 失效 | 地址转换需要重新查询页表 |
| 分支预测失效 | CPU 的分支预测器需要重新学习 |
| 流水线冲刷 | CPU 流水线被清空 |
图 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 是非自愿切换。
图 9:自愿切换与非自愿切换。 自愿切换是进程主动放弃 CPU,非自愿切换是被迫放弃。
七、如何减少上下文切换
上下文切换是必要的,但过多的切换会降低系统性能。以下是一些减少上下文切换的方法。
7.1 调整时间片大小
时间片太小,切换频繁;时间片太大,响应变差。需要根据系统负载调整时间片。
7.2 使用线程而非进程
线程切换比进程切换开销小得多。如果任务之间需要共享数据,使用多线程比多进程更高效。
7.3 减少锁竞争
锁竞争会导致线程频繁阻塞和唤醒,增加上下文切换。可以通过减小锁粒度、使用无锁数据结构等方法减少锁竞争。
7.4 使用 I/O 多路复用
select、poll、epoll 等 I/O 多路复用技术可以让一个线程管理多个 I/O 连接,减少线程数量和上下文切换。
7.5 使用协程
协程是用户态的轻量级线程,切换在用户态完成,不需要进入内核,开销远小于线程切换。
图 10:减少上下文切换的方法。
八、上下文切换的实际应用
8.1 Linux 中的上下文切换
Linux 使用 switch_to 宏完成进程上下文切换。它保存当前进程的寄存器,恢复下一个进程的寄存器,并切换栈和页表。
8.2 容器与上下文切换
容器共享宿主机内核,容器内的进程切换仍然是普通的进程切换。但容器启动比虚拟机快,因为不需要启动完整的内核。
8.3 高性能计算中的上下文切换
在高性能计算中,上下文切换是性能杀手。例如,高频交易系统要求极低的延迟,通常会:
- 使用 CPU 亲和性,把进程绑定到固定 CPU;
- 使用轮询而非中断,避免中断上下文切换;
- 使用用户态网络协议栈,绕过内核。
图 11:上下文切换的实际应用。
九、常见误区澄清
| 误区 | 真相 |
|---|---|
| “上下文切换就是模式切换” | 模式切换是用户态到内核态,上下文切换是进程切换,两者不同 |
| “线程切换和进程切换开销一样” | 线程切换不需要切换页表,开销小得多 |
| “上下文切换没有开销” | 上下文切换涉及保存恢复寄存器、切换页表、刷新 TLB,开销较大 |
| “系统调用一定会导致上下文切换” | 不一定,如果不需要切换进程,只发生模式切换 |
| “上下文切换越多越好” | 过多切换会降低性能,需要平衡 |
| “所有操作系统的上下文切换机制相同” | Linux、Windows 的实现细节不同,但核心思想一致 |
| “上下文切换只发生在进程之间” | 线程之间、中断处理也会发生上下文切换 |
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐



所有评论(0)