跳转至

管道与进程通信 — 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。 实现三件套:

  1. PipeDescriptor:管道的本体,一种特殊的文件描述符(带缓冲); 写端被当作左边命令的 files_[1],读端被当作右边的 files_[0]6;
  2. 顺序陷阱:「files_[1] = pipe_fd 这行必须放在生成 term_desc 之后—— 提前了,新终端的 files_[1] 会错误地连到管道」7;
  3. 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 组合;共享内存作双向互补。

  1. 退出码:0 正常、非 0 出错;$? 查询;应用错误与加载错误分开编码;
  2. 管道两种实现,并发才是 Unix 味;
  3. PipeDescriptor = 特殊 fd;换 fd 的顺序即语义,顺序错全错;
  4. FinishWrite 把「左边完了」翻译成 EOF,读端零修改;
  5. 管道右端要临时终端 + exit_after_command;Finish() 是通用任务终止;
  6. 共享内存 = 可写共享帧,双向、传结构体;同步自理;
  7. 通信机制史:文件 → 管道(单向字节) → 共享内存(双向结构化)。

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(搜「单向」)

  1. 出处:「29.1 退出码」(text/178-ch29-01-29-1-osbook-day29a.txt:14,搜「$?」;:20,搜「last_exit_code_」)。 ↩

  2. 出处:「29.1 退出码」(text/178-ch29-01-29-1-osbook-day29a.txt:47,搜「0表示正常退出」;:49,搜「设置为1」)。 ↩

  3. 出处:「29.1 退出码」(text/178-ch29-01-29-1-osbook-day29a.txt:52,搜「返回值」;:59,搜「取负」;:61,搜「事出有因」)。 ↩

  4. 出处:「29.2 管道」(text/179-ch29-02-29-2-osbook-day29b.txt:22,搜「管道的符号」;:28,搜「标准输入」)。 ↩

  5. 出处:「29.2 管道」(text/179-ch29-02-29-2-osbook-day29b.txt:33,搜「两种实现方式」;:49,搜「同时激活左右」)。 ↩

  6. 出处:「29.4 管道处理的主体PipeDescriptor」(text/181-ch29-04-29-4-pipedescriptor.txt:25,搜「类的定义」;:28,搜「files[0]」;:11,搜「files_[1]」)。 ↩

  7. 出处:「29.4 管道处理的主体PipeDescriptor」(text/181-ch29-04-29-4-pipedescriptor.txt:11,搜「files_[1] = pipe_fd」;:12,搜「导致行为错误」)。 ↩

  8. 出处:「29.4/29.5」(text/181-ch29-04-29-4-pipedescriptor.txt:17,搜「FinishWrite」;text/182-ch29-05-29-5.txt:10,搜「无数据可发送」)。 ↩

  9. 出处:「29.5 启动和关闭终端」(text/182-ch29-05-29-5.txt:25,搜「为true时终止任务」);text/183-ch29-06-29-6.txt:2,搜「终止」)。 ↩

  10. 出处:「29.6 终止任务」(text/183-ch29-06-29-6.txt:16,搜「无法终止」;:20,搜「Finish()的实现」;:22,搜「唤醒」)。 ↩

  11. 出处:「29.7 排序命令」(text/184-ch29-07-29-7-osbook-day29c.txt:8,搜「sort」;:34,搜「Ctrl+D」;:36,搜「grep和sort相结合」)。 ↩

  12. 出处:「29.9 共享内存」(text/186-ch29-09-29-9.txt:25,搜「同一个物理帧」;:35,搜「写入权限」)。 ↩

  13. 出处:「29.9 共享内存」(text/186-ch29-09-29-9.txt:36,搜「单向」;:38,搜「高级表达」)。 ↩