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 时,内核执行以下步骤:
- 分配 PCB:内核为新进程分配一个
task_struct结构体,并分配唯一的 PID。 - 复制进程映像:子进程获得父进程数据段、堆、栈的完整副本(写时复制优化,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的场景。
实践建议: 在编写
fork 程序时,始终通过返回值判断当前进程身份,不要在 fork 之前打开文件描述符而不考虑子进程的继承行为。COW 机制使得 fork 非常高效,但大量写入操作仍会带来复制开销。