Appearance
进程与线程:PCB、状态转换、上下文切换
概念
进程(process)是程序在一个数据集合上的一次运行过程,是系统进行资源分配的基本单位。
其中 PCB(Process Control Block,进程控制块)是进程存在的唯一标志——PCB 在,进程就在;PCB 被回收,进程就消失。
一句话说清它是什么:程序是菜谱,进程是"按这张菜谱做菜这件事"——同一张菜谱可以同时做很多次(同一个程序可以有多个进程),每次做菜都有自己的一份原料、自己的进度、自己的记录卡(PCB)。菜谱是静态的,做菜是动态的。
线程(thread)则是进程内部的一条执行流,是处理机调度(分派)的基本单位:一个进程可以有多条线程,它们共享进程的资源,但各自独立地被调度。
三个基本单位的口径必须分清(408 年年考):进程是资源分配的基本单位,线程是调度/分派的基本单位,程序是静态的(不是执行实体)。
原理
一、进程的五个特征
| 特征 | 含义 |
|---|---|
| 动态性 | 进程是程序的一次执行,有"产生 → 运行 → 消亡"的生命周期。这是进程最基本的特征 |
| 并发性 | 多个进程可以同时存在于内存中、交替使用 CPU |
| 独立性 | 进程是独立运行、独立获得资源、独立接受调度的基本单位 |
| 异步性 | 进程按各自独立的、不可预知的速度向前推进 |
| 结构性 | 每个进程都有 PCB、程序段、数据段,结构固定 |
⚠️ 易错点:动态性是进程最基本的特征,不是"并发性"。程序没有动态性(它是静态的,可以长期存在),所以**"程序"不是"进程"**。
二、PCB 里到底存了什么
PCB 是操作系统感知进程的唯一凭据——它在内存里,操作系统才能"看见"这个进程。它分四类信息:
| 类别 | 内容 | 为什么必须有 |
|---|---|---|
| 进程描述信息 | 进程标识符 PID、用户标识符 UID、父进程号 | 进程之间要能互相识别(kill、wait 都靠 PID) |
| 进程控制和管理信息 | 进程当前状态、优先级、程序入口地址、已用 CPU 时间、时间片剩余 | 调度器要看的全部内容都在这里 |
| 资源分配清单 | 已打开的文件、已占用的 I/O 设备、内存地址空间信息(页表基址) | 进程被换出/换回时要能恢复资源视图 |
| 处理机相关信息 | 通用寄存器、PC、PSW、栈指针(即"现场信息") | 上下文切换时要保存/恢复的就是这一块 |
记住一句话:PCB 是"进程的身份证 + 病历本 + 存折"——身份(PID)、状态(状态字)、财产(资源清单)、现场(寄存器组)。
三、五态模型与状态转换(本章核心)
创建 调度 时间片到 / 被抢占
┌────────┐ ┌───────────┐ ┌─────────────┐
│ 新建 │─────►│ 就绪 │───────►│ 运行 │─────► 终止
└────────┘ └───────────┘◄───────└─────────────┘
▲ 时间片到/被抢占 │
│ │ 阻塞事件
│ I/O 完成 / 被唤醒 │ (等 I/O、等信号量、sleep)
┌───────────┐ │
│ 阻塞 │◄───────────────┘
└───────────┘三种基本状态:就绪(万事俱备,只缺 CPU)、运行(正占有 CPU)、阻塞(缺资源,有了也不能跑)。
状态转换表(备考要点:只有这 6 条合法箭头):
| 转换 | 触发事件 | 方向 |
|---|---|---|
| 就绪 → 运行 | 调度程序选中(分配 CPU) | 只此一条入口 |
| 运行 → 就绪 | 时间片用完 / 被更高优先级抢占 | 自愿或被迫让出 CPU |
| 运行 → 阻塞 | 主动请求资源失败(等 I/O、等信号量、sleep) | 进程自己调用阻塞原语 |
| 阻塞 → 就绪 | 所等事件完成(I/O 结束、信号量被 V、被唤醒) | 别的进程/中断唤醒它 |
| 创建 → 就绪 | 创建原语完成 | — |
| 运行 → 终止 | 正常结束 / 出错 / 被 kill | — |
⚠️ 三条不可能发生的转换(最高频选择题):
- 阻塞 → 运行:不可能。阻塞被唤醒后只能先进就绪队列,还得等调度——否则"调度"就成了摆设。
- 就绪 → 阻塞:不可能。就绪进程没有 CPU,无法"执行到某个请求资源的指令",也就无法主动阻塞。
- 创建 → 运行:不可能。新建进程必须先入就绪队列(除非题目明说"抢占式调度器立刻选中它",那也属于"先就绪再被选中",中间不能跳过就绪状态)。
一句话记住:运行态是唯一的"三岔口"(可以去就绪、去阻塞、去终止),阻塞态只有一个出口(回就绪),就绪态只有一个入口(从运行来,或从阻塞来)。
四、进程控制:原语
进程控制用原语(primitive)实现。原语是不可被中断的——靠关中断保护。
| 原语 | 干什么 | 关键动作 |
|---|---|---|
| 创建 | 分配 PID、申请 PCB、分配资源、入就绪队列 | 走完就是就绪态(不是运行态) |
| 终止 | 回收资源、撤销 PCB | PCB 一撤销,进程即消亡 |
| 阻塞 | 运行 → 阻塞:保存现场、入阻塞队列、转调度 | 必须"保存现场 + 重新调度" |
| 唤醒 | 阻塞 → 就绪:入就绪队列、等待调度 | 不立刻运行,只入就绪队列 |
| 切换 | 保存旧进程现场、恢复新进程现场 | 见下文第七节 |
⚠️ 为什么原语必须不可中断:原语常常在修改关键数据结构(就绪队列、阻塞队列、PCB 链表)。若执行到一半被打断,队列会出现"进程既在就绪队列又在阻塞队列"这类不一致状态——这就是**"关中断"是特权指令**的直接原因(回顾 os/01-overview.md)。
五、进程的组织方式
| 方式 | 做法 | 特点 |
|---|---|---|
| 链接方式 | 同状态的 PCB 用指针串成队列 | 插入/删除快,查找要遍历 |
| 索引方式 | 按状态建索引表,表项指向该状态的 PCB | 查找快(直接索引到状态) |
系统里通常有四类队列:就绪队列(1 个)、阻塞队列(多个,按等待事件分)、运行队列(单核只有 1 个"当前进程")、空闲 PCB 队列。调度只从就绪队列里挑人。
六、线程:为什么要它
引入线程的动机有两个,必须都能说清:
| 动机 | 说明 |
|---|---|
| 降低并发开销 | 进程创建/撤销/切换都要"端着一整套资源搬家"(页表、地址空间、文件表);线程只换栈和寄存器,切换便宜得多 |
| 便于通信与共享 | 同一进程内的线程共享全部资源,通信不需要内核介入(读写同一块内存即可) |
进程与线程的对照(必背表):
| 对比项 | 进程 | 线程 |
|---|---|---|
| 基本单位 | 资源分配的基本单位 | **处理机调度(分派)**的基本单位 |
| 地址空间 | 各自独立(切换要换页表 → 要刷新 TLB) | 同进程内共享地址空间(切换不换页表) |
| 拥有资源 | 拥有 | 基本不拥有(除栈、寄存器、TCB) |
| 私有内容 | — | 栈、寄存器现场、TCB(线程自己的) |
| 共享内容 | — | 同进程的代码段、数据段、堆、打开的文件、信号处理方式 |
| 切换开销 | 大(涉及地址空间) | 小 |
| 一个出错 | 通常只影响自己 | 可能拖垮整个进程(共享地址空间) |
⚠️ 线程私有的三样东西(必考):栈、寄存器(PC、PSW 等现场)、TCB。除了这三样,其余全是共享的。
线程的实现(三个层次):
| 类型 | 谁在管 | 优点 | 缺点 |
|---|---|---|---|
| 用户级线程(ULT) | 用户库(如早期 green thread) | 切换不用进内核,极快;可在不支持线程的 OS 上跑 | 一个线程阻塞 → 整个进程阻塞;内核只看得见"一个进程",多核分不到 |
| 内核级线程(KLT) | 操作系统内核 | 一个阻塞不影响别人;多核可真正并行 | 切换要进内核(开销大) |
| 组合 | 两者结合 | 兼顾 | 实现复杂 |
三种多线程模型:多对一(多个用户线程映射到 1 个内核线程,如 ULT)、一对一(如 Linux 的 pthread 默认)、多对多(m 个用户线程映射到 n 个内核线程,n ≤ m)。
⚠️ "并行"只有在"内核级线程 + 多核"时才可能:纯用户级线程的多线程,在同一时刻只有一条线程在 CPU 上跑(内核根本不知道有几条线程)。这一点与"并发 ≠ 并行"是同一件事(回顾
os/01-overview.md)。
七、上下文切换:到底切了什么
上下文(context)= 保证进程能"暂停后原地继续"所需的全部状态。
| 切的内容 | 在哪 | 说明 |
|---|---|---|
| PC(程序计数器) | PCB | 下一条要执行的指令地址 |
| PSW(程序状态字) | PCB | 含模式位(用户态/内核态)、标志位 |
| 通用寄存器组 | PCB | 运算现场 |
| 栈指针 $sp | PCB | 现场恢复靠它找栈 |
| 页表基址寄存器 / 段表基址 | PCB(进程切换时换) | 线程切换不换——这就是线程切换便宜的根本原因 |
完整过程(四步):
① 旧进程运行中 → 触发切换(时间片到 / 被抢占 / 主动阻塞 / 系统调用返回时发现需调度)
② 保存旧进程现场 → 把 PC、PSW、通用寄存器、$sp 写回它的 PCB
③ 从就绪队列按调度算法选出新进程
④ 恢复新进程现场 → 从 PCB 里读回寄存器;若是"换进程",还要换页表基址 + 刷新 TLB
⑤ 新进程从它上次被切走的下一条指令继续执行(线程切换则回到线程自己的栈)⚠️ 三个必须说清楚的点:
- 上下文切换本身不产生任何用户价值——它是纯粹的开销。所以"时间片不能太小"(切换太频繁,CPU 都在搬家)也"不能太大"(响应变慢)。
- "进程切换" ⊋ "上下文切换":进程切换必然包含上下文切换,但上下文切换未必是进程切换(线程切换也是上下文切换,且不需要换页表)。
- 切换发生在内核态:保存/恢复现场都是特权操作(要碰 PCB 与 PSW)。用户程序不能自己"把 CPU 让给别人"。
示例
例 1:状态转换判断题
下列事件分别让进程发生什么转换?
(a)时间片用完 (b)等待的磁盘 I/O 完成 (c)执行
read()发现数据未就绪 (d)进程执行exit()(e)更高优先级进程到达(抢占式) (f)进程等待的信号量被别的进程 V 了
逐条作答(对照第三节表):
| 事件 | 转换 | 理由 |
|---|---|---|
| (a)时间片用完 | 运行 → 就绪 | 还能跑,只是被"叫停",回就绪队列排队 |
| (b)磁盘 I/O 完成 | 阻塞 → 就绪 | 被中断唤醒,但不能直接运行,要排队等调度 |
(c)read() 数据未就绪 | 运行 → 阻塞 | 进程主动阻塞(自己执行了阻塞原语) |
(d)exit() | 运行 → 终止 | 撤销 PCB |
| (e)更高优先级到达(抢占) | 运行 → 就绪 | 被抢占,回到就绪队列(不是阻塞!) |
| (f)信号量被 V | 阻塞 → 就绪 | 与(b)同理 |
⚠️ 两个陷阱:
- (b)(f)之后进程"并没有运行"——很多同学答"阻塞 → 运行",这是典型错误。
- (e)之后是"就绪"不是"阻塞":被抢占 ≠ 缺资源,它只是被从 CPU 上赶下来了。
例 2:时间片多大才合适——切换开销的完整计算
设一次上下文切换(保存 + 恢复 + 选择 + 换页表)耗时
。求时间片 分别为 1 ms / 5 ms / 10 ms / 100 ms 时,切换开销占 CPU 总时间的比例与有效利用率。
完整计算过程:
第一步,写清"一个时间片周期"包含什么:
第二步,开销占比与有效利用率:
第三步,
第四步,逐项计算(单位统一为 μs):
| 开销占比 | 有效利用率 | ||
|---|---|---|---|
| 1 ms | 1010 | 0.9901% | 99.0099% |
| 5 ms | 5010 | 0.1996% | 99.8004% |
| 10 ms | 10010 | 0.0999% | 99.9001% |
| 100 ms | 100100 | 0.0100% | 99.9900% |
结论:
- 切换开销与时间片成反比:
从 1 ms 涨到 100 ms,开销占比从 0.9901% 掉到 0.0100%(降了 99 倍)。 - 但时间片不能一味加大——这是"响应时间"与"开销"的权衡:
意味着一个交互式作业最多等 100 ms 才被轮到。 - 量级提醒:本机口径下
(进程切换);线程切换只要 2 μs(不用换页表、不刷 TLB),便宜 5 倍。
这就是"线程比进程轻"的定量解释:同一份"保存/恢复寄存器"的代码,进程版还要额外付"换地址空间 + 刷 TLB"的钱。
例 3:C 实现——切换开销与创建开销的量化
#include <stdio.h>
/* 口径: 一次进程上下文切换 10 us; 线程切换 2 us
* 创建一个进程 500 us; 创建一个线程 20 us */
#define PROC_SW_US 10.0
#define THR_SW_US 2.0
#define PROC_CREATE_US 500
#define THR_CREATE_US 20
#define N_TASK 1000
int main(void) {
double q_us;
printf("时间片 q 切换开销占比 有效利用率\n");
q_us = 1000.0; printf("%6.0f ms %12.4f%% %14.4f%%\n", q_us / 1000, PROC_SW_US / (q_us + PROC_SW_US) * 100, q_us / (q_us + PROC_SW_US) * 100);
q_us = 5000.0; printf("%6.0f ms %12.4f%% %14.4f%%\n", q_us / 1000, PROC_SW_US / (q_us + PROC_SW_US) * 100, q_us / (q_us + PROC_SW_US) * 100);
q_us = 10000.0; printf("%6.0f ms %12.4f%% %14.4f%%\n", q_us / 1000, PROC_SW_US / (q_us + PROC_SW_US) * 100, q_us / (q_us + PROC_SW_US) * 100);
q_us = 100000.0; printf("%6.0f ms %12.4f%% %14.4f%%\n", q_us / 1000, PROC_SW_US / (q_us + PROC_SW_US) * 100, q_us / (q_us + PROC_SW_US) * 100);
/* 创建 1000 个任务: 进程 vs 线程 */
printf("创建 %d 个任务: 进程 %d us -> %.1f ms\n", N_TASK, PROC_CREATE_US * N_TASK, PROC_CREATE_US * (double)N_TASK / 1000);
printf(" 线程 %d us -> %.1f ms\n", THR_CREATE_US * N_TASK, THR_CREATE_US * (double)N_TASK / 1000);
printf("创建开销差 %.0f 倍, 省 %.1f ms\n", PROC_CREATE_US / (double)THR_CREATE_US,
(PROC_CREATE_US - THR_CREATE_US) * (double)N_TASK / 1000);
printf("切换开销: 进程 %.0f us vs 线程 %.0f us = %.0f 倍\n", PROC_SW_US, THR_SW_US, PROC_SW_US / THR_SW_US);
return 0;
}
c 本站为静态站,不提供在线运行;可复制到本地用 gcc / python 执行
预期输出:
时间片 q 切换开销占比 有效利用率
1 ms 0.9901% 99.0099%
5 ms 0.1996% 99.8004%
10 ms 0.0999% 99.9001%
100 ms 0.0100% 99.9900%
创建 1000 个任务: 进程 500000 us -> 500.0 ms
线程 20000 us -> 20.0 ms
创建开销差 25 倍, 省 480.0 ms
切换开销: 进程 10 us vs 线程 2 us = 5 倍三个要点:
- "省 480.0 ms" = 500 ms − 20 ms,纯粹是"创建 1000 个任务的固定成本差";任务本身的工作量一文没省。
- 切换开销的 5 倍差来自"要不要换地址空间",不是"要不要保存寄存器"。
- ⚠️ 这段 C 只做了"乘法与除法",没有任何并发——它算的是开销模型,不是在真的创建进程。
例 4:fork 之后为什么会打印两次
MIPS/x86 上执行下面两段代码,输出分别是什么?(假设
stdout连到行缓冲的终端)甲:
printf("hello\n"); fork();乙:printf("hello"); fork();
逐步分析(关键在"缓冲区在哪、有几个"):
| 步骤 | 甲(有 \n) | 乙(无 \n) |
|---|---|---|
1. printf 时 | 数据进 stdio 缓冲区,遇 \n 立即冲刷 | 数据留在缓冲区,不冲刷 |
2. fork() 时 | 缓冲区为空,子进程复制到的也是空 | 缓冲区里有 "hello",被整体复制给子进程 |
| 3. 两个进程各自退出时 | 各冲刷一次 空缓冲 → 都无输出 | 各冲刷一次含 "hello" 的缓冲 |
| 总输出次数 | 1 次 | 2 次 |
用等价的"缓冲区模型"验算(Python):
python
# 模型: 缓冲区就是一个列表, fork = 复制一份, 进程退出 = 各自 flush
for name, flushed in [('printf("hello\\n") 遇换行立即冲刷', True),
('printf("hello") 无换行不冲刷', False)]:
buf = [] if flushed else ['hello'] # fork 那一刻缓冲区里的东西
outs = []
if flushed:
outs.append('hello') # 父进程在 printf 时就已输出
outs += list(buf) # 父进程退出时 flush
outs += list(buf) # 子进程复制了同一份缓冲, 退出也 flush
print('%-34s -> 输出次数 %d' % (name, len(outs)))输出对照:有 \n → 输出 1 次;无 \n → 输出 2 次。与上面的逐步分析一致。
⚠️ 三个必须记住的点:
fork()返回两次:父进程得到子进程 PID(> 0),子进程得到 0,失败得到 −1(此时不产生子进程)。fork()之后,两个进程"各自从 fork 的下一条指令继续跑",代码相同、数据是副本(靠写时复制 COW 省内存,见lang/24-image.md)。- "打印两次"的根因是"缓冲区被复制",不是"
printf执行了两次"。若改成"无缓冲"(fflush过 /setbuf(stdout, NULL)),两种写法都只输出 1 次。
例 5:Python——状态转换合法性与开销全表验算
# ===== 1. 五态转换的合法性检查 =====
legal = {('新建', '就绪'), ('就绪', '运行'), ('运行', '就绪'),
('运行', '阻塞'), ('运行', '终止'), ('阻塞', '就绪')}
for a, b in [('阻塞', '运行'), ('就绪', '阻塞'), ('新建', '运行'), ('就绪', '就绪')]:
print(' %s -> %s : %s' % (a, b, '合法' if (a, b) in legal else '不合法'))
# ===== 2. 切换开销与时间片 =====
print('')
print('时间片 ms 开销占比 % 有效利用率 %')
for q_ms in [1, 5, 10, 100]:
q = q_ms * 1000.0
print(' %6.0f %10.4f %12.4f' % (q_ms, 10 / (q + 10) * 100, q / (q + 10) * 100))
# ===== 3. 进程/线程的创建与切换开销 =====
print('')
for tag, create, sw in [('进程', 500, 10.0), ('线程', 20, 2.0)]:
print(' %s: 创建 %3d us/个, 切换 %4.1f us; 创建 1000 个要 %.1f ms'
% (tag, create, sw, create * 1000 / 1000))
# ===== 4. 进程与线程"共享什么、私有什么" =====
print('')
private = ['栈', '寄存器现场(PC/PSW/sp)', 'TCB']
shared = ['代码段', '数据段', '堆', '打开的文件表', '信号处理方式']
print(' 线程私有: %s' % ' / '.join(private))
print(' 线程共享: %s' % ' / '.join(shared))
print(' 进程私有: 以上全部(同进程的线程共享, 跨进程不共享)')
# ===== 5. 例 4 的缓冲区模型 =====
print('')
for tag, flushed in [('有换行(立即冲刷)', True), ('无换行(留在缓冲区)', False)]:
buf = [] if flushed else ['hello']
outs = (['hello'] if flushed else []) + list(buf) + list(buf)
print(' %-22s fork 后总输出次数 = %d' % (tag, len(outs)))
python 本站为静态站,不提供在线运行;可复制到本地用 gcc / python 执行
输出对照:五态检查得"阻塞→运行 不合法 / 就绪→阻塞 不合法 / 新建→运行 不合法 / 就绪→就绪 不合法";切换开销表得 0.9901% / 0.1996% / 0.0999% / 0.0100%;创建开销 1000 个进程 500.0 ms vs 线程 20.0 ms;例 4 的缓冲区模型得 1 次 / 2 次。全部与手算一致。
考点
考点
1. 必背结论
- 进程 = PCB + 程序段 + 数据段;PCB 是进程存在的唯一标志;进程是资源分配的基本单位,线程是调度/分派的基本单位。
- 进程五特征:动态(最基本)、并发、独立、异步、结构。
- PCB 四类信息:描述信息、控制和管理信息、资源清单、处理机相关信息(现场)。
- 三态:就绪 / 运行 / 阻塞;只有 6 条合法转换。
- 三条不可能:阻塞 → 运行、就绪 → 阻塞、创建 → 运行。
- 原语不可中断(关中断保护)——因为要维护就绪/阻塞队列的一致性。
- 线程私有:栈、寄存器现场、TCB;其余共享。
- 用户级线程的致命缺点:一条线程阻塞会阻塞整个进程,且多核用不上(内核只看到 1 个进程)。
- 上下文切换 = 保存旧现场 + 选新进程 + 恢复新现场;进程切换要换页表并刷 TLB,线程切换不用(口径:进程切换 10 μs、线程切换 2 μs,差 5 倍)。
- 时间片不能太小(切换开销占比升)、也不能太大(响应变慢);
。 fork()返回两次:父得子 PID(>0)、子得 0、失败得 −1;缓冲区会被复制 → 无\n的printf会输出两次。
2. 高频陷阱
- 说"阻塞被唤醒后直接运行":错。阻塞 → 就绪,还得排队等调度。
- 说"时间片用完进程就阻塞":错。时间片用完是"运行 → 就绪",它不缺任何资源。
- 把"被抢占"说成"阻塞":错。被抢占的进程万事俱备,回就绪队列。
- 说"进程切换就是上下文切换(两者等价)":错。线程切换也是上下文切换;进程切换还额外换地址空间。"进程切换 ⊋ 上下文切换"。
- 说"线程是资源分配的单位":错,方向反了。资源归进程,调度归线程。
- 说"多线程一定能在多核上并行":错。纯用户级线程不行(内核只调度"进程"这一个实体)。只有内核级线程 + 多核才能真并行。
- 说"
fork()后父子进程共享变量":错。地址空间是复制出来的,变量互不影响(fork后写同一变量,父子各写各的)。 - 忘了"
fork失败不产生子进程":返回 −1 时没有新进程,父进程继续执行。 - 把"新建进程"写成"直接进入运行态":错。新进程先进就绪队列。
- 说"阻塞是别人让它阻塞的":错。阻塞是进程自己主动发起(执行到请求资源的指令),唤醒才是别人/中断做的。
3. 解题模板("状态转换题")
① 先问三个问题: 它现在有 CPU 吗? 它缺资源吗? 它是自愿让出吗?
② 有 CPU + 缺资源 -> 运行 -> 阻塞 (主动调用阻塞原语)
③ 有 CPU + 被叫停 -> 运行 -> 就绪 (时间片到 / 被抢占)
④ 没 CPU + 缺资源 -> 阻塞; 事件完成 -> 阻塞 -> 就绪 (不是运行!)
⑤ 没 CPU + 不缺资源 -> 就绪; 被调度 -> 就绪 -> 运行
⑥ 碰到"被唤醒/被抢占"字样, 一律回到"就绪队列"再想下一步4. 与相邻章节的接口
os/01-overview.md(操作系统概述):那篇的**"并发/异步"正是本篇"五态走走停停"的宏观描述;"原语不可中断 → 关中断是特权指令"**也出自那篇第六节。os/02-syscall.md(系统调用):fork/exit/wait就是进程控制类系统调用;"陷入内核才可能切换"——保存现场那一步与系统调用的保存现场是同一套机制。os/11-scheduling.md(处理机调度):"就绪 → 运行"由谁决定? 就是下一篇的调度算法;本篇的"时间片"在那里被反复引用。os/12-sync.md(同步与互斥):"运行 → 阻塞"的最常见原因就是"等信号量"——P操作阻塞,V操作唤醒(阻塞 → 就绪)。arch/13-virtual.md(虚拟存储器):进程切换要换页表基址寄存器,正是那篇讲的内容;**写时复制(COW)**也在那里。lang/24-image.md(程序加载与进程映像):"进程 = 代码段 + 数据段 + 堆 + 栈"的内存布局、fork的 COW 在那篇有完整图景。
小结
- 进程 = PCB + 程序段 + 数据段,PCB 是进程存在的唯一标志;进程管资源,线程管调度。
- 只有 6 条合法状态转换,其中三条不可能:阻塞 → 运行、就绪 → 阻塞、创建 → 运行。运行态是唯一的三岔口。
- 时间是唯一的"切换开关":时间片到 → 回就绪;等资源 → 进阻塞;被唤醒只是"进就绪队列",不是"开始运行"。
- 线程私有的只有三样:栈、寄存器现场、TCB;其余全共享——所以线程通信便宜、出错代价大。
- 上下文切换是纯开销:例 2 里
从 1 ms 到 100 ms,切换开销占比从 0.9901% 降到 0.0100%;进程切换 10 μs vs 线程切换 2 μs(5 倍),差在"要不要换地址空间 + 刷 TLB"。 fork()后缓冲区被复制——没有\n的printf会输出两次(例 4 的模型已验算)。
下一篇:处理机调度:调度算法与评价指标
评论(0)
当前浏览器不允许本地存储,评论无法保存。
还没有评论,来说两句。