进程、线程与任务 — 专题笔记
进程
程序是静态的,进程是程序的一次执行过程,是 OS 进行资源分配的基本单位。
OS 管理进程的核心数据结构是 PCB(Process Control Block,进程控制块)。进程创建时建立 PCB,调度、阻塞、唤醒、切换、回收全部依赖它。
PCB 记录什么
| 类别 | 典型内容 | 作用 |
|---|---|---|
| 进程标识 | PID、父进程 PID、所属用户 | 唯一标识 |
| 进程状态 | 创建/就绪/运行/阻塞/终止 | 处于生命周期的哪一步 |
| 调度信息 | 优先级、时间片、等待原因、队列指针 | 决定何时运行、是否被抢占 |
| 资源与内存 | 打开的文件、I/O 设备、页表/段表、地址空间 | 占用了什么、内存如何组织 |
| 处理机现场 | 通用寄存器、PC、PSW、栈指针 | 切换时保存/恢复 CPU 现场 |
PCB 位于内核空间,不跟用户态的代码段、堆、栈并列。
进程内存映像
| 区域 | 内容 | 说明 |
|---|---|---|
.text | 机器指令 | 只读,多进程可共享同一份 |
.data | 已初始化全局/静态变量 | 进程私有 |
.bss | 未初始化全局/静态变量 | 运行时置 0,进程私有 |
| heap | malloc/new 动态分配 | 生命周期自行控制 |
| stack | 函数调用现场、局部变量、返回地址 | 每个线程有自己的栈 |
| 共享库/映射区 | 动态链接库、mmap 映射 | 只读部分可共享 |
对照 编译与链接导论。
进程状态
创建态 → 就绪态 ⇄ 运行态 → 终止态
↘ 阻塞态 ↗
| 状态 | 含义 |
|---|---|
| 创建态 | 资源和 PCB 正在建立 |
| 就绪态 | 具备运行条件,只差 CPU |
| 运行态 | 正在 CPU 上执行 |
| 阻塞态 | 等待某事件(I/O、数据、资源),不能运行 |
| 终止态 | 运行结束,等待回收 |
进程间通信(IPC)
| 方式 | 机制 | 关键点 |
|---|---|---|
| 共享存储 | 多进程访问同一块内存 | 效率高(免复制);必须配合同步/互斥,否则数据冲突 |
| 消息传递 | 发方→收方传递完整消息 | 直接通信:发方直接指定目标进程;间接通信:通过信箱/消息队列中转 |
| 管道 | 内核提供的固定大小环形缓冲区 | 有序数据流,空则阻塞读、满则阻塞写;需处理互斥、同步、对端状态 |
| 信号 | 轻量异步通知,“某事件发生了” | 内核或进程发出;PCB 中维护 pending 位图 + blocked 位图,信号到达置位,处理完清零,阻塞则暂挂 |
互斥与同步的细节见 同步、互斥与通信。
线程
线程是 CPU 调度的基本单位。进程是资源容器,线程是真正上 CPU 跑的执行流。
线程与进程的资源关系
| 同进程线程共享 | 每个线程独立拥有 |
|---|---|
| 代码段、数据段、堆 | 寄存器现场 |
| 打开的文件/文件描述符 | 程序计数器 PC |
| 大部分系统资源 | 栈 |
| — | 线程局部存储 TLS |
一句话:共享资源,不共享执行现场。
TCB
TCB(Thread Control Block)管线程:
| 类别 | 典型内容 |
|---|---|
| 线程 ID | 唯一标识 |
| 寄存器集合 | 通用寄存器、PC、状态字 |
| 当前状态 | 运行/就绪/阻塞 |
| 调度优先级 | 静态/动态优先级 |
| TLS | 线程私有数据 |
| 堆栈指针 | 指向该线程自己的栈 |
线程开关为何比进程开关轻量 — 空间与时间
| 对比项 | N 个独立进程 | 1 个进程 + N 个线程 |
|---|---|---|
| 控制块 | N 份完整 PCB | N 份精简 TCB(无内存/文件/IO 信息) |
| 切换时做什么 | 换掉整个地址空间(页表、TLB 刷新)+ 保存/恢复全部上下文 | 只切换寄存器、PC、栈指针,不动地址空间 |
| 共享信息的位置 | 各自复制,IPC 搬运 | 堆、数据段、文件直接共享,无需传递 |
直觉:把原来 8 个独立进程当作 1 个进程,内部改成 8 个线程 → 控制块变小、切换不动地址空间 → 时空开销都降下来。
线程状态
核心三态:运行态 ↔ 就绪态 → 阻塞态(与进程状态同理,不再展开)。
线程实现方式
| 类型 | 管理方 | 特点 |
|---|---|---|
| 用户级线程(ULT) | 用户空间线程库 | 切换快(不陷内核);但一线程阻塞→全进程阻塞,无法利用多核 |
| 内核级线程(KLT) | 内核直接管理 | 支持多核并行,一线程阻塞不拖累其他;但切换开销大 |
为什么 KLT 切换比 ULT 慢?
- ULT:切换全在用户态完成,线程库自己保存/恢复寄存器、换栈、跳 PC。OS 根本不知道发生了切换,不存在用户态↔内核态的往返。
- KLT:切换必须走内核。先 陷入指令(trap)从用户态切到内核态,内核调度器选下一个线程,再从内核态切回用户态。多出来的就是这个 用户态 ↔ 内核态的模式切换——它不是线程切换本身的开销,而是”要切线程必须先去找内核”的代价。
映射模型
ULT 是用户态自己搞出来的线程,内核不认识。内核只认 KLT。所以必须有一种映射关系,把 ULT “挂”到 KLT 上,内核才能给这个进程分 CPU。
| 模型 | 映射关系 | 怎么干活 | 死穴 |
|---|---|---|---|
| 多对一 | 多个 ULT → 1 个 KLT | 用户空间线程库在进程内部轮转 ULT,内核完全不知情,它只看到”一个线程” | 一个 ULT 阻塞(调了阻塞系统调用)→ 内核把唯一的 KLT 挂起 → 全部 ULT 都瘫痪;也无法利用多核 |
| 一对一 | 1 个 ULT → 1 个 KLT | 每个 ULT 背后都对应一个内核线程,内核直接调度,能真正并行到多核 | 创建/删除/切换每次都要陷内核(trap),开销上去了 |
| 多对多 | M 个 ULT ↔ N 个 KLT(N ≤ M) | 用户态分配 ULT 到空闲的 KLT 上跑,一个 ULT 阻塞了,同进程其他 ULT 可以换绑到另的 KLT | 两头都要协调,实现复杂 |
直觉:
- 多对一 = 全公司共用一个电话,一个人占线,全公司失联
- 一对一 = 每人一部座机,但打电话必须去前台转接(trap)
- 多对多 = N 部公用电话,谁用谁拿,一个占线换另一部
与 RTOS 任务的对应
在 MCU + RTOS 场景中,Task 更像线程,不是完整进程——多数小型 RTOS 没有进程隔离和独立地址空间。
对照 FreeRTOS 任务模型。