--- title: "02-进程与线程:从程序到并发执行" aliases: - 进程与线程 created: 2026-08-29 tags: - 基础与理论 - 操作系统 - "408" --- # 进程与线程:从程序到并发执行 > 本篇对应王道第二章上半(原 08 篇)。四个问题贯穿全篇:**为什么要引入进程、进程是什么、进程由什么组成、进程是怎么解决问题的**。最后是它的续集——线程。 ## 一、为什么要引入进程 单道程序时代,程序独占整台机器,从头跑到尾,结果只取决于程序本身——这叫**封闭性**。 多道程序并发之后,假设崩了: - 程序走走停停(时间片到了让出 CPU、等 IO 阻塞) - 程序之间相互制约(抢资源、要同步) - **失去封闭性**:不同速度下推进,执行结果可能不同 为了描述和控制这种"并发执行的程序",让并发性和共享性真正落地,引入了**进程**。 ## 二、进程是什么 ### 进程实体:程序段 + 数据段 + PCB 为了使参与并发执行的每个程序都能独立运行,必须配置一个专门的数据结构——**进程控制块(PCB)**。 系统利用 PCB 描述进程的基本情况和运行状态,进而控制和管理进程。**程序段、相关数据段、PCB 三部分构成进程实体(进程映像)**。 两个关键认知: - **创建进程 = 创建 PCB,撤销进程 = 撤销 PCB**——操作的都是 PCB - **PCB 是进程存在的唯一标志**——系统只有通过 PCB 才能感知到进程的存在 - 进程映像是**静态的**,进程是**动态的**(一个是数据结构,一个是运行过程) 定义(引入进程实体之后): > 进程是进程实体的运行过程,是系统进行资源分配和调度的一个独立单位。 ### 四大特征 | 特征 | 一句话 | 备注 | | --- | --- | --- | | **动态性** | 有创建、活动、暂停、终止的生命周期 | **最基本的特征** | | **并发性** | 多个进程同存于内存,一段时间内同时运行 | 引入进程的目的 | | **独立性** | 独立运行、独立获取资源、独立接受调度的基本单位 | 没建 PCB 的程序不配 | | **异步性** | 按各自独立的、不可预知的速度推进 | 可能导致结果不可再现 → 必须配同步机制 | ## 三、进程的状态与转换 ### 五状态模型 三种基本状态 + 两个过渡状态: | 状态 | 含义 | 备注 | | --- | --- | --- | | **运行态** | 正在 CPU 上跑 | 单处理机中每个时刻**最多一个**(但可以零个,见盲点) | | **就绪态** | 万事俱备,**只差 CPU** | 可能有多个,排成就绪队列 | | **阻塞态** | 等 CPU **以外**的资源或事件(等 IO 完成、等资源可用) | 按阻塞原因排多个阻塞队列 | | **创建态** | 正在被创建,还没转到就绪态 | PCB 申请好了但资源没到位(比如内存不足) | | **终止态** | 正在从系统中消失 | 先置终止态,再做资源释放回收 | **就绪态和阻塞态的区别是本章第一道坎**:就绪差的是 CPU,阻塞差的是 CPU 以外的东西——所以 CPU 空闲阻塞进程也不能跑,它得先变成就绪。 另一个体感区别:**就绪↔运行切换非常频繁**(分时轮转,毫秒级);阻塞相关的切换就少得多(资源分配和 IO 等待往往很长)。 ![[image-97daf2d3.png]] ### 状态转换 | 转换 | 触发 | 方向性 | | --- | --- | --- | | 就绪 → 运行 | 被调度,获得 CPU | 被动(调度程序选的) | | 运行 → 就绪 | 时间片用完;或被更高优先级进程抢占 | 被动 | | 运行 → 阻塞 | 请求资源/等待事件(系统调用形式) | **主动行为** | | 阻塞 → 就绪 | 等待的事件到来(IO 完成、中断结束) | **被动**(由中断处理程序协助) | > ⚠️ 记住两个"唯一":运行→阻塞是**主动**的(进程自己调用的);阻塞→就绪是**被动**的(要相关进程协助)。另外**阻塞不能直接跳到运行**,必须先进就绪队列排队。 ## 四、进程由什么组成:PCB ### PCB 里装了什么 | 信息类别 | 内容 | 干什么用 | | --- | --- | --- | | **进程描述信息** | 进程标识符(唯一 PID)、用户标识符 | 标识进程、归属用户(共享和保护服务) | | **进程控制和管理信息** | 进程当前状态、优先级 | 处理机分配调度的依据 | | **资源分配清单** | 内存地址空间/虚拟地址空间状况、打开文件列表、所用 IO 设备 | 说明进程占着哪些资源 | | **处理机相关信息** | 各寄存器的值(处理机上下文) | 切换时保存现场,恢复时从断点继续 | 整个生命周期中系统都靠 PCB 控制进程: 1. 调度前:从 PCB 查现行状态和优先级 2. 调度后:按 PCB 保存的处理机状态恢复现场,按 PCB 记的地址找到程序和数据 3. 运行中:同步、通信、访问文件都要碰 PCB 4. 暂停时:断点的处理机环境存回 PCB ### PCB 怎么组织 系统里 PCB 很多(就绪的、各种原因阻塞的),常用两种组织方式: - **链接方式**:同一状态的 PCB 链接成一个队列(就绪队列、多个阻塞队列)——主流 - **索引方式**:同一状态的进程组织在索引表中,表项指向 PCB(就绪索引表、阻塞索引表……) ### 程序段与数据段 - **程序段**:能被调度到 CPU 执行的程序代码。可以被**多个进程共享**——多个进程运行同一个程序 - **相关数据段**:程序加工的原始数据,也可以是执行时产生的中间/最终结果 ## 五、进程怎么被控制:原语 进程控制 = 创建新进程、撤销已有进程、实现状态转换。做这些事的程序段称为**原语**——要么全做要么全不做(原子性靠**关中断**实现,见 [[01-操作系统概述|操作系统概述]] 里的内核部分)。 ### 创建原语(创建态 → 就绪态) 允许一个进程创建另一个进程:创建者是**父进程**,被创建的是**子进程**,子进程继承父进程的所有资源;子进程被撤销时资源归还父进程,父进程被撤销时所有子进程同时撤销——**这点学过面向对象后应该很容易理解**。 触发场景:用户登录、作业调度、系统提供服务、应用请求。 流程: 1. 分配唯一进程标识号,申请空白 **PCB**(PCB 是有限的,申请失败 = 创建失败) 2. 分配运行所需资源(内存、文件、IO 设备……)——**资源不足不是创建失败,是进创建态等资源** 3. 初始化 PCB(标志信息、处理机状态、控制信息、优先级) 4. 插入就绪队列,等调度 ### 终止原语(→ 终止态) 三种终止原因:**正常结束**(任务完成)、**异常结束**(存储区越界、保护错、非法指令、运行超时、IO 故障……)、**外界干预**(操作员/OS 干预、父进程请求或终止)。 流程: 1. 按标识符检索出 PCB,读出状态 2. 若处于运行态,立即终止执行,CPU 让给其他进程 3. 若还有子孙进程,**全部连带终止** 4. 所有资源归还给父进程或操作系统 5. PCB 从所在队列删除(涉及随机删除 → 一般用链表实现队列) ### 阻塞原语与唤醒原语 - **阻塞(block)**:正在运行的进程期待的某些事件未发生(资源请求失败、等 IO 完成、新数据未到达),进程**自己调用阻塞原语**,运行态 → 阻塞态。再次印证:**阻塞是主动行为,只有运行态能变阻塞** - **唤醒(wakeup)**:期待的事件出现时(IO 完成、数据到达),由**相关进程**(释放 IO 设备的进程、提供数据的进程)调用唤醒原语:等待队列中找到 PCB → 移出置就绪态 → 插入就绪队列 > ⚠️ Block 和 Wakeup 是一对作用相反的原语,**必须成对使用**——否则进程会因不能被唤醒而永久阻塞。 ## 六、进程怎么说话:进程通信 ### 低级通信:PV 操作 信号量 + P(尝试,申请资源,不可用则阻塞)/V(增加,释放资源,可能唤醒等待者),用于**互斥**(不同时碰共享资源)和**同步**(控制执行顺序)。传的只是信号不是数据,所以是"低级"通信。细节全在 [[04-同步与互斥]] 里展开。 ### 高级通信:传大量数据 #### 共享存储 通信进程之间存在一块**可直接访问的共享空间**,读写这片空间实现信息交换。 ![[image-bee835bb.png]] - **低级方式**:基于数据结构的共享(如共享空间里只放一个固定长度的数组)——速度慢、限制多 - **高级方式**:基于存储区的共享(划出一块存储区,形式、位置、存取速度都由进程控制) **操作系统只负责提供共享空间和同步互斥工具(如 PV 操作),数据怎么读写交换由用户自己安排**。 直观理解:AB 之间有个 **C 麻袋**,A 把东西放进麻袋,B 从麻袋里拿——但 B 不能直接从 A 手里拿,A 也不能直接从 B 手里拿,一切通过麻袋中转。而且用麻袋时双方要约定好规矩(互斥访问),不然两个人同时伸手就乱了。 注意:进程空间一般是独立的,进程运行期间不能访问别的进程的空间,**想让两个进程共享空间必须通过特殊的系统调用**;而进程内的线程是**天然共享**进程空间的。 #### 消息传递 进程之间没有可直接访问的共享空间时,用操作系统提供的**发送/接收两个原语**,以格式化消息(消息头 + 消息体)为单位交换数据。 ![[image-93aa69d6.png]] - **直接通信**:消息直接挂到接收进程的消息缓冲队列上 - **间接通信**:消息先发到中间实体——**信箱**,接收进程再从信箱取 写信比喻:A 要告诉 B 某些事情,就写信让邮差送。**直接通信就是邮差把信直接送到 B 手上;间接通信就是 B 门口有个邮箱,邮差把信放进邮箱**。后者广泛应用于计算机网络,也隐藏了通信细节、对用户透明,是当前应用最广泛的 IPC 机制——微内核 OS 的微内核与服务器之间用的就是消息传递,也因为它能很好支持多处理机、分布式系统和计算机网络。 #### 管道通信 管道是连接读写进程的一个**特殊共享文件**(pipe 文件),本质是**内存中一个固定大小的缓冲区**(Linux 中通常 4KB)。两个进程按**生产者—消费者**方式通信:生产者向一端写,消费者从另一端读,数据**先进先出**。 ![[image-d1e80d39.png]] - 管道非空 → 读进程可读;读空 → 读进程阻塞,等写进程写入新数据再唤醒 - 管道不满 → 写进程可写;写满 → 写进程阻塞,等读进程读出数据再唤醒 - 从管道读数据是**一次性操作**:数据一旦被读走就释放空间(不像文件可以反复读) - **半双工**:普通管道只允许单向通信,父子进程要双向通信就得**定义两个管道** 管道机制必须提供三方面协调能力:**互斥**(同一时间只能一个进程操作管道)、**同步**(空等读、满等写的协调)、**确定对方存在**(对方没了别傻等,避免"悬挂"错误)。 和普通文件的区别:**固定缓冲区大小**(防止数据无限增长,资源可控)+ **阻塞机制**(同步读写双方)。管道还有**继承性**:父进程创建的管道,子进程创建时自动继承(类似文件描述符),所以管道常用于父子进程通信。 ## 七、线程:更小的调度单位 ### 为什么要引入线程 引入进程是为了让多道程序并发执行,提高资源利用率和吞吐量;而**引入线程是为了减小程序并发执行时付出的时空开销,提高并发性能**。 进程切换动辄换上下文(PCB 四大类信息、地址空间……),但如果是**同一个进程内部的线程切换**,地址空间都不用动,只需很少的时空开销。 ### 线程是什么 最直接的理解:**"轻量级进程"**。它是基本的 CPU 执行单元,也是程序执行流的最小单元,由**线程 ID、程序计数器、寄存器集合和堆栈**组成。 - 线程自己**不拥有系统资源**(只拥有一点运行中必不可少的东西),但可与同进程的其他线程**共享进程的全部资源** - 一个线程可以创建和撤销另一个线程;同进程内多线程并发执行 - 既然并发,线程间也相互制约 → 线程也有**就绪、阻塞、运行**三种基本状态,转换关系同进程 **引入线程后,进程的内涵变了**: - 以前:进程是资源分配**和调度**的独立单位 - 现在:**进程只作为除 CPU 以外系统资源的分配单元,线程作为处理机(CPU)的分配单元** ### 线程 vs 进程 | 维度 | 进程 | 线程 | | --- | --- | --- | | 调度 | 引入线程前是调度基本单位 | **处理机调度的基本单位**;同进程内线程切换不引起进程切换 | | 并发性 | 进程间并发 | 进程间、进程内多线程、甚至跨进程的线程都能并发 | | 拥有资源 | **拥有资源的基本单位** | 不拥有系统资源,但可访问隶属进程的资源 | | 独立性 | 独立地址空间,除共享全局变量外其他进程不可见 | 同进程线程共享地址空间;对**其他进程**的线程不可见 | | 系统开销 | 创建/撤销要分配回收 PCB 及资源,切换要换整个上下文 | 切换只保存设置少量寄存器;同进程线程同步通信天然容易 | > 💡 为什么线程必须"不拥有资源"?如果线程也是拥有资源的单位,切换线程就要较大的时空开销,线程这个概念也就失去意义了——它的价值恰恰建立在"轻"上。 ### 线程控制块 TCB 与线程的组织 系统为每个线程配一个 **TCB**,记录:线程标识符、寄存器集(PC、SP 等,切换时保存恢复现场)、线程状态、线程堆栈(函数调用返回地址、局部变量)、优先级、链接指针(指向同进程其他线程或队列中的下一个线程)、同步机制信息(锁/信号量)、资源使用信息、CPU 使用时间等。 线程也有生命周期:由创建而产生(初始化线程负责创建新线程,创建函数传入口指针、堆栈大小、优先级,返回线程标识符)、由调度而执行、由终止而消亡。**线程被终止后并不直接释放资源**——等其他线程执行了分离函数后,被终止线程才与资源分离;未释放资源的终止线程甚至可以被重新恢复运行。 ### 用户级线程 vs 内核级线程 **用户级线程(ULT)**:线程管理(创建、撤销、切换)全部在**用户空间**由线程库完成,**内核意识不到线程的存在**,调度仍以**进程**为单位。 - 优点:切换不需要进核心态,**效率高**;各应用可定制自己的调度算法;跨平台(不依赖内核实现) - 缺点:**一个线程阻塞系统调用 → 整个进程全阻塞**(内核只看得到进程);**多处理机利用不足**(内核认为进程是单线程的,同一时刻整个进程只能在一个 CPU 上跑) > 💡 **答疑:不是说多线程能把多个线程分到不同 CPU 提高效率吗?** 那说的是**内核级线程**。ULT 的场景里 CPU 只看得到进程,多线程"多此一举";KLT 的场景里内核认识每个线程,才能真正发挥多核优势。这也是为什么要把两者分开设计——各有各的适用场景。 > > **算法竞赛能不能用多线程提效?** 大多数竞赛(ACM、LeetCode 等)在单线程环境下评测,多线程要么跑不了要么不符合评分逻辑;竞赛考察的是算法本身的复杂度优化。除非题目明确鼓励并行,否则专注单线程算法效率更实际。 **内核级线程(KLT)**:线程管理全部由**内核**完成,每个线程有 TCB,内核认识并独立调度每个线程。 - 优点:能利用多核真正并行;一个线程阻塞,内核可调度同进程其他线程继续跑;线程管理高效 - 缺点:线程切换要在用户态/核心态之间转换,**系统开销大**;同步管理更复杂 > ⚠️ 即使是同一进程内的两个内核级线程切换,也需要**从用户态转到核心态**进行——系统开销较大。 **组合方式**:内核支持 KLT 的建立调度,用户程序又能建 ULT,若干 ULT 通过时分复用映射到 KLT 上——结合两者优点:既能并行,阻塞又不会连坐整个进程。 ![[image-f2ae1028.png]] ### 多线程模型 按 ULT 和 KLT 的映射关系分三种: | 模型 | 映射 | 优点 | 缺点 | | --- | --- | --- | --- | | **多对一** | n 个 ULT → 1 个 KLT | 线程管理在用户空间,效率高 | 一个线程访问内核时阻塞 → 整个进程阻塞;不能并行到多处理机 | | **一对一** | 每个 ULT → 1 个 KLT | 一个线程阻塞可调度别的,**并发能力强** | 每建一个用户线程就建一个内核线程,**开销大** | | **多对多** | n 个 ULT → m 个 KLT(n ≥ m) | 集两家之长:并发性不差,开销可控 | 实现最复杂 | ### 线程库 线程库是给程序员提供创建和管理线程的 API。两种实现:用户空间库(调用库函数只是本地函数调用,如 POSIX Pthreads 在纯用户态实现时)和内核级库(调用 API 触发系统调用,如 Windows 线程)。常见:Pthreads(Unix-like 通用,`pthread_create`/`pthread_join`)、Windows(`CreateThread`)、Java(语言级内置,`Thread`/`Runnable`)、C#(`System.Threading`)、Python(`threading`,但受 **GIL** 限制——多核上也无法真正并行)。 ## 八、盲点总复习 1. **线程包含 CPU 现场,可以独立执行程序**——对,线程是处理机调度的基本单位 2. "单处理机系统中任何时刻只有一个进程处于运行态"——**错**。死锁时可能全部进程阻塞,CPU 空转,一个运行的都没有(**最多一个,可以零个**) 3. 单处理机 10 个进程:就绪态最多 **9** 个(至少留一个运行态),阻塞态最多 **10** 个(可以全部阻塞) 4. **并发进程失去封闭性**:封闭性 = 结果只取决于进程本身、不受外界影响、与速度无关;并发后速度不同结果可能不同 5. 进程实体各部分对应 C 程序(形成闭环):二进制代码和常量、全局赋值变量 → **共享正文段**;malloc 动态分配 → **数据堆段**;未赋值局部变量、函数实参 → **数据栈段**;进程优先级 → **PCB** 6. 同一程序多次创建运行在不同数据集上 → **不同的进程**;系统动态 DLL 的系统线程被不同进程调用 → **相同的线程** 7. 进程是暂时的、动态的,程序是永久的、静态的;进程至少由代码、数据、PCB 组成,程序只需代码和数据 8. 多道系统中就绪队列不空时,就绪进程越多,处理器效率**不变**——只要队列不空 CPU 就总能调度进程保持繁忙,与就绪数目无关(除非队列空了 CPU 等待才降效率) 9. **两个合作进程无法利用全局变量交换数据**——不同进程地址空间独立,全局变量只在进程内有意义。交换数据得靠:文件系统(管道也算一种文件)、共享内存、消息传递 10. 内核级线程的切换(哪怕同进程内)也要从用户态转核心态,开销较大 11. **父子进程共享一部分资源,但不能共享虚拟地址空间**——创建子进程时会为它分配自己的虚拟地址空间 --- 进程会"抢"CPU,那 CPU 到底该给谁?下一章讲调度 → [[03-CPU调度]] ⬅️ 🏠 [[00-操作系统总览]] ➡️ [[03-CPU调度]]