管道、Fork 和僵尸进程
摘要
本文来自哈佛大学的 CS 61 课程,涵盖了 Unix 中的管道、Fork 和僵尸进程概念,解释了在关闭时管道如何自动终止程序,以及如何使用管道实现对子进程的阻塞等待。
暂无内容
查看缓存全文
缓存时间: 2026/05/14 15:24
# CS 61 2017
来源:https://cs61.seas.harvard.edu/wiki/2017/Shell3/
## 管道、分叉与僵尸进程
## 管道历史
### 管道的概念
Doug Mcllroy 在管道实现之前很久就描述了管道的概念。
```
总结——最重要的是什么。
把我最关心的要点浓缩一下:
1. 我们应该有某种方式来耦合程序,就像花园软管一样——当需要以另一种方式处理数据时,可以拧上另一节管子。
这也是一种 IO 的方式。
2. 我们的加载器应该能够执行链接加载和受控建立。
3. 我们的文件归档方案应该允许相当通用的索引、责任、世代、数据路径切换。
4. 应该可以为调试目的获取私有系统组件(所有例程都是系统组件)。
M. D. McIlroy
1964年10月11日
```
### 文学编程
Don Knuth 是提出“计算机科学”这个术语的人。他撰写了《计算机程序设计艺术》,并创造了 `Latex` 语言。Knuth 非常喜欢写作和编程,以至于他发展了“文学编程”。Knuth 创建了一种编程风格,允许你在写代码的同时编写关于程序的文本。其思想是同时编写散文和程序。然而,这个想法并没有流行起来,因为完成像文本解析这样简单任务的开销很大。从系统编程的角度使用管道,Mcllroy 对 Knuth 的工作做出了回应。他仅用 6 行 shell 代码(使用管道)就完成了相同的文本解析任务。Knuth 从算法角度处理问题,而 Mcllroy 从系统角度处理问题,将中间输出链接在一起以得到答案。
管道很重要!
Mcllroy (http://doc.cat-v.org/unix/pipes/) Knuth (http://www.literateprogramming.com/knuthweb.pdf)
## Less 程序
`seq` 程序接受一个数字作为参数,并从第一个数字开始打印连续的数字。如果提供了第二个参数,则打印到第二个数字为止。否则,数字会无限地继续打印。
```
$ seq 2 5
2
3
4
5
```
如果我们将 `seq` 程序的输出通过管道传给 less,屏幕上显示的输出会被截断,因为通过管道传给 less 只显示足以填满屏幕的输出。`seq` 程序看起来像是暂停了。但它还在运行吗?
```
$ seq 2 100000000 | less
2
3
4
5
6
:█
```
如果我们将 `seq` 通过管道传给 `less`,并使用 `ps aux` 查看正在运行的进程列表,可以看到 `seq` 程序并未运行。使用 strace 进一步检查发生了什么,发现在一系列 `write` 命令之后,有一个 `SIGPIPE` 信号。当你向一个没有读取端的管道写入时,就会发生 `SIGPIPE`。`SIGPIPE` 的默认操作是杀死该程序。当一个程序的输出不再需要时,管道会自动杀死该程序。这就解释了为什么当 `seq` 被通过管道传给 less 时,它会被杀死。
## 使用管道实现 Waitpid
```c
waitpid(p, &status, 0); // 阻塞直到 p 退出或有信号到来
```
假设我们不在乎 `&status`,我们如何使用管道创建一个阻塞调用,当进程死亡时解除阻塞?当子进程返回时,我们希望 `read` 调用返回 0,因为所有子进程都已退出,管道的所有写入端都将关闭。
```c
int main() {
int pipefd[2];
pipe(pipefd);
pid_t p = fork();
if (p == 0) {
exec();
}
close(pipefd[1]); // 所有写入端必须关闭
char buf;
read(pipefd[0], &buf, 1); // read 系统调用将阻塞直到子进程退出
// 当子进程退出时,此调用将返回 0
close(pipefd[0]); // 管道卫生
}
```
## 父进程、子进程和僵尸进程
### 进程层次结构
每个进程都有一个单一的父进程。进程层次结构(或进程树)的根是一个称为 `init` 的进程,它的 pid 为 1。这是唯一不能被杀死的进程。`waitpid` 用于获取进程的退出状态。进程的退出状态存储在进程结构中,直到父进程需要该状态为止。`waitpid` 会收集状态并回收进程结构。这意味着进程结构可以被另一个进程重用。
### ManyFork
`manyfork` 程序尝试执行 fork 指令 10000 次。然而,如果我们运行 `./manyfork`,只创建了大约 3400 个进程。运行 `sudo ./manyfork`(赋予程序更多权限)会创建约 6890 个进程。操作系统正在保护用户免受失控程序的影响。如果我们查看 `manyfork` 程序创建的进程,会发现其中大部分是失效的。
### 僵尸进程
`manyfork` 程序没有使用 `waitpid` 等待其子进程。这会产生所谓的**僵尸进程**。僵尸进程是已经终止但尚未被父进程等待的进程。`ps` 命令可以识别这些僵尸进程。下面是运行 `manyfork` 程序后 `ps` 的输出示例。
```
user 78623 0.0 0.0 0 0 pts/0 Z+ 15:44 0:00 [manyfork]
user 78624 0.0 0.0 0 0 pts/0 Z+ 15:44 0:00 [manyfork]
user 78625 0.0 0.0 0 0 pts/0 Z+ 15:44 0:00 [manyfork]
```
`Z+` 列告诉我们这些进程是僵尸进程。这些僵尸进程是资源,即进程 ID。当一个子进程比其父进程存活得更久时,该子进程的父进程会被重新分配给 pid 为 1 的 `init` 进程。`init` 进程通过这种方式收集孤儿子进程。`init` 的工作是调用 waitpid 来收集孤儿子进程的资源。
相似文章
使用 systemfd 将终端输出管道传输到浏览器
本文演示了一种 Bash 脚本技巧,利用 systemfd、watchexec 和 socat,在 Rust Web 服务器开发期间将终端编译日志直接管道传输到浏览器。
@0xblacklight: https://x.com/0xblacklight/status/2055000745251070368
上下文分叉是一种编码代理使用的技术,用于回退对话历史,让开发者能够重用高质量上下文,节省时间和令牌,并探索不同的设计路径。本指南解释了如何有效使用上下文分叉。
Acorn Archimedes 上的 PipeDream
本文探讨了 Acorn Archimedes 及其 RISC OS 的历史,以 PipeDream 生产力套件为例,聚焦于这种偏离标准 WIMP 界面的早期计算创新。
In Parallel
In Parallel 是一款面向执行的操作系统,专为管理和协调并行工作流与任务而设计。
你已经安装了 `fzf`。接下来怎么办?(2023)
本文提供了一份实用指南,介绍如何最大限度地利用 `fzf` 模糊查找工具来提高生产力,展示了其与 shell 命令、`vi` 和 `ripgrep` 的集成,以实现高效的文件和命令搜索。