Skip to content

进程:复制执行现场,替换程序,等待资源回收 ​

fork 复制的是什么 ​

进程是一个受保护的执行环境:地址空间、寄存器、打开文件、当前目录、状态和身份。程序文件只是其中的输入。两个进程可以运行同一个程序,却拥有不同的变量;一个进程也可以通过 exec 换成另一个程序,保持自己的 PID。

2025 源码从 sys_fork() 进入 kernel/proc.c:kfork()。它调用 allocproc() 创建子进程,调用 uvmcopy() 建立用户内存,再复制 trapframe。子进程的 a0 被设置为 0;父进程则得到新 PID。两边都从 fork 调用之后继续,不是从 main 重新开始。

设 x=10,fork 后子进程执行 x=20,父进程仍应看到 10。基础实现通过复制物理页实现这种隔离;COW 实验先共享只读物理页,写入时再拆开。二者必须提供相同的用户语义。也就是说,物理实现可以变,接口承诺不变。

文件描述符则不是把文件内容复制一遍。p->ofile[fd] 指向 struct file;kfork() 用 filedup() 增加引用。父子继承的描述符可能指向同一个打开文件对象,因此共享它的偏移量。读入的字节属于文件,偏移属于打开文件对象,整数 fd 属于各自进程,这三个层次要分开。

exec 为什么不创建进程 ​

shell 的常见流程是先 fork,再让子进程 exec,父进程 wait。exec 装载 ELF 段,建立新的页表与栈,把参数字符串及指针数组放进新栈,准备新的入口地址。成功后从新入口开始运行,旧用户调用栈已经消失,所以成功的 exec 不会正常返回到旧代码。失败时必须保留可运行的旧进程并返回错误。

这可以用“先准备、后提交”理解:新地址空间尚未构造完整时,不急着破坏旧地址空间;所有关键步骤完成后才替换 p->pagetable。在文件系统或数据库中也会反复遇到相同的设计原则。

flowchart TD
  A[shell] --> B[fork]
  B --> C[父进程 wait]
  B --> D[子进程调整文件描述符]
  D --> E[exec 新程序]
  E --> F[程序 exit]
  F --> G[成为 ZOMBIE]
  G --> H[父进程 wait 回收]
  H --> A
查看流程图文本
flowchart TD
  A[shell] --> B[fork]
  B --> C[父进程 wait]
  B --> D[子进程调整文件描述符]
  D --> E[exec 新程序]
  E --> F[程序 exit]
  F --> G[成为 ZOMBIE]
  G --> H[父进程 wait 回收]
  H --> A

为什么需要 ZOMBIE ​

2025 的 kexit() 关闭文件、释放当前目录引用,记录退出状态,把状态改为 ZOMBIE,随后进入调度器。它不能在仍使用自己的内核栈时把一切都释放。kwait() 找到僵尸子进程,把退出状态复制给父进程,再通过 freeproc() 清理相应进程资源。

不要把 ZOMBIE 理解为“还在偷偷运行”。它保存的是让父进程获知退出结果所需的状态。没有 wait 的长期后果是进程表项积累,最终 fork 失败。父进程先退出时,孤儿会被重新交给 init,避免退出状态永远无人处理。

状态切换需同时维护锁和状态。调度器只选择 RUNNABLE 进程,将它设为 RUNNING;等待事件的进程会进入 SLEEPING;唤醒只是恢复 RUNNABLE,不等于马上占用 CPU。CPU 调度顺序不是程序可依赖的同步工具。

管道与引用计数的直观例子 ​

管道提供一段内核缓冲区,分别由读端和写端描述符引用。read 在缓冲区空而写端仍打开时等待;所有写端都关闭且数据耗尽后才返回 EOF。如果父进程只是“不再打算写”,却忘记关闭自己持有的写端,读进程会一直等待一个永远不会来的字节。这不是调度器坏了,而是资源生命周期尚未结束。

实现 find -exec 时也要管理生命周期:子进程 exec 失败必须退出;父进程 wait;目录 fd 在递归分支结束后关闭;参数数组末尾放空指针。每个资源都问“谁创建、谁共享、谁释放”,往往比反复添加 sleep 更快定位问题。

阅读任务与自测 ​

依次阅读 proc.h:struct proc、proc.c:allocproc/kfork/kexit/kwait、exec.c:kexec、file.c:filedup/fileclose。记录每个字段的创建点、复制点和释放点。新加 sandbox、alarm 或 VMA 字段时,也必须回到这张生命周期表检查,而不能只改正常路径。

自测:fork 后父子各执行一次 printf,为什么顺序不确定?

fork 建立两个可调度的执行流,没有承诺谁先运行。若业务要求顺序,需要 wait、管道或其他同步协议;加一段空循环只能改变概率,不能建立顺序保证。

自测:为什么 fork 后共享文件偏移不会破坏内存隔离?

文件对象是内核显式共享的资源;用户地址空间隔离是另一条接口承诺。隔离并不等于禁止共享,而是让共享通过受控的对象与规则发生。

实践入口:util、COW。来源:2025 教材及官方 util 提交中的 proc.c/exec.c/file.c。