uCore 实现
管道让两个进程经内核中的缓冲区传递字节。父进程先创建管道,再 fork;父子进程各保留一端,便能在不同地址空间之间通信。以下代码来自 uCore 2026A 第七章。
管道对象与描述符
pipealloc 分配一页保存 struct pipe。管道内部有 512 字节缓冲区、读写计数器和两端是否打开的标志。sys_pipe 创建分别用于读、写的两个文件对象,再把它们放进当前进程的描述符表。
内核将两个描述符写回用户数组。该接口按两个 uint64 写入,共 16 字节;调用者应使用课程用户库约定的数组类型。fork 之后,父子进程的描述符指向同一对文件对象,因此也指向同一管道。
环形缓冲区
nread、nwrite 记录累计读取和写入的字节数。下一次访问的数组下标分别是 nread % 512 与 nwrite % 512。两者相等表示缓冲区为空;nwrite == nread + 512 表示已满。
复制时,piperead、pipewrite 都会在数组末尾截断本段长度。假设下一写入位置是 508,写入 12 字节,前 4 字节落在 508–511,后 8 字节从下标 0 开始。用户缓冲区若跨越页边界,copyin 或 copyout 还会按用户页再次分段。
读写与调度
写端在缓冲区满时调用 yield,获得处理器后重新检查空间;读端在空缓冲区上等待也采用同一办法。进程被切走时,内核栈保存着 pipewrite 或 piperead 的局部进度,恢复后继续原来的循环。这里没有专门的管道等待队列。
pipewrite 尝试写完请求的全部字节。piperead 有数据后最多读到当前可用量,缓冲区一旦变空就返回,所以一次读取可能短于请求长度。调用者需要按返回的字节数累计结果。
端点关闭与资源释放
fork 增加文件对象的引用数。只有读端文件对象的最后一份引用关闭,readopen 才变为 0;写端同理。写端关闭后,读端仍可取走剩余数据。缓存读空后再次读取,在本实现中返回 -1;读端关闭后,写入也返回 -1。
两端都关闭时,pipeclose 归还管道页。关闭父进程自己的描述符不会影响子进程仍持有的引用;进程退出时逐个关闭文件,也可能完成管道的最终回收。
用户地址与失败处理
pipewrite 从调用进程的用户页表经 copyin 取字节,piperead 经 copyout 写回。两个进程只通过内核管道中的字节交换数据,各自使用自己的用户页表转换缓冲区地址。
sys_pipe 依次取得文件对象、分配管道页、分配两个描述符、写回用户数组。中途失败会关闭已经取得的端点,并清空已占用的描述符槽。两个端点都关闭后,管道页也被释放。读写请求长度小于等于零则触发 panic;描述符无效时,系统调用返回 -1。
程序执行与通信
课程程序 ch7b_pipetest 创建管道后调用 fork。父进程关闭读端,写入 hello pipe!,关闭写端;子进程关闭写端,从读端取得内容并校验。父进程随后调用 wait。这条路径同时用到了共享文件对象、独立用户地址空间和进程调度。
exec 换掉用户程序时仍保留描述符表。因此,已打开的管道端点也可以交给新程序使用。
运行观察
边界实验让父进程写入 3000 字节,子进程每次请求 137 字节,并逐字节核对内容。写入量大于 512 字节的管道缓冲区,写端必然要等待读端腾出空间。实验还把用户缓冲区放在页尾附近,经过跨页的 copyin、copyout。
子进程分 24 次取得全部 3000 字节,其中有短读;写端关闭且缓冲区读空后,再读返回 -1。实验还填满描述符表,使第二个描述符分配失败,然后关闭已有管道并重新创建。沿 sys_pipe → fdalloc → fileclose → pipeclose 能看到失败后的资源清理。