这个问题就像问“跑步中途换个赛道和换个选手,哪个更费事?”
换赛道,也就是系统调用,本质上是同一个线程换了工作场所,从用户态跑到内核态,干完活再跑回来。而换选手,也就是线程切换,则需要保存当前选手的状态,把下一位选手拉上场,还得考虑两人是否跑在同一个跑道上,甚至是否用了同一根接力棒(资源)。
线程切换和系统调用看似相似,但复杂度天差地别。为什么线程切换会比系统调用耗时多得多?
什么是系统调用和线程切换?
系统调用:换个场地,继续干活
系统调用是用户程序向内核请求服务的机制,比如访问文件、分配内存或者进行网络操作。这本质上是同一个线程在“用户态”和“内核态”之间跑来跑去:
- 用户态发起调用:触发一个软中断或 syscall 指令。
- 切换到内核态:进入内核的上下文,执行操作。
- 回到用户态:任务完成后返回,继续执行用户代码。
这里关键点就是:线程不变,只是换了个“场地”。
线程切换:换个人,继续跑步
线程切换是指操作系统让 CPU 从当前线程切换到另一个线程。无论是同一进程内还是跨进程,线程切换都会涉及以下步骤:
- 保存当前线程的上下文:寄存器、程序计数器、栈指针等状态信息。
- 选择下一个要运行的线程:调度器决定下一位“上场选手”。
- 恢复目标线程的上下文:将新线程的状态信息加载到 CPU。
这里关键点就是::线程发生了变化,需要调度器和上下文切换。
为什么线程切换更耗时?
系统调用是“单线程内的上下文切换”,而线程切换是“多线程的切换”
系统调用的上下文切换是同一个线程在用户态和内核态之间切换,它只需要保存用户态的寄存器状态,切换到内核态栈,执行内核代码。切换回用户态,恢复寄存器。
整个过程中,CPU 的核心“逻辑流”没有改变,保存和恢复的状态信息相对简单。而线程切换需要保存一个线程的完整上下文,并加载另一个线程的上下文,包括:
- CPU 上的寄存器状态(程序计数器、通用寄存器)。
- 线程的栈指针和堆栈信息。
- 其他线程相关的 CPU 状态(如浮点寄存器)。
线程切换的上下文保存和恢复远比系统调用复杂。
线程切换需要调度器参与,系统调用不需要
线程切换会触发操作系统的调度器,调度器的作用是从所有线程中挑选下一个要运行的线程。调度器的开销包括:
- 遍历可运行的线程队列:不同操作系统使用不同的调度算法(如时间片轮转、优先级调度),但都需要一定的计算时间。
- 管理等待队列:如果线程因为等待资源而阻塞,调度器还需要将其从等待队列移除,或将其重新加入就绪队列。
系统调用则不涉及线程调度。同一个线程从用户态到内核态的执行过程是线性、连续的,不需要调度器介入。
线程切换可能涉及缓存和 TLB 刷新
线程切换会对 CPU 缓存和 TLB(Translation Lookaside Buffer,地址转换缓存)造成影响:
- 缓存污染(Cache Pollution):线程切换后,新线程需要加载自己的数据,这可能会替换掉当前线程的缓存数据,导致缓存命中率下降。
- TLB 刷新(跨进程线程切换时):如果线程切换涉及到不同进程,操作系统可能需要刷新 TLB(翻译虚拟地址到物理地址的缓存),这会进一步增加开销。
而系统调用在切换时通常不涉及这些问题,系统调用发生在同一个线程内,数据的局部性更好,缓存和 TLB 命中率较高。
线程切换也可能涉及到跨核通信
在多核系统中,线程切换还可能涉及跨核操作:
跨核线程调度:如果线程从一个 CPU 核切换到另一个核运行,可能需要在两个核之间同步缓存数据。
NUMA 架构的内存访问:如果线程被调度到另一个核,可能会访问非本地内存,增加额外延迟。
而系统调用始终由当前线程在当前 CPU 上完成,不涉及跨核切换。
操作系统优化:系统调用比线程切换的优化更多
现代操作系统对系统调用进行了大量优化,以尽量减少开销。例如快速路径优化(Fast Path),对于简单的系统调用(如 getpid
或 read
),操作系统会使用快速路径直接返回结果。还有上下文切换的最小化:系统调用通常只需要保存极少量的上下文信息,减少保存和恢复寄存器的开销。
相比之下,线程切换的复杂性让它难以被完全优化。
线程切换费时,是因为“换人”本身就比“换场地”复杂得多。
如果觉得文章有帮助,记得点赞关注,我是旷野,探索无尽技术!