管道与进程通信 — `cat memmap | grep hoge` 在自制 OS 里跑通
管道与进程通信 — cat memmap | grep hoge 在自制 OS 里跑通
这一章讲什么: 单个命令会用了,该让它们合伙干活了。 **管道(|)**把左边命令的标准输出接到右边命令的标准输入——
cat memmap | grep hoge从此不再是 Linux 的专利。 为把它跑通,先收拾退出码(0 正常、非 0 出错、echo $?可查), 再造管道本体 PipeDescriptor、补任务终止机制 Finish(), 顺手收获 sort 命令;章末共享内存登场——同一块物理内存映射给多个应用,双向通信。
1. 先看现象:一条管道的一生
主线例子:
cat memmap | grep 3FE.D000
终端解析命令行发现 | → 建一个 PipeDescriptor(带缓冲的特殊文件描述符)
→ 左边 cat 的 files_[1](stdout)换成管道的写端
→ 右边 grep 由一个「执行完命令就退出」的临时终端(subtask)启动,
它的 files_[0](stdin)接到管道的读端
→ 两边并发跑:cat 往里写,grep 从里读
→ cat 结束 → FinishWrite() 告诉读端「没有数据了」(= EOF)→ grep 读到 0 自然收工
→ 临时终端 exit_after_command=true,命令一完任务自杀(Finish)
echo $? → 打印上一条命令的退出码(0=正常,非 0=出错;应用加载失败取负区分)
sort @stdin | …… → 小工具开始组合2. 顶层全景
29.1 退出码:last_exit_code_;$?;出错=1;ExecuteFile 返回 int(加载失败取负)
29.2–29.4 管道:| 的语义;两种实现(顺序等左/左右并发)选并发;
PipeDescriptor(files_[1] 换写的顺序陷阱;FinishWrite = EOF);
subtask + term_desc 描述新终端的初始 fd
29.5–29.6 终端开关与任务终止:exit_after_command;TaskManager::Finish()
29.7 sort;29.8 管道终端的错误修复;29.9 共享内存(可写共享帧 vs 管道单向字节)
图说:一个 | 符号,牵出退出码、任务终止、临时终端一整个配套。3. 核心原理
3.1 退出码先立规矩
每次应用退出都打 app exited. ret = 3 太吵,改为存进 last_exit_code_,
shell 风格 echo $? 查询1。规矩:0 = 正常,非 0 = 出错;命令找不到文件、
目标是目录,exit_code = 12。ExecuteFile 从 void 改返回 int;
应用加载失败的 err 不等于应用失败,退出码取负以示区分3。
3.2 管道:一个 | 背后的并发
管道 = 把一个命令的标准输出连到另一个命令的标准输入4。 两种实现:等左边跑完记录输出再喂右边(简单但慢); 左右并发(本文选择)——grep 边读边过滤,cat 不用全跑完5。 实现三件套:
- PipeDescriptor:管道的本体,一种特殊的文件描述符(带缓冲); 写端被当作左边命令的 files_[1],读端被当作右边的 files_[0]6;
- 顺序陷阱:「files_[1] = pipe_fd 这行必须放在生成 term_desc 之后—— 提前了,新终端的 files_[1] 会错误地连到管道」7;
- FinishWrite:左边跑完调用它,告诉读端「不再发送数据」—— 读端收到 0,与文件 EOF 行为一致,应用零修改8。
3.3 临时终端与任务终止
右边命令不能跑在当前终端里(当前终端要等整条管道),于是起 subtask:
一个以 term_desc 描述的临时终端(初始 fd 配好、命令行配好),
exit_after_command=true——命令一完,终端任务自杀9。
自杀靠新机制 TaskManager::Finish():把任务从运行队列摘除、
在 tasks_ 记「已完成」、唤醒 finish_waiter_ 上等它的人——
「不仅适用于终端,也适用于 Task 代表的所有任务」10。
3.4 sort 与共享内存
sort(29.7):读全部行、排序、输出——管道右端的优等生,
grep ... | sort 的组合当场演示11。
共享内存(29.9):同一物理帧可写地映射进多个应用的虚拟空间——
与写入时复制同族,只是权限换成共享可写12。
对照管道:管道是单向字节流;共享内存双向、能放数组和结构体,
高级表达力更强——代价是没有内置的同步,得自己想办法13。
4. 作者的判断与证据
判断:并发管道是正解,尽管更难。 顺序实现的简单摆在桌上,仍选了并发—— 「这有点复杂,但最能体现( Unix 式组合的)修改全貌」。
判断:通信机制的进化路线:文件 → 管道 → 共享内存。 每一代补上一代的短板: 文件慢且落盘、管道单向只能字节、共享内存双向能传结构——同步的责任随之转移给应用。
证据: cat | grep、grep | sort 的截图;管道终端 bug(29.8)的复现与修复;共享内存与 COW 的机制对照。
5. 边界与局限
- 管道缓冲在内存里,没有背压与容量上限的正式语义;
- 管道只有竖线一种接法,没有 2>&1、多路复用;
- 共享内存没有同步原语(锁、信号量),两个应用同时写是裸奔;
- Finish 是协作式终止(任务在切换点才被摘),不能抢占杀死死循环任务。
6. 可带走的
本章主线例子,一行写完: $? 立规矩 → | 解析建 PipeDescriptor → 左写端/右读端(files_[1] 换写顺序有讲究)→ 并发跑 → FinishWrite = EOF → 临时终端 exit_after_command + Finish() 收尾 → sort/grep 组合;共享内存作双向互补。
- 退出码:0 正常、非 0 出错;
$?查询;应用错误与加载错误分开编码; - 管道两种实现,并发才是 Unix 味;
- PipeDescriptor = 特殊 fd;换 fd 的顺序即语义,顺序错全错;
- FinishWrite 把「左边完了」翻译成 EOF,读端零修改;
- 管道右端要临时终端 + exit_after_command;Finish() 是通用任务终止;
- 共享内存 = 可写共享帧,双向、传结构体;同步自理;
- 通信机制史:文件 → 管道(单向字节) → 共享内存(双向结构化)。
7. 原文地图
| 主题 | 原书章 | 原文位置 |
|---|---|---|
| $? 与退出码规矩 | 29.1 退出码(osbook_day29a) | text/178-ch29-01-29-1-osbook-day29a.txt:14(搜「$?」) · text/178-ch29-01-29-1-osbook-day29a.txt:47(搜「0表示正常退出」) · text/178-ch29-01-29-1-osbook-day29a.txt:61(搜「取负」) |
| 管道定义与并发选择 | 29.2 管道(osbook_day29b) | text/179-ch29-02-29-2-osbook-day29b.txt:22(搜「管道的符号」) · text/179-ch29-02-29-2-osbook-day29b.txt:33(搜「两种实现方式」) · text/179-ch29-02-29-2-osbook-day29b.txt:49(搜「同时激活左右」) |
| PipeDescriptor 与顺序陷阱 | 29.4 管道处理的主体PipeDescriptor | text/181-ch29-04-29-4-pipedescriptor.txt:11(搜「files_[1] = pipe_fd」) · text/181-ch29-04-29-4-pipedescriptor.txt:25(搜「类的定义」) |
| FinishWrite = EOF | 29.4/29.5 | text/181-ch29-04-29-4-pipedescriptor.txt:17(搜「FinishWrite」) · text/182-ch29-05-29-5.txt:10(搜「无数据可发送」) |
| term_desc 与 exit_after | 29.5 启动和关闭终端 | text/182-ch29-05-29-5.txt:25(搜「为true时终止任务」) · text/183-ch29-06-29-6.txt:2(搜「终止」) |
| Finish() 通用终止 | 29.6 终止任务 | text/183-ch29-06-29-6.txt:16(搜「无法终止」) · text/183-ch29-06-29-6.txt:20(搜「Finish()的实现」) |
| sort 命令 | 29.7 排序命令(osbook_day29c) | text/184-ch29-07-29-7-osbook-day29c.txt:8(搜「sort」) · text/184-ch29-07-29-7-osbook-day29c.txt:35(搜「Ctrl+D」) |
| 共享内存定义 | 29.9 共享内存 | text/186-ch29-09-29-9.txt:25(搜「同一个物理帧」) · text/186-ch29-09-29-9.txt:36(搜「单向」) |
Footnotes
-
出处:「29.1 退出码」(
text/178-ch29-01-29-1-osbook-day29a.txt:14,搜「$?」;:20,搜「last_exit_code_」)。 ↩ -
出处:「29.1 退出码」(
text/178-ch29-01-29-1-osbook-day29a.txt:47,搜「0表示正常退出」;:49,搜「设置为1」)。 ↩ -
出处:「29.1 退出码」(
text/178-ch29-01-29-1-osbook-day29a.txt:52,搜「返回值」;:59,搜「取负」;:61,搜「事出有因」)。 ↩ -
出处:「29.2 管道」(
text/179-ch29-02-29-2-osbook-day29b.txt:22,搜「管道的符号」;:28,搜「标准输入」)。 ↩ -
出处:「29.2 管道」(
text/179-ch29-02-29-2-osbook-day29b.txt:33,搜「两种实现方式」;:49,搜「同时激活左右」)。 ↩ -
出处:「29.4 管道处理的主体PipeDescriptor」(
text/181-ch29-04-29-4-pipedescriptor.txt:25,搜「类的定义」;:28,搜「files[0]」;:11,搜「files_[1]」)。 ↩ -
出处:「29.4 管道处理的主体PipeDescriptor」(
text/181-ch29-04-29-4-pipedescriptor.txt:11,搜「files_[1] = pipe_fd」;:12,搜「导致行为错误」)。 ↩ -
出处:「29.4/29.5」(
text/181-ch29-04-29-4-pipedescriptor.txt:17,搜「FinishWrite」;text/182-ch29-05-29-5.txt:10,搜「无数据可发送」)。 ↩ -
出处:「29.5 启动和关闭终端」(
text/182-ch29-05-29-5.txt:25,搜「为true时终止任务」);text/183-ch29-06-29-6.txt:2,搜「终止」)。 ↩ -
出处:「29.6 终止任务」(
text/183-ch29-06-29-6.txt:16,搜「无法终止」;:20,搜「Finish()的实现」;:22,搜「唤醒」)。 ↩ -
出处:「29.7 排序命令」(
text/184-ch29-07-29-7-osbook-day29c.txt:8,搜「sort」;:34,搜「Ctrl+D」;:36,搜「grep和sort相结合」)。 ↩ -
出处:「29.9 共享内存」(
text/186-ch29-09-29-9.txt:25,搜「同一个物理帧」;:35,搜「写入权限」)。 ↩ -
出处:「29.9 共享内存」(
text/186-ch29-09-29-9.txt:36,搜「单向」;:38,搜「高级表达」)。 ↩