Lazy loaded image
OS丨进程、线程与协程
字数 1840阅读时长≈ 5 分钟
2025-10-12
type
Post
status
Published
date
Oct 12, 2025
slug
os2
summary
tags
技术探索
category
icon
password

一、 从隔离到共享:进程与线程的本质

要想理解进程与线程,首先要弄清它们被发明出来是为了解决什么问题。

1. 进程 (Process):为了“隔离”而生

操作系统的答案是:虚拟内存与进程。
操作系统为每个进程分配了独立的虚拟地址空间、页表、文件描述符等资源。进程就像是一个全副武装的堡垒,内部的崩溃不会波及外部。因此,进程的本质,是操作系统进行资源分配和隔离的基本单位。

2. 线程 (Thread):为了“共享与并发”而生

进程的隔离性带来了极高的安全性,但也付出了沉重的代价:进程间通信(IPC)成本高昂,且创建和切换极其耗时。
为了在同一个程序内实现多任务并发,线程应运而生。它依附于进程存在,被剥离了大部分沉重的资源,仅仅保留了 CPU 调度所需的最小上下文。
我们可以用下面这张图来直观感受两者的包含与资源划分关系:
Code snippet
结论: 线程的本质,是 CPU 调度的基本单位。同一进程内的多个线程,天然共享着代码段、数据段和打开的文件等资源。

二、 深度拷问:上下文切换到底在切换什么?

很多人都知道“线程切换比进程切换快”,但快在哪?我们需要下钻到硬件层面。无论是进程还是线程,切换时都需要保存和恢复寄存器、程序计数器(PC)和独立的栈。
核心的性能分水岭在于:虚拟内存空间(页表)的切换。
Code snippet
切换页表带来最致命的后果是导致 TLB(Translation Lookaside Buffer,页表缓存)失效。TLB 缓存一旦被清空,CPU 接下来每次访问内存都不得不去主存中查询慢速的多级页表。这种“隐性惩罚”才是进程切换极其昂贵的根本原因。

三、 突破极限:协程与用户态调度的崛起

当面临海量并发(如 C10K 问题),即使是微秒级的内核线程切换,也显得过于笨重,且线程自带的几 MB 栈空间极易撑爆内存。
协程 (Coroutine) 是系统调度权限的一次“下放”:它的切换完全由用户空间的 Runtime 接管,全程不陷入操作系统内核态,开销从微秒级降到了纳秒级,初始栈通常仅需 2KB。

工程实践:Go 语言的 GMP 模型

以 Go 语言为例,它采用了极其精妙的 M:N 模型(GMP 调度器)。我们可以通过下面这张图来看清它的运转机制:
  • G (Goroutine):用户态协程。
  • M (Machine):操作系统的内核线程,负责真正的物理执行。
  • P (Processor):逻辑处理器,内部维护了一个本地 Goroutine 队列。
核心机制:
P 将 G 和 M 巧妙地解耦。当 M 阻塞(如系统调用)时,P 会带着队列里的 G 转移到新的 M 上(Hand off);当 P 闲置时,它会主动去其他 P 的队列里“偷”任务(Work Stealing)。
在实际参与开源项目(如阅读 dubbo-go 底层网络模型源码)时,能直观感受到这种设计的威力:利用海量的 Goroutine 和 Channel,系统能以极低的资源消耗扛住超高吞吐量的 RPC 并发请求。

四、 跨越孤岛:IPC 进程间通信的核心考量

面对繁杂的 IPC 方式,系统设计时通常重点考量以下两种极端场景:管道(最简单)与共享内存(最快)。
为什么共享内存最快?
我们可以对比一下传统 IPC 与共享内存的数据流向:
像管道、消息队列等机制,数据传输必然经历 发送方用户态 -> 内核态 -> 接收方用户态 的多次内存拷贝。
而共享内存通过映射物理内存到不同进程的虚拟地址空间,实现了真正的零拷贝(Zero Copy),进程直接读写内存,完全绕过了操作系统内核。当然,在工程落地时,为了避免数据竞争,共享内存通常必须配合信号量(Semaphore)等锁机制使用。

结语

从进程的严格隔离,到内核线程的共享并发,再到协程的用户态极致压榨,操作系统的每一次演进,都是在“安全性、开发效率与运行性能”之间寻找当下的最优解。
回到首页