进程与线程⚓︎
摘要
本文从一般程序的执行模型出发,说明进程地址空间、fork、exec、写时复制、线程共享边界、用户线程与内核线程,以及上下文切换成本如何共同决定系统中的执行实体组织方式。
问题背景⚓︎
理解进程与线程时,最重要的问题不是术语区别本身,而是“谁来执行这些逻辑”。只要任务足够多、等待足够频繁或共享足够复杂,执行实体的数量、共享方式和切换频率就会直接影响系统表现。
因此,进程与线程并不是两个孤立名词,而是程序运行模型的两个基本选择。真正需要回答的问题是:
进程首先解决的是隔离⚓︎
进程是资源分配的基本单位。一个进程通常拥有独立的虚拟地址空间、打开的文件描述符集合、页表、信号处理状态以及其他内核资源。抽象地看,可以写成:
这意味着进程模型首先提供的是隔离性。某个进程的内存越界通常不会直接破坏其他进程的地址空间,权限边界也更自然。因此 shell、服务进程、批处理程序乃至图形应用都天然以进程为基本承载单位。
但这种隔离并不是免费的。每增加一个进程,就会增加地址空间管理、页表维护、调度状态和资源复制成本。当任务规模不断上升时,隔离性带来的收益未必能覆盖执行实体膨胀的代价。
fork 与 exec 是两种不同动作⚓︎
fork 复制当前进程,得到一个新的子进程;exec 则在当前进程上下文中装入新的程序映像,用新的代码段、数据段和入口点替换原来的地址空间内容。它们经常一起出现,但职责完全不同。
在 Unix 风格系统里,fork 常被用来派生子进程,exec 则更常见于 shell、作业系统或需要启动新程序的场景。理解这一区别很重要,因为很多“进程模型很重”的判断,其实针对的是频繁 fork 与执行体数量失控,而不是 exec 语义本身。
写时复制降低了 fork 的初始代价⚓︎
Linux 的 fork 并不会在调用瞬间把整个地址空间的物理页逐页拷贝一份。它依赖写时复制(Copy-On-Write, COW)机制,让父子进程先共享同一批物理页,并在页表中把对应页标记为只读共享。只有当任意一方尝试写入时,才会触发缺页异常,由内核为写入方分配新页并复制旧内容。
因此,fork 的初始成本并不是:
而更接近页表复制与进程元数据创建的成本。这也是为什么预 fork 服务、进程池模型在某些场景仍然可行。
但 COW 只是把复制成本延后,并没有消除复制成本。如果子进程启动后很快写入大量堆内存、缓存或全局状态,那么这些共享页仍然会被逐步拆开,实际代价仍然会发生。要理解这一点背后的页级机制,需要回到 虚拟内存。
线程把重点从隔离转向共享⚓︎
线程是调度的基本单位。与进程不同,同一进程内的多个线程共享地址空间、打开文件、堆和大多数进程级资源,但每个线程仍然保留自己的寄存器上下文、栈和调度状态。可以近似写成:
这使线程之间共享数据与通信更加直接,不需要像多进程那样显式经过 IPC 或跨地址空间复制。因此线程更容易实现线程池、任务队列、共享缓存和统一状态管理。
但共享带来的并不只是高效,也意味着多个执行流会同时读写同一批状态。于是问题从“如何复制资源”变成“如何控制并发访问”。这正是 锁与同步 一文讨论的核心。
用户线程、内核线程与协作边界⚓︎
用户线程由用户态运行时管理,切换成本较低,也更容易和协程、调度器结合;但如果底层阻塞调用无法被运行时接管,那么一个阻塞点就可能拖住整个调度实体。内核线程由操作系统直接感知和调度,能够与阻塞系统调用自然配合,但创建、销毁和切换成本更高。
现代程序常见的组合并不是二选一,而是“以内核线程提供与内核交互的基础执行体,再在用户态叠加线程池、协程或其他调度结构”。因此讨论线程模型时,更准确的问题不是“用户线程好还是内核线程好”,而是“应用层调度与内核调度分别负责什么”。
线程栈与实体规模⚓︎
每个线程都需要独立的栈,用来保存函数调用帧、返回地址和局部变量。当线程数量上升时,栈空间开销会线性增长:
这也是“任务数远大于线程数”经常成为必要设计的原因之一。即使线程大部分时间只是在等待 I/O,没有真正执行很多计算,栈空间和调度状态仍然必须长期存在。
因此,控制执行实体数量不是一种优化技巧,而是程序设计中的基础约束。某些 I/O 模型之所以重要,就在于它们让线程数不必与等待对象数量直接绑定,参见 I/O 模型。
上下文切换为什么会成为系统成本⚓︎
无论是进程切换还是线程切换,系统都需要保存旧执行流的寄存器状态,恢复新执行流的上下文,并付出调度器参与、缓存污染和可能的 TLB 影响。进程切换通常更重,因为地址空间相关状态也会发生变化;线程切换虽然更轻,但频率一高,同样会侵蚀吞吐。
因此需要关注的并不是“线程切换比进程切换轻”这一句结论本身,而是:
是否仍然处于系统可承受范围内。对高并发程序而言,这往往比单个切换动作的微观开销更重要。
与系统设计的关系⚓︎
从系统设计角度看,进程与线程这一主题真正回答的是:在隔离、共享和调度成本之间,程序应当如何组织执行实体。
- 如果优先考虑隔离、崩溃边界和权限控制,多进程模型更自然。
- 如果优先考虑共享内存、降低跨实体通信成本,多线程模型更直接。
- 如果任务规模远大于 CPU 核数,通常需要把执行实体数量控制在远小于任务数量的级别,并结合线程池、协程或其他调度方式。
因此,进程与线程并不是纯粹的“操作系统基础概念”,而是程序如何在隔离性、共享性和调度成本之间取得平衡的前提条件。继续往下看共享状态的协调成本,可阅读 锁与同步。