4. fork 系统调用入门:函数原型、返回值特性与父子进程创建

fork 是 Linux 进程管理的核心系统调用,也是创建新进程的唯一原生接口。理解 fork 的返回值语义和父子进程关系,是掌握多进程编程与 IPC 通信的基础。本节从函数原型出发,逐步拆解 fork 的执行机制与返回值特性。

4.1 fork 函数原型与头文件

fork 定义在 <unistd.h> 头文件中,其原型极为简洁:

#include <unistd.h>

pid_t fork(void);
项目 说明
头文件 <unistd.h>
返回值类型 pid_t —— 在 Linux 中通常为 int,用于表示进程 ID
参数 无参数。所有进程上下文由内核自动复制
调用一次,返回两次 这是 fork 最独特的语义:在父进程和子进程中各返回一次
关键认知: fork 不是“创建一个新进程”,而是“复制当前进程”。子进程是父进程的完整副本,包括代码段、数据段、堆、栈以及打开的文件描述符。

4.2 返回值特性:调用一次,返回两次

fork 的返回值是区分父子进程的唯一依据。其返回值规则如下:

  • 在父进程中:返回子进程的 PID(正整数)。父进程通过此值识别子进程,并可用于后续 waitpid 等系统调用。
  • 在子进程中:返回 0。子进程通过返回 0 得知自己是新创建的进程。
  • 出错时:返回 -1,并设置 errno。常见错误包括进程数达到系统上限(EAGAIN)或内存不足(ENOMEM)。
返回值 所属进程 含义
> 0 父进程 子进程的 PID
0 子进程 标识自身为子进程
-1 父进程(调用者) 创建失败,errno 指示错误原因
常见误区: 初学者常误以为 fork 返回 0 表示失败。实际上,返回 0 是子进程的正常标识,返回 -1 才是失败。务必通过返回值判断当前代码运行在哪个进程中。

4.3 父子进程创建流程

当程序调用 fork 时,内核执行以下步骤:

  1. 分配 PCB:内核为新进程分配一个 task_struct 结构体,并分配唯一的 PID。
  2. 复制进程映像:子进程获得父进程数据段、堆、栈的完整副本(写时复制优化,COW)。
  3. 复制文件描述符表:子进程继承父进程所有打开的文件描述符,共享文件偏移量。
  4. 设置返回值:内核将子进程的 PID 写入父进程的返回值寄存器,将 0 写入子进程的返回值寄存器。
  5. 调度执行:父子进程均进入就绪队列,由调度器决定谁先运行。
fork 系统调用执行流程 父进程调用 fork() 内核:分配 task_struct 复制进程映像(COW) 父进程:返回子 PID 子进程:返回 0 父进程继续执行 子进程开始执行

4.4 父子进程关系与特性

创建完成后,父子进程形成以下关系:

  • 独立性:父子进程拥有独立的虚拟地址空间,互不干扰。子进程对全局变量的修改不会影响父进程。
  • 继承性:子进程继承父进程的文件描述符、信号处理设置、环境变量、工作目录等。
  • 调度平等:父子进程由内核调度器平等调度,执行顺序不确定(取决于调度策略和系统负载)。
  • 父子关系:子进程的 ppid(父进程 ID)被设置为父进程的 PID。父进程终止后,子进程成为孤儿进程,由 init(PID 1)收养。
属性 父进程 子进程
PID 保持不变 新分配的 PID
PPID 父进程的父进程 父进程的 PID
地址空间 原始数据 完整副本(COW)
文件描述符 保持不变 继承父进程的副本
执行顺序 不确定 不确定

4.5 典型代码模板

以下是最基本的 fork 使用模式:

#include <stdio.h>
#include <unistd.h>

int main() {
    pid_t pid = fork();

    if (pid < 0) {
        // fork 失败
        perror("fork failed");
        return 1;
    } else if (pid == 0) {
        // 子进程代码
        printf("子进程: PID = %d, PPID = %d\n", getpid(), getppid());
    } else {
        // 父进程代码
        printf("父进程: PID = %d, 子进程 PID = %d\n", getpid(), pid);
    }

    // 父子进程都会执行到这里
    printf("共同执行: PID = %d\n", getpid());
    return 0;
}
注意: 上述代码中,printf 在父子进程中各执行一次,因此“共同执行”行会被打印两次。这是 fork 后代码共享的直接体现——子进程从 fork 返回处继续执行,而非从 main 开头重新执行。

4.6 写时复制(COW)优化

现代 Linux 内核并不在 fork 时立即复制所有物理内存,而是采用 写时复制(Copy-on-Write) 技术:

  • 父子进程共享同一物理内存页,并将页表标记为只读。
  • 当任一进程尝试写入共享页时,触发缺页异常,内核才为该进程复制一份物理页。
  • 这种优化大幅降低了 fork 的开销,尤其适用于 fork 后立即 exec 的场景。
写时复制(COW)内存共享示意 父进程页表 子进程页表 共享物理内存页(标记为只读) fork 时未复制,仅增加引用计数 写入操作 → 缺页异常 → 复制物理页
实践建议: 在编写 fork 程序时,始终通过返回值判断当前进程身份,不要在 fork 之前打开文件描述符而不考虑子进程的继承行为。COW 机制使得 fork 非常高效,但大量写入操作仍会带来复制开销。