进程切换开销比线程切换大的根本原因

进程切换之所以比线程切换昂贵了很多,主要原因有两个:

  1. 需要切换页表
  2. 缓存失效导致切换后命中率降低

1. 切换页表

在 CPU 中有控制寄存器(如 x86 的 CR3)存放当前进程的页表基地址

  • 进程切换时:因为进程之间不共享虚拟地址空间,所以需要修改寄存器值;
  • 线程切换时:因为线程之间共享虚拟地址空间,所以这个寄存器不需要修改。

但实际上,修改寄存器值本身非常快,真正带来性能损失的,是切换后带来的连锁反应。

2. 缓存失效

2.1 TLB(页表缓存)失效

TLB 的作用

CPU 访问内存使用的是虚拟地址,这个虚拟地址需要被翻译成物理地址。物理地址映射关系通常被保存在 4 级页表中,这意味着:读取一个数据,可能需要先进行四次额外的内存访问来查找地址,是巨大的性能损耗。

但有 TLB 会把 CPU 最近翻译过的虚拟地址及其物理地址保存下来,如果 CPU 在短期内再次访问同样的内存地址,就可以直接命中。由于 TLB 是高速缓存,这个地址转换过程几乎没有性能损耗。

失效的代价

当进程切换时,CPU 硬件通常会强制清空 TLB(注:虽然现代 CPU 支持 PCID/ASID 等技术来减少完全冲刷,但上下文切换带来的 TLB 失效依然是主要开销来源)。这会导致新进程开始运行的瞬间,TLB 中几乎没有可用的缓存项,CPU 的每次内存访问,都需要去慢速内存中查页表,这时候是一个 CPU 性能的低谷期。

但如果是线程切换,就完全不会有这种烦恼,因为共享虚拟地址空间,TLB 不会被清空。

2.2 三级缓存 (L1/L2/L3 Cache) 失效

三级缓存的作用

三级缓存是为了解决 CPU 和内存之间的速度差而出现的产物,由 SRAM 组成,比内存的 DRAM 快很多,也贵很多。 缓存的大小从 L1 到 L3 逐渐变大,速度也逐渐变慢。打个比方:

如果 CPU 从 L1 Cache 读取数据需要花 1 秒,那么在 L2 Cache 读取可能要花 3 秒,在 L3 读取可能要花 10 秒,到内存 (RAM) 读取要花 2 分钟。

CPU 内有控制电路负责三级缓存的更新,目的是尽量提高缓存命中的概率(通过提前把 CPU 可能需要的数据加载到三级缓存中),让 CPU 可以尽可能地保持高速运行。

Cache Miss 的开销

进程切换后,CPU 的 L1/L2/L3 Cache 里存的数据对新进程而言就失效了(或不再适用),所以切换后的一段时间内,会产生大量的 Cache Miss。和 TLB 一样,也是不得不从慢速内存中读取数据,这进一步加重了 CPU 性能的低谷。

同样地,因为线程共享虚拟地址空间(包括代码段、堆内存和全局变量等),线程切换后的命中率会比进程切换后的命中率高很多。


总结

造成进程切换代价明显大于线程切换的根本原因,是进程切换导致的缓存失效

CPU 的高速运行非常依赖于缓存的有效性,否则就只能到慢速的内存中读取数据,这会在很大程度上拖累 CPU 的运行速度。从这个角度看,更根本的矛盾可能是:CPU 太快了,而内存太慢了!