CH5 补充读物:IPC(从 pingpong 读懂 pipe)¶
1. 这份读物怎么用¶
这份文档是给你课后独立看进程代码用的,不是课堂提纲。
读完后,你应该能回答下面 8 个问题:
- 当前仓库真正实现了哪一种 IPC?
pingpong为什么是读 CH5 最合适的起点?pipe()创建出来的到底是什么内核对象?- 为什么
fork()对 ordinary pipe 特别关键? struct pipe如何体现 bounded buffer?- 缓冲区空或满时,当前代码如何阻塞与唤醒?
- 当前
pipe在 naming、synchronization、buffering 三个维度上分别是什么语义? - 哪些 PPT 内容在本仓库里还没有对应实现?
2. 从一个真实程序开始:先运行 pingpong¶
建议你先在系统里实际运行一次:
然后按这条路径阅读代码:
user/pingpong.c:父子进程如何创建两条管道并互相发消息。user/syscall.c:pipe/read/write/fork/close/wait如何触发ecall。kernel/core/syscall.c:sys_pipe、sys_read、sys_write如何进入内核对象层。kernel/core/file.c:通用文件接口如何把read/write/close转发给 pipe。kernel/core/pipe.c:FIFO 缓冲区、空满判断和阻塞唤醒的真正实现。kernel/core/proc.c:sleep()/wakeup()如何支撑阻塞式 IPC。
如果你能把这 6 步串起来,CH5 在当前仓库里的主线就已经抓住了。
3. 先把边界说清:当前仓库实现了哪种 IPC¶
在这套代码里,最值得看的 IPC 机制是:ordinary pipe(匿名管道)。
也就是说:
- 已实现并且有用户程序样例的,是
pipe(int fd[2])。 - 最合适的观察样本是
user/pingpong.c。 - 当前没有 POSIX shared memory、
mmap风格共享映射、FIFO 或socket()用户接口。 - 当前
user/sh.c只支持普通命令执行,不支持解析|这样的 shell 管道语法。
这一章的正确期待
读 CH5 时,不要试图在当前仓库里把所有 IPC 机制都找齐。更准确的理解是:课程代码提供了一个非常典型、非常适合教学的 IPC 样本——ordinary pipe;你需要借它把 PPT 中的 bounded buffer、blocking communication 和 producer-consumer 真正看懂。
4. cooperating processes 在当前代码里最直观的样子¶
PPT 里说 cooperating processes 是“会互相交换数据、互相影响推进”的执行实体。
在当前仓库中,最直观的例子就是 pingpong:
- 父进程先调用
pipe()创建通信通道。 - 再调用
fork()创建子进程。 - 父子进程分别关闭自己不用的端口。
- 一侧
write(),另一侧read()。 - 双方通过同一个内核 pipe 对象交换数据并同步推进。
这就是 cooperating processes 在课程代码里的最小闭环。
5. pipe() 是怎么从系统调用变成内核对象的¶
从用户态看,接口很简单:
但内核里实际上发生了下面几步:
user/syscall.c:pipe()把系统调用号放进寄存器后执行ecall。kernel/core/syscall.c:sys_pipe()调用pipealloc(&rf, &wf)创建读端和写端。sys_pipe()再通过fdalloc()把这两个struct file *挂到当前进程的ofile[]表中。- 最后通过
copyout()把两个文件描述符编号写回用户态数组。
因此,用户态拿到的 fd[0] / fd[1] 只是编号;真正的通信对象已经在内核里创建好了。
6. 为什么 fork() 对 ordinary pipe 特别关键¶
ordinary pipe 最自然的使用场景通常是 related processes,尤其是 parent-child。当前代码里这个结论是可以直接从实现读出来的。
关键点在 kernel/core/proc.c:fork():
- 子进程会复制父进程的
ofile[]。 - 复制方式不是新建一套 pipe,而是对现有
struct file调用filedup()增加引用计数。 - 这两个
struct file又都指向同一个struct pipe。
这意味着:
- 父子进程天然共享同一对 pipe 端点。
- 之后只要各自关闭不用的一端,就能形成清晰的单向通信路径。
为什么 pingpong 要用两条 pipe
一条 ordinary pipe 在当前实现里是单向的:fd[0] 读,fd[1] 写。因此 pingpong 要完成“去一次、回一次”的双向通信,就必须建立两条 pipe。
7. struct pipe:bounded buffer 在代码里的真实形态¶
PPT 里的 bounded buffer 在当前代码里并不是抽象图,而是 kernel/core/pipe.c 里的这个对象:
data[PIPESIZE]:真正的数据缓冲区。nread:已经读到哪里。nwrite:已经写到哪里。readopen/writeopen:读端和写端是否还开着。lock:保护共享状态的自旋锁。
而 PIPESIZE 在当前实现里被定义为:
这就是 CH5 里“有限容量缓冲区”的代码落点。
8. Producer-Consumer 在当前代码里到底怎么跑¶
如果把 pipe 看成 producer-consumer 模型,那么:
pipewrite()是 producer。piperead()是 consumer。struct pipe就是双方共享的有限 FIFO 缓冲区。
8.1 缓冲区满时,写者怎么办¶
pipewrite() 的核心判断是:
- 如果
pi->nwrite == pi->nread + PIPESIZE,说明缓冲区满了。 - 此时写者不会 busy waiting。
- 它会先
wakeup(&pi->nread)提醒可能的读者,再sleep(&pi->nwrite, &pi->lock)进入阻塞。
8.2 缓冲区空时,读者怎么办¶
piperead() 的核心判断是:
- 当
pi->nread == pi->nwrite且writeopen仍为真时,说明“暂时没数据,但写端还活着”。 - 此时读者也不会空转。
- 它会
sleep(&pi->nread, &pi->lock)等待新数据到来。
8.3 这正是 PPT 里“不要忙等”的代码版本¶
因此,PPT 里的 bounded buffer 在当前仓库里已经不是伪代码,而是完整可运行逻辑:
- 满了就让 producer 睡眠。
- 空了就让 consumer 睡眠。
- 条件变化后,对端负责
wakeup()。
9. pipe 在 naming、synchronization、buffering 三个维度上的语义¶
把当前实现放回 PPT 的分析框架,可以得到很清楚的结论:
9.1 Naming¶
当前 pipe 不是 mailbox,也不是 FIFO 文件名。
它更接近:
- 进程通过文件描述符持有通信端点。
- 这些端点通常经由
fork()继承给 related processes。 - 因此它没有一个全局名字供任意进程随时打开。
9.2 Synchronization¶
当前 pipe 具有明显的 blocking 语义:
- 读端可能因为“缓冲区空”而睡眠。
- 写端可能因为“缓冲区满”而睡眠。
- 所以它不是简单的 non-blocking message queue。
9.3 Buffering¶
当前 pipe 是典型的 bounded buffering:
- 底层 FIFO 长度有限。
- 容量上限由
PIPESIZE决定。 - 缓冲区满时,发送方必须等待。
10. close() 为什么也是 IPC 语义的一部分¶
很多同学读 CH5 时只看 read/write,忽略 close,这是不够的。
在当前代码里:
fileclose()会在FD_PIPE情况下转发到pipeclose()。pipeclose()会更新readopen或writeopen。- 同时会
wakeup()对端,避免对方永远睡下去。
这会带来两个非常重要的语义:
- 如果写端都关了,而且缓冲区也读空了,
piperead()会返回0,这就是 EOF 风格语义。 - 如果读端已经关了,
pipewrite()会看到readopen == 0,从而返回错误。
所以,普通的“关闭文件描述符”在 pipe 场景下其实也是通信协议的一部分。
11. Shared Memory 和 Message Passing 在当前仓库里怎么区分¶
PPT 把 IPC 分成 shared memory 和 message passing 两大类。放到当前仓库里,最容易得出的结论是:
- 当前课程代码主要落在 message passing 这一侧,因为用户态通过
read/write面向“通道”交换数据。 - 但从内核实现视角看,
pipe又确实是一块被锁保护的共享 FIFO 对象。
两个视角都对
从接口层看,pipe 是消息传递;从实现层看,pipe 又是共享对象上的受控读写。真正重要的不是二选一,而是你要分清自己此刻讨论的是“抽象 API”还是“内核内部实现”。
12. 当前代码没有实现哪些 CH5 机制¶
读完 pipe.c 之后,请明确边界,不要过度延伸:
- 当前没有 POSIX shared memory 的
shm_open()/mmap()路径。 - 当前没有 named pipe(FIFO)对应的用户接口。
- 当前没有网络协议栈与
socket()接口。 - 当前 shell 也没有把
|解析成管道命令。
所以 CH5 的正确阅读姿势是:
- 用
pipe把 IPC 的基础模型看懂。 - 用 PPT 建立更完整的 IPC 地图。
- 始终区分“课程代码已经实现了什么”和“理论课还介绍了哪些更一般机制”。
13. 建议的代码阅读主线¶
按下面顺序读,会最顺:
user/pingpong.cuser/syscall.ckernel/core/syscall.c中的sys_pipe、sys_read、sys_writekernel/core/file.ckernel/core/pipe.ckernel/core/proc.c中的sleep()、wakeup()
这条线能把“用户程序里的两个 fd”一路追到“内核里的 bounded buffer 和阻塞唤醒机制”。
14. 最常见误解¶
- 以为 CH5 在当前仓库里应该能直接看到 shared memory、FIFO 和 socket 的完整实现。
- 以为 shell 已经支持
cmd1 | cmd2,于是去user/sh.c里找不存在的解析逻辑。 - 以为 ordinary pipe 是天然双向的,忽略了当前实现其实是一读一写的单向通道。
- 以为 IPC 只是“把数据送过去”,忽略了空/满条件、阻塞语义和关闭语义。
- 把
sleep/wakeup当成某种用户态库函数,而不是内核里的阻塞协调机制。
15. 最小自测(仅自检)¶
你可以问自己这 8 个问题:
- 当前仓库里最适合读 IPC 的用户程序为什么是
pingpong? sys_pipe()做了哪几件关键事情?fork()为什么会让 ordinary pipe 天然适合 parent-child?struct pipe里哪几个字段最能体现 bounded buffer?pipewrite()在缓冲区满时为什么不 busy waiting?piperead()什么时候会返回0?- 当前
pipe在 naming、synchronization、buffering 三个维度上的语义分别是什么? - 当前仓库里有哪些 CH5 机制还没有实现?
如果这些问题都能结合源码回答,CH5 的主线就真正掌握了。