首页/操作系统/进程管理/fork 系统调用 🔗 在 Obsidian 中打开
操作系统 · 进程管理 · 进程创建

fork 系统调用

重要度 ★★★★★ fork进程创建写时复制exec系统调用
速查
fork() 调用一次、返回两次:父进程返回子进程 PID(>0),子进程返回 0,失败返回 −1。现代系统用写时复制(COW)共享物理页,写入时才复制。

基本概念

fork() 是 Unix / Linux 系统中创建新进程的核心系统调用。调用一次,返回两次——这是 fork 最独特的特征。

调用语义

$pid\_t\; pid = fork();$

  • 父进程中:返回子进程的 PID(正整数)。
  • 子进程中:返回 0。
  • 出错时:返回 −1。

fork 执行流程

  1. 分配新 PID;
  2. 创建子进程 PCB;
  3. 复制父进程地址空间(COW);
  4. 复制文件描述符;
  5. 设置返回值(父返回 PID,子返回 0)。

写时复制(Copy-on-Write, COW)

现代操作系统不会在 fork 时立即复制整个地址空间,而是采用写时复制技术。

工作原理

  1. fork 时,父子进程共享物理内存页(只读映射);
  2. 任一进程尝试写入时,触发页错误;
  3. 内核为写入方分配新的物理页,复制内容;
  4. 更新页表映射,解除共享。
优势fork 后紧跟 exec 的场景(最常见)不需要复制;节省内存与时间;Linux 中 fork 后通常紧跟 exec,COW 避免了无效复制。

fork 后父子继承关系

属性是否继承说明
地址空间(副本)继承(COW)写时复制
文件描述符继承共享文件偏移量
信号处理函数继承信号掩码继承
当前工作目录继承
环境变量继承
PID / PPID不继承子进程获得新 PID,PPID = 父进程 PID
未处理信号 / 文件锁 / 定时器不继承子进程清空

僵尸进程与孤儿进程

  • 孤儿进程:父进程先于子进程退出,子进程被 init / systemd 收养。
  • 僵尸进程:子进程退出但父进程未调用 wait(),子进程 PCB 残留。
处理父进程调用 wait()/waitpid() 回收子进程,或注册 SIGCHLD 信号处理函数,可避免僵尸进程。

返回两次的底层原理

fork 通过系统调用进入内核,内核完成:

  1. 创建新进程的 PCB;
  2. 复制父进程地址空间(COW);
  3. 在父进程栈上设置返回值 = 子进程 PID;
  4. 在子进程栈上设置返回值 = 0;
  5. 将父子进程都设为就绪态,由调度器决定谁先运行。
典型题n 次 fork 产生 $2^n$ 个进程;若每次 fork 后都 printf("A"),则共打印 $2^n$ 次。

记忆卡片

fork 返回几次?各返回什么?
两次:父返回子 PID(>0),子返回 0,失败 −1。
什么是写时复制?
fork 时共享物理页(只读),写入时才复制,避免无效复制。
父子共享 / 不共享哪些?
共享:地址空间(COW)、文件描述符、环境变量;不共享:PID、未处理信号、文件锁。
如何避免僵尸进程?
父进程调用 wait()/waitpid(),或注册 SIGCHLD 处理。

交互动画 · fork 与写时复制

父进程返回 PID 子进程返回 0 父 物理页 子 物理页 共享页(只读)
点击「fork()」观察一个父进程如何分裂出子进程;再点「写入」看写时复制如何分配新页。
提示:fork 后父子共享物理页(只读),写入才复制。
示意图:fork 后父子各自有页表,但映射到同一物理页(只读共享);任一方写入触发 COW,内核为其复制出新页。

相关知识点

process-and-thread thread-model

↑ 以上为站内 HTML 相对链接(纯网页可浏览);本页右上「在 Obsidian 中打开」跳回源笔记。