当前位置:首页 > 技术学院 > 技术前线
[导读]在现代多任务操作系统中,我们能同时流畅运行十几个应用,背后依靠的就是操作系统不断在不同执行单元之间完成切换的能力。很多人听过“进程是资源分配单位,线程是调度执行单位”的笼统说法,却很少真正理清进程切换和线程切换的完整区别——它们的开销差距可能达到十几倍,甚至会直接决定一个高并发服务的性能上限。从底层的资源变更、执行流程到实际的系统表现,这两种切换行为有着本质的不同,理清这些差异,才能真正理解操作系统调度的核心设计逻辑,也能在实际开发中避开很多不必要的性能陷阱。

在现代多任务操作系统中,我们能同时流畅运行十几个应用,背后依靠的就是操作系统不断在不同执行单元之间完成切换的能力。很多人听过“进程是资源分配单位,线程是调度执行单位”的笼统说法,却很少真正理清进程切换和线程切换的完整区别——它们的开销差距可能达到十几倍,甚至会直接决定一个高并发服务的性能上限。从底层的资源变更、执行流程到实际的系统表现,这两种切换行为有着本质的不同,理清这些差异,才能真正理解操作系统调度的核心设计逻辑,也能在实际开发中避开很多不必要的性能陷阱。

先理清基础概念:进程与线程的底层身份差异

要搞懂两种切换的区别,首先要明确进程和线程在操作系统内核里的身份定位,这是所有切换行为的基础前提。

进程是操作系统分配系统资源的最小单位,每一个进程从诞生开始,就拥有一块完全独立的虚拟地址空间,里面包含了自己的代码段、数据段、堆、栈,还有独立的文件描述符表、信号处理函数表、用户权限标识、打开的设备状态等一系列系统资源。这些资源完全和进程绑定,不同进程之间的地址空间相互隔离,一个进程根本无法直接访问另一个进程的内存地址,彼此之间的资源完全不共享。

而线程是操作系统调度执行的最小单位,它没有自己的独立资源体系,所有线程都必须寄生在一个进程的内部,同一个进程里的所有线程,会100%共享这个进程的全部虚拟地址空间、打开的文件、信号处理配置、用户权限等所有资源。每个线程自己只拥有一小块私有的栈空间、独立的寄存器上下文、线程局部存储数据,以及自己独立的执行流,除此之外没有任何专属的系统资源。

简单来说,进程就像一个完全独立的公司,有自己独立的办公场地、公章、财务体系和资产,和其他公司之间完全隔离;而线程就像同一个公司里的不同员工,他们共享整个公司的所有场地、设备和资源,每个员工只拥有自己独立的工位和手头正在处理的工作,彼此之间可以直接协作,不用走复杂的跨公司对接流程。这个比喻能帮你瞬间理解两种切换行为的核心差异。

进程切换:完整更换整个“资源容器”的全流程操作

进程切换的本质,是操作系统从一个进程的执行上下文,完全跳转到另一个完全独立的进程的执行上下文,这个过程里,操作系统需要把整个进程的“资源容器”全部替换掉,是整个系统里开销最重的切换操作。

当操作系统决定从当前运行的进程A切换到进程B的时候,第一步要做的是把进程A的全部执行现场保存到它的内核栈里,包括所有CPU通用寄存器、程序计数器、栈指针、状态寄存器这些基础执行信息,这部分操作和线程切换的第一步是类似的。但接下来的步骤就是进程切换独有的、开销最大的部分:操作系统必须修改CPU的页表基址寄存器,把整个虚拟地址空间的映射关系,从进程A的页表完全切换成进程B的页表。

这个动作带来的间接性能损耗远大于寄存器保存本身。现代CPU里有一个专门用来加速虚拟地址到物理地址翻译的TLB缓存,里面缓存了最近常用的地址映射关系,一旦切换页表,整个TLB缓存里的所有条目就会全部失效。接下来进程B刚开始运行的时候,几乎每一次内存访问都要触发TLB不命中,CPU必须从内存里读取页表完成地址翻译,这会带来非常大的性能延迟。同时进程A之前已经加载到CPU高速缓存里的指令和数据,也会被进程B的内容逐步覆盖,缓存命中率会在切换后的一段时间里大幅下降,进一步拖慢运行速度。

除了地址空间的切换之外,操作系统还要更新当前进程关联的所有全局资源:把当前进程的文件描述符表、信号处理表、用户权限标识全部替换成进程B对应的配置,还要更新内核里所有和当前运行进程绑定的统计信息、状态标识,整个流程的复杂度非常高。完成所有这些操作之后,操作系统才能把CPU的执行权交给进程B,让它开始运行。

也正是因为进程切换的开销这么大,操作系统的调度器会尽量避免不必要的进程切换,只有当当前进程的时间片完全耗尽、进程主动进入阻塞状态,或者有更高优先级的进程需要抢占CPU的时候,才会触发进程切换。

线程切换:同一“资源容器”内的轻量执行流跳转

和进程切换的重操作完全不同,线程切换的本质,是在同一个进程的内部,从一个执行流跳转到另一个执行流,整个过程完全不需要更换底层的“资源容器”,开销要小得多。

当同一个进程里的线程A切换到线程B的时候,操作系统只需要把线程A保存在CPU寄存器里的执行现场,保存到线程A自己私有的栈空间里,然后把线程B之前保存的寄存器现场,重新加载到CPU的寄存器中,就完成了整个切换的核心操作。整个过程里,当前进程的虚拟地址空间完全没有变化,页表基址寄存器不需要做任何修改,TLB缓存里的所有地址映射条目全部有效,之前已经加载到CPU高速缓存里的指令和数据,绝大多数还能继续被新线程复用。

操作系统完全不需要更新进程级别的全局资源,文件描述符表、信号处理配置、用户权限标识这些全局资源本来就是两个线程共享的,根本不需要做任何修改。整个切换过程的操作量非常小,不需要操作任何和地址空间相关的硬件资源,只需要处理寄存器上下文的保存和恢复,所以切换的耗时只有进程切换的十几分之一甚至几十分之一。

哪怕是跨进程的线程切换——也就是从进程A里的某个线程,切换到进程B里的某个线程,这个过程也会先完成一次完整的进程切换,再完成线程级别的上下文切换,它的整体开销和普通进程切换没有本质区别,并不会因为切换的对象是线程就降低开销。很多新手会误以为所有线程切换都是轻量的,其实只有同一个进程内部的线程切换,才能体现出低开销的优势。

两种切换的实际性能差异,对系统设计的直接影响

进程切换和线程切换的巨大开销差异,会直接决定高并发系统的设计思路,很多实际开发里的性能优化方案,本质上都是在尽可能减少不必要的重开销切换。

比如早期的高并发服务器,很多采用多进程架构,每一个客户端连接分配一个独立的进程,这种架构的稳定性非常高,一个进程崩溃完全不会影响其他进程,但当并发连接数达到几千上万的时候,操作系统频繁在大量进程之间切换,TLB失效和缓存失效带来的性能开销会直接把CPU的有效算力吃掉大半,系统整体吞吐量很难提升。后来的多线程架构,同一个进程里创建大量工作线程,所有线程共享同一个地址空间,线程之间的切换开销非常低,同样的并发连接数下,系统的整体吞吐量能比多进程架构高出好几倍。

但线程切换也不是完全没有开销,当同一个进程里的线程数量太多,远远超过CPU核心数量之后,大量的线程之间频繁切换,也会带来不可忽视的性能损耗。所以现在的高并发系统会进一步演进到协程架构,用户态自己实现轻量级的执行流切换,完全不进入操作系统内核,切换的开销比内核线程切换还要小一个数量级,进一步压榨系统的有效算力。

除此之外,两种切换的差异还会影响程序的内存访问模式。同一个进程里的多个线程切换之后,因为地址空间没有变化,新线程访问的内存数据大概率还留在CPU的高速缓存里,访问速度非常快;而进程切换之后,新进程访问的几乎所有内存数据都不在缓存里,需要从内存甚至磁盘加载,性能差距非常明显。这也是为什么现在很多高性能的中间件、数据库服务,都会优先采用多线程架构,而不是多进程架构,尽可能利用共享地址空间的缓存优势,减少切换带来的性能损耗。

很多人在做性能压测的时候会发现,同样的业务逻辑,用多进程实现的版本,CPU使用率看起来不高,但实际业务吞吐量远低于多线程版本,排查之后才发现大量CPU时间都被进程上下文切换的间接开销吃掉了,真正用来执行业务逻辑的有效时间占比很低,这就是进程切换高开销带来的典型表现。

从切换差异看操作系统的并发设计思路

进程和线程的切换差异,本质上是操作系统在“隔离安全”和“执行效率”之间做的经典权衡。进程切换的高开销,换来了进程之间完全的地址空间隔离,一个进程的内存访问错误、非法操作,完全不会影响其他进程和操作系统内核的稳定性,这是整个系统安全运行的基础。而线程切换的低开销,换来了更高的并发执行效率,让同一个业务逻辑里的多个执行流可以用极低的成本快速切换,最大化利用CPU的算力。

操作系统的调度器在设计的时候,也会针对两种切换的特性做不同的优化:针对进程切换,调度器会尽量延长进程的运行时间片,减少切换的频率,尽可能降低TLB失效和缓存失效带来的性能损耗;针对同进程的线程切换,调度器会在调度的时候优先选择同一个进程里的其他就绪线程,尽量避免跨进程切换,尽可能复用当前已经热起来的TLB和CPU缓存。

理解进程切换和线程切换的完整区别,从来不是为了应付面试里的概念题,而是能让你在做系统设计的时候,清晰判断不同并发架构的性能天花板在哪里,避开那些盲目开大量进程、完全不考虑切换开销的错误设计,真正写出高效稳定的高并发程序。

本站声明: 本文章由作者或相关机构授权发布,目的在于传递更多信息,并不代表本站赞同其观点,本站亦不保证或承诺内容真实性等。需要转载请联系该专栏作者,如若文章内容侵犯您的权益,请及时联系本站删除( 邮箱:macysun@21ic.com )。
关闭