Skip to content

01 · 进程与线程:谁在跑,跑的时候带着什么 ​

这一篇是 OS 模块的地基。后面的内存、锁、IO,主体都是这里的"进程/线程"。

一句话本质 ​

进程 = 一个正在运行的程序 + 它独占的一套资源(地址空间、文件表)。线程 = 进程内部真正被 CPU 执行的那条流水线。

生活类比:餐厅里的订单 ​

  • 程序(存在硬盘上的 exe):菜谱。放在那里不动,不会做菜。
  • 进程:一桌客人下的单。有独立的食材(内存)、独立的进度(执行到哪一步)、独立的账单(资源)。
  • 线程:这一桌订单里的几道菜。共用同一份食材和同一个操作台,但可以几条流水线同时做。
  • PCB(进程控制块):这张订单的小票。内核靠它知道"这桌做到哪了、食材放在哪"。
  • 上下文切换:厨师从做这桌换到做那桌。得先把当前这桌的进度记下来(保存寄存器),再把那桌的进度恢复(恢复寄存器)。

关键差别一句话:换一桌客人(换进程)要清空整个操作台;同一桌换一道菜(换线程)不用。

核心机制 ​

一、进程的状态机 ​

一个进程从创建到消亡,只会走下面这几条边:

← 左右滑动看完整图 →新建就绪运行阻塞终止分配资源完成被调度器选中时间片用完 / 被抢占等待 IO / 锁 / 信号主动退出事件到达
五个状态之间只有这 6 条合法转换。面试最爱追问「阻塞能不能直接到运行」——不能,事件到达后只能先回就绪队列,等调度器再选一次。

三个必考点:

  1. 阻塞 > 就绪,不能直接 > 运行。因为事件到达时 CPU 可能正忙,必须重新排队等调度。这是最高频的细节坑。
  2. 就绪和阻塞的区别:就绪是"只差 CPU",阻塞是"CPU 给我也没用,我在等别的东西"。
  3. 运行 > 就绪是时间片用完(被动),运行 > 阻塞是自己发起 IO(主动)。

二、PCB 里存了什么 ​

+-- PID / 父进程 PID
+-- 进程状态(就绪 / 运行 / 阻塞)
+-- 程序计数器 PC(下一条指令)
+-- 寄存器现场(通用寄存器、栈指针)
+-- 内存管理信息(页表基址)
+-- 打开的文件表
+-- 优先级 / 时间片 / 统计信息

PCB 是进程存在的唯一标志。所谓"创建进程",内核动作就是"分配并初始化一个 PCB"。

三、线程共享什么、独占什么(必背) ​

独占(每个线程一份)共享(同进程内所有线程共用)
程序计数器 PC代码段
寄存器现场全局变量 / 静态变量
栈(函数调用、局部变量)堆(new 出来的对象)
线程 ID、状态打开的文件描述符
错误码 errno信号处理器

一句话记忆:栈是私有的,堆是共享的。所以"局部变量天然线程安全,成员变量不是"——这不是语言规定,是内存布局决定的。

四、上下文切换到底切了什么 ​

进程 A 在跑
   v 时间片用完 / 主动让出
1. 保存 A 的寄存器到 A 的 PCB          < 纯 CPU 操作,很快
2. 切换内核栈
3. 加载 B 的 PCB 里的寄存器
4. 切换页表(CR3 寄存器)              < 贵!会刷 TLB
5. 刷新流水线 / 分支预测器             < 贵!
   v
进程 B 开始跑

进程切换 vs 线程切换的差别就在第 4 步:同进程内的线程共享页表,不用切,所以线程切换比进程切换便宜得多。这是"线程比进程轻量"的真正原因,不是"线程小"这种含糊说法。

五、协程:比线程更轻 ​

线程的切换由内核决定(抢占式),协程的切换由程序自己决定(协作式)。

线程:内核说"你该下了" > 用户程序无法拒绝 > 有内核开销
协程:程序说"我要等 IO 了" > 主动让出 > 纯用户态切换,几十纳秒

代价:协程不能利用多核(一个线程内的协程是串行的),而且一个协程如果死循环不让出,整个线程都卡住。

为什么这么设计 ​

为什么要"进程"这层隔离? ​

反事实:如果没有进程,所有程序共享一个地址空间。

  • A 程序写地址 0x1000,B 程序也写 0x1000 > 互相覆盖 > A 崩了会带崩 B。
  • A 程序有个野指针,能直接改掉 B 的数据库连接 > 一个 bug 变成安全事故。
  • 一个程序内存泄漏,把整台机器吃光 > 所有程序一起死。

进程通过独立的地址空间(02 篇的虚拟内存)解决这一切:每个进程都以为自己独占 0x0 到 0xFFFFFFFF。

为什么进程里还要有线程? ​

反事实:如果只有进程,没有线程。

  • 一个 Web 服务器要同时处理 1000 个请求 > 得开 1000 个进程 > 每个进程一套独立地址空间 > 内存爆炸,切换开销爆炸。
  • 一个进程内想"边算边响应用户点击" > 做不到,只能等算完。

线程让同一份资源被多条流水线共用:1000 个请求共享同一份代码和缓存,切换还便宜。

为什么要协程? ​

反事实:如果用线程做高并发。

  • 每个线程默认栈 1MB,1 万个线程 = 10GB 内存,直接崩。
  • 线程切换要走内核,一次几微秒,1 万线程频繁切换,CPU 全花在切换上。

协程把栈做到几 KB 且按需增长,切换在用户态完成。这就是 Go 的 goroutine 能做到百万级并发的原因。

面试怎么问 ​

Q:进程和线程的区别? 别只答"进程是资源分配单位,线程是调度单位"——这是第一层,人人都会。要接上:

更本质的差别在隔离与开销:进程有独立地址空间,所以隔离强、切换贵(要换页表、刷 TLB);线程共享地址空间,隔离弱(一个线程野指针能搞崩整个进程)、切换便宜。

Q:一个进程最多能创建多少线程?

理论上受三样限制:① 虚拟地址空间(每线程栈默认 1~8MB);② 内核参数(/proc/sys/kernel/threads-max);③ 系统内存。实际瓶颈通常是内存,不是"数量上限"。

Q:线程越多越好吗?

不是。线程数超过 CPU 核数后,多出来的线程只会增加切换开销和锁竞争。CPU 密集型任务,线程数 ≈ 核数;IO 密集型可以多一些(因为线程大部分时间在阻塞)。

Q:fork 之后父子进程共享什么?

共享代码段(只读),不共享数据。但 Linux 用 写时复制(COW):fork 时父子页表指向同一批物理页,且标记为只读;任一方写入时触发缺页,内核才真正复制那一页。 所以 fork 很快,且"读多写少"的场景几乎不花额外内存。

追问链:

进程和线程区别?
  > 线程共享什么?(栈私有、堆共享)
    > 为什么栈私有?(每个线程要有独立的函数调用链)
      > 那局部变量一定线程安全吗?(不一定,如果它是指向堆的引用/被取地址传出去)
        > 那怎么保证共享数据安全?(锁 > 03 篇)
          > 锁多了会怎样?(死锁 > 03 篇;性能下降 > 锁粒度)

和其他知识的连接 ​

自测 3 问 ​

Q1:为什么"阻塞 > 就绪"不能直接变成"运行"?

点开看答案

因为 CPU 可能正被别的进程占着。IO 完成只是"这个进程具备运行条件了",但能不能立刻上 CPU,得由调度器决定。

如果直接跳到运行,就相当于"事件一到就抢占 CPU",会破坏调度策略(比如优先级、公平性),也容易让刚唤醒的进程立刻又被打断。

所以标准路径是:阻塞 > 就绪 > (排队)> 运行。

Q2:进程切换比线程切换贵在哪一步?

点开看答案

贵在切换页表。

线程共享同一套页表,切换时只要换寄存器现场;进程有各自的页表,切换要改 CR3 寄存器指向新页表,这会导致 TLB(地址翻译缓存)失效——之前缓存的"虚拟页>物理页"映射全作废,接下来一段时间每次访存都要重新查页表,程序明显变慢。

此外流水线和分支预测器也要冲刷。所以进程切换的开销通常是线程切换的几倍到几十倍。

Q3:局部变量一定线程安全吗?

点开看答案

不一定,这里有两个坑:

  1. 如果局部变量是引用类型,它本身在栈上,但指向的对象在堆上。多个线程拿到同一个引用,就共享了那个对象。
  2. 如果局部变量的地址被传出去(比如取地址赋给全局变量、或者传给另一个线程),那它就不再"私有"了。

所以准确说法是:栈上的值本身是私有的,但栈上的引用可能指向共享数据。

类比失效处 / 易错点 ​

  • "一桌订单"会让人以为进程之间有共享。实际上进程地址空间完全隔离,共享必须显式申请(共享内存 shm)。
  • "换菜不用清操作台"不完全对。线程切换虽然不换页表,但寄存器现场必须换,而且如果两个线程跑在不同核上,还会涉及缓存行的迁移(伪共享问题)。
  • "线程比进程轻量"不等于"线程可以无限开"。默认栈 1MB,1 万线程就是 10GB 虚拟地址空间,32 位系统直接不够用。
  • fork 之后子进程从哪开始执行? 不是从头,是从 fork 返回处。父子进程的 fork 返回值不同(父得到子 PID,子得到 0),这是区分两者的标准做法。
  • 僵尸进程 vs 孤儿进程:僵尸是子进程死了但父进程没 wait(占着 PCB);孤儿是父进程先死,子进程被 init/systemd 收养。两者方向相反,别混。

✅ 验收清单 ​

  • 能默画进程 5 状态转换图,并说清每条箭头的触发条件(尤其"阻塞只能到就绪")
  • 能说出 PCB 里至少 5 项内容,并解释"为什么 PCB 是进程存在的唯一标志"
  • 能不看资料列出线程独占的 3 样、共享的 3 样,并说出"栈私有堆共享"的由来
  • 能解释进程切换比线程切换贵在哪一步(要说出页表和 TLB,不能只说"开销大")
  • 能用"反事实"讲清:没有进程隔离会出什么事、没有线程会出什么事
  • 能说出 fork 的写时复制机制,以及它为什么让 fork 很快
  • 合上书,用自己的话讲 3 分钟,中途不卡壳