CH3 补充读物:Process Concepts & Operations(读懂 fork/exec/wait)¶
1. 这份读物怎么用¶
这份文档是给你课后独立看进程代码用的,不是课堂提纲。
读完后你应该能回答:
- 进程在这套内核里由什么数据结构表示。
- 进程状态怎么变化,谁触发变化。
fork/exec/wait/exit这四个动作怎样拼成完整生命周期。
2. 先建立一个最重要的区分:Program vs Process¶
把这个区分想清楚,后面就不容易乱:
- Program:静态文件(例如
fs.img里的/ls、/sh、/cat)。 - Process:运行中的执行实体(有 PID、状态、地址空间、打开文件等)。
在当前项目里:
user/*.c被编译成 ELF。tools/mkfsimg.py把 ELF 打包进镜像。exec把 ELF 装载成正在运行的进程。
3. 进程在代码里长什么样:struct proc¶
直接看:kernel/include/proc.h。
这个结构体就是你要找的“PCB 对应物”,关键字段包括:
pid/state/parent/xstate:进程身份与关系。context:调度切换用寄存器上下文。pagetable/sz/trapframe:用户地址空间与执行现场。ofile[]/cwd:打开文件和当前目录。
当你调试进程问题,几乎都会落回这些字段。
4. 进程状态:从概念图到真实代码¶
状态枚举在 enum procstate:
UNUSEDUSEDSLEEPINGRUNNABLERUNNINGZOMBIE
你可以把它看成一张“可执行状态机”。
关键转换路径:
scheduler():挑RUNNABLE进程运行。yield():运行进程主动让出 CPU。sleep():进入阻塞等待。wakeup():唤醒阻塞进程。proc_exit():进程结束进入ZOMBIE。wait():父进程回收子进程,释放资源。
5. fork:如何“克隆”出子进程¶
调用链:
- 用户态
fork()。 sys_fork。kernel/core/proc.c:fork()。
核心动作:
allocproc()分配新struct proc。uvmcopy()复制父进程用户内存。- 复制 trapframe,但子进程返回值设为
0。 - 复制打开文件引用与工作目录。
- 设置
parent,把子进程置为RUNNABLE。
你要记住两个返回值语义:
- 父进程看到的是子 PID。
- 子进程看到的是
0。
6. exec:不是新建进程,而是替换运行镜像¶
调用链:
- 用户态
exec()。 sys_exec。kernel/core/proc.c:exec()。
核心行为:
- 创建新页表。
- 装载目标 ELF。
- 重新搭建用户栈并拷贝参数。
- 替换当前进程的
pagetable/sz/epc/sp。 - 释放旧地址空间。
所以 exec 后:
- PID 还是原来的。
- 运行程序变成新程序。
- 旧用户态镜像被替换。
7. wait 与 exit:父子同步与回收闭环¶
这是最容易“概念懂了,代码没懂”的一段。
在这套实现里:
- 子进程
proc_exit(status)后进入ZOMBIE。 - 子进程唤醒可能在等它的父进程。
- 父进程
wait(addr)扫描子进程表。 - 找到
ZOMBIE后取退出码并回收资源。
这就是“退出不等于立刻消失,必须被父进程回收”的实际机制。
8. orphan 问题是怎么处理的¶
如果父进程先退出,子进程会 orphan。
本项目在 proc_exit() 里做了标准处理:
- 把这些子进程重挂到
initproc。 - 由 init 继续承担回收责任。
这一步是避免“没人回收导致永久悬挂”的关键。
9. 建议你亲手走一遍的观察链¶
以 shell 启动一条普通命令为例:
- shell 调
fork。 - 子进程
exec命令程序。 - 父进程
wait。 - 子进程退出,父进程被唤醒并回收。
对照文件:
user/sh.ckernel/core/syscall.ckernel/core/proc.c
10. 最常见误解¶
- 把
fork误解成“创建全新无关进程”。 - 把
exec误解成“再开一个进程”。 - 把
wait误解成“仅仅睡一会儿”。 - 忽略
ZOMBIE的存在和意义。
11. 最小自测(不交作业,仅自检)¶
你可以自问这 6 个问题:
struct proc里哪几个字段最能体现“进程是动态实体”?- 子进程为什么在
fork后返回0? exec为什么 PID 不变?wait在没有子进程可回收时会怎样?ZOMBIE为什么必须存在?- orphan 最终由谁处理?
如果你能给出具体函数和字段位置,CH3 就基本掌握了。