Skip to content

第二章 进程与线程:CPU 的虚拟化起点


1. 从一个问题出发:一个 CPU 怎么"同时"跑一百个程序

上一章说过,操作系统最重要的魔术是虚拟化。这一章讲第一个魔术:CPU 虚拟化

机器上可能只有 4 个 CPU 核心,但你同时开着浏览器、音乐、聊天软件和几十个后台服务。操作系统的做法不是变出更多 CPU,而是让 CPU 快速轮流:每个程序跑几毫秒就被暂停,换下一个上场。切换足够快,人的感官就以为大家在同时运行。

要实现"暂停一个程序、以后还能原样恢复",操作系统必须回答一个问题:一个正在运行的程序,到底由哪些状态组成?暂停时要把什么存起来? 对这个问题的完整回答,就是"进程"这个概念。

2. 程序 vs 进程:静态与动态的区别

程序是躺在磁盘上的一个可执行文件,是一段静态的指令和数据,像一份菜谱。

进程(process)是程序被加载进内存、跑起来之后的那个"活的执行过程",像照着菜谱做菜的整个动态过程——有进度、有中间状态、会占用炉灶和食材。

对比项程序进程
性质静态的文件动态的执行过程
生命周期长期存在于磁盘有创建、运行、消亡的过程
与资源的关系不占用 CPU、内存占用内存、CPU 时间、打开的文件等
数量对应一个程序可对应多个进程,同一程序可同时运行多份

考试高频表述:"进程是程序的一次执行","进程 = 程序 + 数据 + PCB"。要点是动态性:进程有生命周期,程序没有。同一个浏览器程序开三个窗口,可能就是三个进程。

进程的几个教材级特征:动态性(最基本特征)、并发性、独立性(资源分配和调度的基本单位)、异步性(走走停停、推进速度不可预知)。

3. 进程由什么组成:PCB 与地址空间

3.1 PCB:进程的"身份证 + 病历本"

操作系统要管理成百上千个进程,必须为每个进程建立一份档案,这份档案叫进程控制块(PCB, Process Control Block)。PCB 是进程存在的唯一标志——创建进程本质上就是创建 PCB,撤销进程就是回收 PCB。

PCB 里主要记四类信息:

类别内容举例用途
标识信息进程号 PID、父进程号、用户认出这是谁
现场信息程序计数器、通用寄存器、状态字暂停后能原样恢复
控制信息当前状态、优先级、队列指针调度时决定谁上场
资源清单内存指针(页表位置)、打开的文件、占用的设备记账与回收

一句话点透:CPU 虚拟化的秘密全在 PCB 里——把寄存器现场存进 PCB,进程就"冻结"了;把 PCB 里的现场装回寄存器,进程就"解冻"继续跑,它自己毫无察觉。

3.2 地址空间布局:进程眼中的内存

每个进程都以为自己独占一条从低地址到高地址的完整内存,典型布局如下:

记两个方向:栈向低地址方向生长,堆向高地址方向生长,两者相向而行。代码区只读且可被同一程序的多个进程共享。这张图第五章讲内存管理时还会用到。

4. 进程的状态:三态与五态模型

4.1 为什么至少需要三个状态

进程不是"要么在跑、要么没跑"这么简单。关键洞察是:没在跑的进程分两种情况——一种是"万事俱备只差 CPU",另一种是"就算给它 CPU 也没法跑,因为它在等磁盘、等输入"。这两种必须分开,否则调度器会把 CPU 白白分给一个还在等数据的进程。

于是有了三个基本状态:

  1. 运行态:正占着 CPU 执行。单 CPU 下同一时刻最多一个进程处于运行态。
  2. 就绪态:只差 CPU,其他条件全齐,排队等调度。
  3. 阻塞态(等待态):在等某个事件(如 I/O 完成),此时给它 CPU 也没用。

4.2 状态迁移图:每条边为什么存在

逐条边解释"为什么":

迁移触发原因主动还是被动
就绪 → 运行调度程序选中它被动,由操作系统决定
运行 → 就绪时间片到、被抢占被动,进程自己并不想让
运行 → 阻塞进程自己请求等待某事件主动,只能由运行中的进程自己发起
阻塞 → 就绪所等事件发生被动,由中断处理程序或其他进程唤醒

哪些迁移不可能发生,这是选择题最爱:

  1. 阻塞 → 运行:不可能直接发生。事件完成后必须先回就绪队列排队,能不能马上上 CPU 要看调度。
  2. 就绪 → 阻塞:不可能。阻塞是"运行中发现要等东西"才发生的,一个没在运行的进程无从提出等待请求。

一句话点透:变阻塞必是自己的选择,脱离阻塞必靠别人唤醒

4.3 五态模型:加上创建与终止

三态之外,再加"创建态"(正在分配 PCB 和资源,尚未准备好)和"终止态"(已结束但 PCB 尚未回收,留着给父进程查退出状态),就是五态模型。有的教材还引入挂起(进程被换出到磁盘),形成七态,考试一般点到为止:挂起是"进程整体暂时搬出内存",与阻塞(在内存中等事件)是两个维度。

5. 进程的创建与终止

创建一个进程,操作系统要做四件事:分配唯一 PID 并建立 PCB → 分配内存等资源 → 加载程序代码和数据 → 把 PCB 挂入就绪队列。典型创建时机:系统初始化、用户命令启动、正在运行的进程调用"创建子进程"的系统调用(如 UNIX 的 fork)、批处理作业被调度装入。

终止分三类:正常结束(程序跑完主动退出)、异常结束(除零、非法内存访问等错误被内核强制终止)、外界干预(用户或父进程发出终止命令)。终止时内核回收其内存、关闭其文件,最后撤销 PCB。

父子关系值得记一句:子进程可以继承父进程的资源;父进程被撤销时,有的系统会级联撤销全部子孙进程

6. 线程为什么被发明

进程用了很多年之后,人们发现两个痛点:

痛点一:进程切换太贵。 切换进程要保存和恢复全套现场,还要切换地址空间(换页表、冲刷 TLB 缓存),开销可观。如果一个网页浏览器想同时"下载图片 + 渲染页面 + 响应点击",为每件事开一个进程,切换成本高得离谱。

痛点二:进程之间共享数据太难。 进程的设计初衷就是互相隔离,各有各的地址空间。可上面三件事偏偏要频繁共享同一份网页数据,跨进程传数据既慢又麻烦。

反思一下会发现,进程这个概念其实捆绑了两种职责:

  1. 资源的容器:地址空间、打开的文件、设备——这些是"财产"。
  2. 执行的流:程序计数器、寄存器、栈——这些是"干活的人"。

线程(thread)就是把这两者拆开:让一个资源容器(进程)里可以住多个执行流(线程)。同一进程的多个线程共享地址空间和打开的文件,但各自有自己的程序计数器、寄存器和栈。

一句话点透:进程是资源分配的基本单位,线程是 CPU 调度的基本单位——这句话是全章最高频考点,务必一字不差记住。

7. 进程 vs 线程:全维度对照表

对比维度进程线程
定义角色资源分配和保护的基本单位处理机调度的基本单位
地址空间各自独立同进程内所有线程共享
拥有资源拥有内存、文件、设备基本不拥有资源,只有栈、寄存器等运行必需品
切换开销大:换地址空间、刷缓存小:不换地址空间,只换现场和栈
通信方式需要专门的进程间通信机制,如管道、消息、共享内存直接读写共享变量即可,但需要同步
隔离与健壮性一个进程崩溃通常不影响他人一个线程崩溃可能拖垮整个进程
创建销毁开销

注意最后一行的反面含义:线程共享带来了效率,也带来了脆弱——隔离性是进程的优势,共享性是线程的优势,两者是同一枚硬币的两面。第四章讲并发问题时,祸根正是线程间的共享。

8. 用户级线程 vs 内核级线程

线程可以由谁来管理?两种方案:

用户级线程:线程的创建、切换、调度全部由用户空间的线程库完成,内核根本不知道线程的存在,只看得见进程。

内核级线程:线程由内核直接管理,内核为每个线程建立控制块,调度以线程为单位。

对比项用户级线程内核级线程
内核是否感知不感知感知
切换开销极小,不用进内核态较大,需要陷入内核
一个线程阻塞时整个进程全部线程被拖住只阻塞该线程,其他线程照常调度
能否利用多核不能,内核只给该进程一个 CPU能,多个线程可同时在多核上跑
调度自主权应用可自定义调度策略由内核统一调度

记忆抓手:用户级线程"便宜但瞎"——切换便宜,但内核瞎、看不见线程,一人阻塞全家等待;内核级线程"贵但明"——切换贵,但内核明察秋毫,能并行、能单独阻塞。现代主流系统(Linux、Windows)以内核级线程为主,也存在两者组合的多对多模型。

9. 上下文切换到底切换了什么

上下文(context)就是"恢复执行所需的全部现场"。上下文切换的完整流程:

切换内容按"进程切换"与"线程切换"区分:

切换内容同进程内线程切换进程切换
寄存器、程序计数器、栈指针要切要切
内核中的调度信息要切要切
地址空间(页表)不切,这正是线程切换便宜的原因要切
TLB、缓存的有效性基本不受影响可能大量失效,隐性成本高

另外注意:上下文切换本身是纯开销——切换期间 CPU 没有为任何用户程序干活。切换太频繁,系统就会把大量时间浪费在"换人"而不是"干活"上,这为第三章讨论时间片大小埋下伏笔。

10. 本章要点回顾

主题必须记住的结论
程序与进程程序静态、进程动态;进程 = 程序的一次执行
PCB进程存在的唯一标志,存放标识、现场、控制、资源四类信息
三态模型就绪差 CPU,阻塞差事件;阻塞不能直达运行,就绪不能直达阻塞
主动与被动运行→阻塞是进程主动的,其余迁移都是被动的
线程的动机进程切换贵 + 共享数据难,拆出"执行流"单独成为调度单位
核心口诀进程是资源分配的基本单位,线程是 CPU 调度的基本单位
用户级 vs 内核级线程用户级便宜但一人阻塞全家等、不能用多核;内核级相反
上下文切换线程切换不换地址空间,所以便宜;切换本身是纯开销

11. 做题提醒

  1. "阻塞态的进程等到事件完成后直接投入运行"——错,必须先回就绪态排队。
  2. "就绪态的进程可以转为阻塞态"——错,只有运行中的进程才能提出等待请求。
  3. 单 CPU 系统中,处于运行态的进程最多 1 个,但就绪、阻塞态的进程可以有多个。
  4. "同一进程的各线程共享栈空间"——错,栈是线程私有的;共享的是代码、全局数据、堆和打开的文件。
  5. 时间片用完后进程进入就绪态而不是阻塞态——它没有在等任何事件,只是暂时没了 CPU。
  6. 判断切换开销的题目:同进程内线程切换 < 跨进程切换,根本原因是是否需要切换地址空间