ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

MIT 6.S081 util 实验篇(lab1):pingpong (easy)

MIT 6.S081 util 实验篇(lab1):pingpong (easy) pingpong (easy)实验目标pingpong 是 util 实验里第一次接触进程间通信IPC的小关难度不高但把几个最基础、也最容易踩坑的 OS 概念串起来了练习使用管道pipe通过管道实现父进程 ↔ 子进程之间的双向简单通信父发ping子回pong。理解fork之后文件描述符fd如何共享以及pipe的读写端语义。体会一条管道编程的铁律“不需要的管道端要尽早 close”——否则read会阻塞或对端永远收不到 EOF。这些知识看着简单却是后面primes多进程管道链、乃至整个文件描述符 / 进程体系的地基值得单独写清楚。前置知识1. 进程与 fork 基础pingpong依赖fork()创建子进程。fork 之后父、子是两个独立进程但子进程拥有父进程内存和文件描述符表的副本注意是副本但指向的是同一批内核文件对象所以通过 fd 能看到同一管道。2. 管道pipe是什么——结合 xv6 手册xv6 手册原文Chapter 1: Pipes“A pipe is a small kernel buffer that is exposed to processes as a pair of file descriptors, one for reading and one for writing.”“pipe( p ) creates a new pipe, records the read and write file descriptors in p[0] and p[1], and returns 0 or -1.”“After fork(), the parent and the child have file descriptors referring to the same pipe.”“If no data is available, a read on a pipe waits for data to be written; … if all file descriptors referring to the write end of a pipe are closed, a read returns end-of-file (0).”提炼成要点管道 内核里一小块缓冲区对进程暴露为一对 fdp[0]是读端p[1]是写端。pipe(p)系统调用创建管道把读/写 fd 分别填进p[0]、p[1]成功返回 0。写入p[1]的数据可以从p[0]读出FIFO先进先出。fork之后父子持有指向同一管道的 fd因此可以通过它通信。3. 为什么必须 close 不用的端本实验的核心考点手册那句“if all file descriptors referring to the write end of a pipe are closed, a read returns end-of-file (0)”是关键管道的读端read在没数据时默认阻塞一直等到有数据写入或所有写端 fd 都被关闭时才返回后者返回 0表示 EOF。如果父进程迟迟不关闭自己手里用不到的写端那么子进程在读端read时即使数据已经读完内核仍认为还有一个写端开着可能还会写于是子进程会一直阻塞等待不会收到 EOF也不会退出 → 父进程的wait()也就永远等不到子进程结束形成死锁。更进一步文件描述符是有限资源一个进程打开的 fd 数量有上限不 close 会造成泄漏。所以铁律是自己不会用的那一端立刻 close。本实验中父子各自关掉两个用不到的端正是为了这个目的。4. 本实验的通信模型我们用两条管道实现双向通信避免同一管道双向读写的混乱管道写端WR1读端RD0用途p2c[2]父写子读父 → 子 发pingc2p[2]子写父读子 → 父 回pong一次交互的时序父往p2c[WR]写一个字节p子从p2c[RD]读到打印received ping此时getpid()是子进程pid子往c2p[WR]写回一个字节父从c2p[RD]读到打印received pong此时getpid()是父进程pid父wait()等子进程退出。注意两次打印的getpid()值不同父子 pid 不同顺序固定为ping在前、pong在后。实现思路顺着建管道 → fork → 各自关掉不用的端 → 按方向读写 → 收尾的思路创建两条管道p2c、c2ppipe()各调用一次。fork()出子进程通过返回值区分父子子进程用不到p2c的写端、c2p的读端 → 先close掉然后read(p2c[RD])收ping、write(c2p[WR])回pong用完再close剩余两端。父进程用不到p2c的读端、c2p的写端 → 先close掉然后write(p2c[WR])发ping、read(c2p[RD])收pong用完close剩余两端最后wait(0)等子进程。任何read/write出错都通过fprintf(2, ...)打到标准错误fd 2并以非零退出码结束。代码实现user/pingpong.c—— 管道双向通信/* * user/pingpong.c */#includekernel/types.h#includeuser/user.h#defineRD0#defineWR1intmain(){intexit_status0;charbufp;// 创建两个管道intp2c[2],c2p[2];pipe(p2c);pipe(c2p);// 创建进程intpidfork();if(pid0){fprintf(2,fork failed!\n);exit_status1;}elseif(pid0){// 子进程// 子进程不会通过 p2c 向父进程写close(p2c[WR]);// 子进程不会通过 c2p 从父进程读close(c2p[RD]);// 子进程读if(read(p2c[RD],buf,sizeof(char))!sizeof(char)){fprintf(2,child: read error!\n);exit_status1;}else{printf(%d: received ping\n,getpid());}// 子进程写if(write(c2p[WR],buf,sizeof(char))!sizeof(char)){fprintf(2,child: write error!\n);exit_status1;}// 及时关闭管道的端close(p2c[RD]);close(c2p[WR]);}else{// 父进程close(p2c[RD]);close(c2p[WR]);// 父进程写if(write(p2c[WR],buf,sizeof(char))!sizeof(char)){fprintf(2,parent: write error!\n);exit_status1;}// 父进程读if(read(c2p[RD],buf,sizeof(char))!sizeof(char)){fprintf(2,parent: read error\n);exit_status1;}else{printf(%d: received pong\n,getpid());}// 及时关闭管道的端close(p2c[WR]);close(c2p[RD]);// 父进程等待子进程的结束wait((int*)0);}exit(exit_status);}几点说明RD0、WR1对应pipe()返回的读/写端下标p[0]读、p[1]写用宏让代码语义更清楚。子进程先close(p2c[WR])和close(c2p[RD])——这正是前置知识里强调的关掉用不到的端避免对端read永远阻塞。read/write都检查返回值是否等于sizeof(char)1不相等就视为出错这是健壮的 I/O 写法实际只传 1 字节但养成检查返回值的习惯很重要。父进程最后wait((int*)0)或wait(0)等待子进程退出确保子进程先结束、回收其资源。Makefile—— 把程序编进内核镜像新写的用户程序仍需在Makefile的UPROGS里注册。在列表中新增一行其余程序用...表示省略UPROGS\ $U/_cat\ $U/_echo\ ... $U/_zombie\ $U/_sleep\ $U/_pingpong\ # 添加 $U/_pingpong验证方式一手动在 xv6 里测makeqemu# 启动 xv6进入 shell 后执行pingpong预期输出pid 数值取决于调度但顺序固定、ping在前pong在后且两个 pid 不同4: receivedping3: received pong方式二用评分脚本测在 Linux 终端运行./grade-lab-util pingpong应看到测试通过Test pingpongpingpong: OK(2.9s)复盘本实验解决了什么表面只是父子互发一个字节实则把进程间通信的骨架立了起来第一次用管道做 IPC——理解了内核缓冲区 一对 fd的通信模型。第一次面对 fd 共享与关闭时机——fork让父子共享 fd而尽早 close 用不到的端是管道编程不出 bug 的前提。为后续做铺垫下一关primes会用进程链 管道实现素数筛再往后sh的命令管道|也是同一套机制。为什么 close 是铁律再强调一次结合手册的 EOF 规则只有当所有写端 fd 都关闭read才会返回 EOF。本实验只传 1 字节子进程read拿到字节就返回、不会卡在等 EOF上所以单次交互里不 close 未必立刻死锁但一旦通信变成多轮、或对方依赖 EOF 来结束读取不 close 写端必然死锁。把用完即关变成肌肉记忆比事后调试死锁划算得多。另外 fd 是有限资源不关就是泄漏。收获进程间通信IPC方式能列举管道、消息队列、共享内存、信号、socket 等并说清匿名管道只能用于有亲缘关系的进程fork 出来的命名管道/FIFO 才能跨无亲缘进程——pingpong 正是匿名管道 fork 的最小范例。文件描述符与 fork 的关系fork 后子进程复制父的 fd 表但指向同一内核文件对象引用计数归零才真正关闭。这正是为什么父子共享同一条管道的底层原因。阻塞 vs EOFread在管道空时阻塞所有写端关闭才返回 0EOF——这是理解为什么必须 close 写端的关键也是高频面试题。wait/ 僵尸进程父进程wait()回收子进程避免子进程变成僵尸进程占用进程表项。建议把前置知识里的管道通信模型表 EOF 规则截图存一份——primes的管道都会反复用到且坑都出在该关的 fd 没关。
返回列表