02 进程 — 第一个抽象,与一对古怪的创建接口¶
1. 这一章讲什么¶
程序是死的:躺在磁盘上的一堆字节。把它跑起来,操作系统要给它配上内存、
寄存器、打开的文件,这一整套运行中的家当才叫进程。本章前半讲这个抽象的
构成和三态生命周期;后半讲 UNIX 创建进程的接口——fork() 和 exec()——
它故意设计得古怪,而这古怪正是 shell 全部威力的来源。
在全书链条里的位置:这是「虚拟化 CPU」的第一块基石。进程定义了 「被虚拟的那个东西」是什么;下一章讲怎么在它之间切换。
2. 顶层全景¶
磁盘上的程序 ──加载──▶ 进程(内存里的代码+数据 + 栈 + 堆 + 寄存器 + 打开的文件)
│
▼
三态:Running(在跑) ⇄ Ready(排队等跑)
│▲
▼│ I/O 发起 / I/O 完成
Blocked(等事,不让跑)
创建新进程:fork() ──复制自己──▶ 父子两个几乎一样的进程
│
└─▶ exec() ──换成另一个程序(成功就不返回)
图说:上图左半是进程的「生命状态机」,右半是 UNIX 的「复制再替换」式创建法。
3. 核心原理¶
3.1 主线例子:一次 fork(),两个进程,一个 PID 之差¶
书里用了一个十行的小程序 p1.c 做主线例子,我们照原样接住1:
int main(int argc, char *argv[]) {
printf("hello (pid:%d)\n", (int) getpid());
int rc = fork();
if (rc < 0) { // fork 失败
fprintf(stderr, "fork failed\n");
exit(1);
} else if (rc == 0) { // 子进程走这里
printf("child (pid:%d)\n", (int) getpid());
} else { // 父进程走这里
printf("parent of %d (pid:%d)\n", rc, (int) getpid());
}
return 0;
}
运行它,书里记录的真实输出(注意:两次运行的次序可能不同,这是本章的题眼):
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146) ← 父进程打印,rc 是 29147
child (pid:29147) ← 子进程打印
跟着这个输出走一遍 fork() 这件怪事:
- fork() 复制整个进程。子进程不是从
main()重新起跑—— 「hello」只打了一次,说明子进程是从fork()返回处直接开始活的, 「仿佛它自己刚刚调用了 fork() 一样」2。此时父与子几乎一模一样: 各自的私有内存、各自的寄存器、各自的程序计数器。 - 父子靠返回值区分。这是复制留下的唯一记号:父进程收到的是 新子进程的 PID(29147),子进程收到的是 03。就靠这一个数的差别, 同一份代码走出两条分支。
- 次序不保证。两个进程谁先跑,由调度器决定——书里明说输出 「不是确定性的」4;上面的输出里父先子后,换个调度决定就变成 子先父后。记住这个「不确定」,它是全书并发部分的总引线。
wait()把次序钉死。父进程调wait()后,即使它先抢到 CPU, 也会一直等到子进程退出才返回5。所以加了 wait 的版本, 子一定先打印——确定性是被「等」回来的。
3.2 exec():不换进程,换程序¶
fork() 只能复制自己;要运行一个别的程序,得用 exec()。
它的行为配得上「古怪」二字:给它一个可执行文件名和参数,
它用新程序的代码覆盖当前进程的代码段,堆、栈重新初始化,
然后从这个新程序的起点开始跑——进程还是那个进程(PID 没变),
里面的程序已经换人了6。成功时它从不返回;
exec() 后面那行打印「this shouldn't print out」的代码,
就是永远执行不到的死代码7。
主线例子的完整版(p3.c):子进程先打印自己的 pid,然后
execvp("wc", ["wc", "p3.c"])——瞬间,这个子进程不再是 p3,
变成了字数统计程序 wc,输出 29 107 1030 p3.c;
父进程 wait() 到 wc 结束,才打印最后一句8。
3.3 为什么非要拆成两个调用?¶
这是最要紧的一问。直接把「创建并运行新程序」做成一个调用不行吗?
书里给出的答案是:拆开来,才能在中间动手脚。shell 就是个
活生生的例子——它是个普通用户程序,你敲的一行命令,
它做的事是:fork() 出子进程 → 在子进程里、exec() 之前、
做点小动作 → exec() 运行你的命令 → 自己 wait() 等命令结束9。
「小动作」成就了 UNIX 的两大招牌:
- 输出重定向(
wc p3.c > newfile.txt):shell 在子进程里 先关掉标准输出,再打开newfile.txt——UNIX 从 0 开始找空闲的 文件描述符,新打开的文件正好占了标准输出那个号,于是 wc 的 输出悄悄进了文件。exec()会保留已打开的文件描述符,魔术成立10。 - 管道(
grep foo file | wc -l):一个进程的输出接进内核里的 一条队列(pipe),另一个进程的输入接同一条队列, 两个普通程序就此首尾相接11。
如果创建新程序是一整个原子调用,shell 就没地方塞这两段代码了。
书里把这归于 Lampson 定律——「先把事情做对」,并称
fork()+exec() 的组合是 UNIX 设计师「真的做对了」的一处12。
3.4 进程的三态与状态转移¶
一个进程任一时刻处于三种状态之一(多出的都是细节)13:
| 状态 | 含义 | 什么时候 |
|---|---|---|
| Running | 正在处理器上执行指令 | 被调度 |
| Ready | 能跑,但操作系统选了别人 | 被抢下(desceduled) |
| Blocked | 在等某件事(I/O),不让跑 | 发起 I/O;I/O 完成后回 Ready |
图说:I/O 是状态机的枢纽——进程一读磁盘就 Blocked, CPU 立刻转给别的进程,这正是「一个 CPU 假装很多个」的第一个具体机制。 书里用两个进程、九到十步的时序表走了一遍「纯 CPU 型」和「带 I/O 型」 两种交错轨迹,并点明:每一步「换不换人」都是调度器的决策—— 那是下一章之后几章(本库 04、05)的主角14。
3.5 进程的构成与操作系统手里的账本¶
书里给「进程是什么」开了一张清单,叫机器状态(machine state): 程序运行时能读能改的一切15——
- 内存(它的地址空间:代码、静态数据、堆、栈都在里面);
- 寄存器,特别是三个特殊的:程序计数器(PC,下一条要执行的指令)、 栈指针和基址指针(管函数调用);
- 打开的文件列表。
操作系统自己也是个程序,它的账本(以 xv6 内核为例)是每个进程一个
proc 结构:保存哪些寄存器、当前状态、PID、父进程指针等16。
上下文切换就是把这份账本换上另一份——怎么换、换得多快、
换的时候怎么防程序捣乱,是下一章「受限直接执行」的全部内容。
4. 作者的判断与证据¶
书里给了证据的判断:
- 三态模型来自真实历史系统([DV66, V+65]),不是教学简化—— 早期多道程序系统就是这么记账的。
- fork/exec 的设计评价引 Lampson「Hints for Computer Systems Design」,
并给出具体机制证据:文件描述符跨
exec()保留、从 0 开始分配, 重定向和管道由此成立10。
作者的判断(推测/口味):
- 「fork() 是你见过的最古怪的例程」1——带玩笑,但点出一个真问题: 复制整个地址空间再丢弃,是现代系统上昂贵的做法(书里没展开, 我们把它放进 §5 边界)。
- wait 让输出确定性的论证里,作者自己插话「 beware of any absolute and unqualified statements this book makes」17——这种对绝对化 表述的警惕是全书的一贯风格。
5. 边界与局限¶
- fork 的「整份复制」在今天是惰性实现的(写时才真复制,本库第 07 章讲),
书中按语义模型讲,没讲实现开销——fork 在现代大内存进程上的代价
是真实存在的,这是「复制再替换」设计被后人批评最多的一点
(补充,不在书里:POSIX 的
posix_spawn、Linux 的clone都是回应)。 - 本章的进程模型是单线程的;线程来了之后,进程内会有多个执行点, 状态机不变但「机器状态」的归属复杂了(本库 11 章)。
- 书里对调度决策(例子 4.3/4.4 中「该不该切回 P0」)故意留为开放问题, 说「不清楚这是不是个好决定」——这诚实,但也意味着此处没有定论可背。
6. 可带走的¶
- 进程 = 运行中的程序 = 内存 + 寄存器(尤其 PC)+ 打开的文件。 判断「什么东西属于进程」就看断电重启后还在不在——在的是磁盘,不在的是进程。
fork()一次调用、两次返回:父得子 PID,子得 0;子进程从 fork 返回处起跑,不重新进 main。exec()不换进程换程序,成功不返回;PID 不变、内容全换。- fork 与 exec 分开,是为了让 shell 在中间塞代码——重定向、管道由此而来。 接口的古怪常常是为了给调用者留缝。
- 三态(Running/Ready/Blocked)是进程的全部生命;I/O 是从 Running 掉进 Blocked 的门票。
- 无 wait 时父子执行次序不确定——「不确定」三个字是全书并发部分的总开关。
7. 原文地图¶
| 主题 | 原书章 | 原文位置 |
|---|---|---|
| 进程的定义:运行中的程序 | 4. The Abstraction: The Process | text/06-ch04-4-the-abstraction-the-process.txt:4(搜「a running program」) |
| 机器状态:内存、寄存器、PC | 4. The Abstraction: The Process | text/06-ch04-4-the-abstraction-the-process.txt:62(搜「machine state」)与:73(搜「program counter」) |
| 加载:从程序到进程 | 4. The Abstraction: The Process | text/06-ch04-4-the-abstraction-the-process.txt:192(搜「jumping to the main」) |
| 三态模型 | 4. The Abstraction: The Process | text/06-ch04-4-the-abstraction-the-process.txt:201(搜「three states」) |
| 带 I/O 的状态转移轨迹 | 4. The Abstraction: The Process | text/06-ch04-4-the-abstraction-the-process.txt:256(搜「Process0 initiates」) |
| xv6 的进程结构 | 4. The Abstraction: The Process | text/06-ch04-4-the-abstraction-the-process.txt:321(搜「struct proc」) |
| fork 复制调用进程 | 5. Interlude: Process API | text/07-ch05-5-interlude-process-api.txt:69(搜「exact copy」) |
| 子进程从 fork 返回处开始 | 5. Interlude: Process API | text/07-ch05-5-interlude-process-api.txt:75(搜「as if it had called fork」) |
| 父子凭返回值区分 | 5. Interlude: Process API | text/07-ch05-5-interlude-process-api.txt:105(搜「return code」) |
| 输出不确定 | 5. Interlude: Process API | text/07-ch05-5-interlude-process-api.txt:108(搜「not deterministic」) |
| wait 钉死次序 | 5. Interlude: Process API | text/07-ch05-5-interlude-process-api.txt:141(搜「until the child」) |
| exec 覆盖代码段、成功不返回 | 5. Interlude: Process API | text/07-ch05-5-interlude-process-api.txt:235(搜「overwrites its current code」)与:242(搜「never returns」) |
| fork/exec 分离成就 shell | 5. Interlude: Process API | text/07-ch05-5-interlude-process-api.txt:247(搜「separation of fork」) |
| Lampson 定律 | 5. Interlude: Process API | text/07-ch05-5-interlude-process-api.txt:225(搜「GETTING IT RIGHT」) |
-
出处:「5. Interlude: Process API」第 24 段(
text/07-ch05-5-interlude-process-api.txt:24,搜「strangest routine」)。原书称 fork() 是「你调用过的最古怪的例程」;程序与输出见同章图 5.1 及第 53–59 段。 ↩↩ -
出处:「5. Interlude: Process API」第 75 段(
text/07-ch05-5-interlude-process-api.txt:75,搜「as if it had called fork」)。 ↩ -
出处:「5. Interlude: Process API」第 105 段(
text/07-ch05-5-interlude-process-api.txt:105,搜「return code」)。 ↩ -
出处:「5. Interlude: Process API」第 108 段(
text/07-ch05-5-interlude-process-api.txt:108,搜「not deterministic」)。 ↩ -
出处:「5. Interlude: Process API」第 141 段(
text/07-ch05-5-interlude-process-api.txt:141,搜「until the child」)。原文:wait() 会延迟父进程执行,直到子进程完成。 ↩ -
出处:「5. Interlude: Process API」第 235–240 段(
text/07-ch05-5-interlude-process-api.txt:235,搜「overwrites its current code」)。 ↩ -
出处:「5. Interlude: Process API」第 242 段(
text/07-ch05-5-interlude-process-api.txt:242,搜「never returns」)。 ↩ -
出处:「5. Interlude: Process API」第 211–216 段(
text/07-ch05-5-interlude-process-api.txt:214,搜「29 107」)。 ↩ -
出处:「5. Interlude: Process API」第 252–260 段(
text/07-ch05-5-interlude-process-api.txt:252,搜「just a user program」)。 ↩ -
出处:「5. Interlude: Process API」第 272–281 段(
text/07-ch05-5-interlude-process-api.txt:274,搜「kept open across」)。 ↩↩ -
出处:「5. Interlude: Process API」第 296–302 段(
text/07-ch05-5-interlude-process-api.txt:296,搜「implemented in a similar way」)。 ↩ -
出处:「5. Interlude: Process API」第 225–233 段(
text/07-ch05-5-interlude-process-api.txt:227,搜「Neither abstraction」)。 ↩ -
出处:「4. The Abstraction: The Process」第 201 段(
text/06-ch04-4-the-abstraction-the-process.txt:201,搜「three states」)。 ↩ -
出处:「4. The Abstraction: The Process」第 282 段(
text/06-ch04-4-the-abstraction-the-process.txt:282,搜「OS scheduler」)。 ↩ -
出处:「4. The Abstraction: The Process」第 62 段(
text/06-ch04-4-the-abstraction-the-process.txt:62,搜「machine state」)。 ↩ -
出处:「4. The Abstraction: The Process」第 321–330 段(
text/06-ch04-4-the-abstraction-the-process.txt:321,搜「struct proc」)。 ↩ -
出处:「5. Interlude: Process API」第 166–169 段(
text/07-ch05-5-interlude-process-api.txt:167,搜「beware」)。 ↩