「Linux 系统编程」fork() 创建子进程

1 fork() 函数

在 Linux 系统编程中,进程创建是核心概念之一。一个进程(父进程)可以通过 fork() 函数 创建出一个与自己几乎完全相同的副本(子进程)。

1.1 函数原型

1
2
3
#include <unistd.h>

pid_t fork(void);

1.2 工作原理

调用 fork() 函数后,操作系统会 创建一个新的进程,这个新进程是调用者进程(父进程)的一个完整副本(子进程)。

  • 复制进程上下文:操作系统会为子进程 分配一个新的进程标识符(PID),然后复制父进程的文本段、数据段、堆栈段以及进程环境(如打开的文件描述符、信号处理程序等)到子进程中。

  • 读时共享、写时复制

    • fork() 刚成功时,或者 只涉及读的操作,父子进程的物理内存页是共享的(读时共享)。

    • 只有当任一进程试图修改这些内存页时,涉及写的操作,操作系统才会为该进程复制一份独立的副本(写时复制)。

  • 返回值分流

    fork() 函数会 返回两次

    • 父进程 中,fork() 函数返回 新创建子进程的 PID(一个大于 0 的正整数)。

    • 子进程 中,fork() 函数返回 0。表示创建子进程成功。

    • 如果创建失败(例如系统进程数达到上限),则返回 -1 给父进程。

可以通过判断返回值,决定后续是执行父进程的代码还是子进程的代码。


1.3 getpid() 与 getppid()

在处理进程时,经常需要获取进程的标识符(PID):

pid_t getpid(void):返回 当前进程的 PID
pid_t getppid(void):返回 当前进程的父进程的 PID(PPID)。


1.4 创建子进程示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

int main() {
pid_t pid; // 用于存储 fork() 的返回值

printf("Before fork, PID: [%d]\n", getpid());

pid = fork(); // 从这里开始,程序一分为二

// 判断当前是父进程还是子进程
if (pid == -1) {
// fork 失败
perror("Fork failed");
return 1;
} else if (pid == 0) {
// 子进程代码块
printf("This is the child process. My PID is [%d], my parent's PID is [%d].\n", getpid(), getppid());
} else {
// 父进程代码块 (pid > 0)
sleep(1); // 等待一段时间 保证子进程完成时,父进程处于执行状态,子进程不会成孤儿进程。
printf("This is the parent process. My PID is [%d], my child's PID is [%d].\n", getpid(), pid);
}

// 这里的代码父子进程都会执行
printf("This message is from PID: [%d]\n", getpid());

return 0;
}

进程调度是随机的,父进程和子进程异步执行,它们的输出顺序不确定,可能会交错出现。



2 循环创建多个子进程

2.1 错误示例

如果直接在循环中调用 fork() 函数,实际上会创建出 2n12^n - 1 个子进程:

1
2
3
4
// 错误示例:会产生 2^n 个子进程
for (int i = 0; i < n; i++) {
fork();
}

上面的代码会产生指数级增长的进程。每次 fork() 后,父进程 和 刚创建的子进程 都会继续执行下一次循环。

假设 n = 3,上面代码实际循环创建子进程的过程如下:


2.2 正确示例

正确的做法是在 fork() 后通过 判断返回值来终止子进程的循环

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

#define N 10

int main() {
pid_t pid;
int i;

printf("Parent PID [%d] starts creating %d children...\n", getpid(), N);

for (i = 0; i < N; i++) {
pid = fork();

if (pid < 0) {
perror("Fork failed");
return 1;
} else if (pid == 0) {
// 子进程:立刻跳出循环,避免子进程继续创建孙子进程
printf("I am child %d, my PID is [%d], my parent is [%d]\n", i, getpid(), getppid());
break; // 子进程跳出循环
}
// 父进程继续下一次循环,创建下一个子进程
}

// 区分不同的进程
if (pid == 0) {
// 所有子进程都会执行的代码
printf("Child process [%d] is done.\n", getpid());
} else {
// 只有父进程会执行的代码
sleep(5); // 父进程等待一段时间,避免子进程变为孤儿进程
printf("Parent process [%d] has collected all children.\n", getpid());
}

return 0;
}



3 父子进程相同和不同的内容

特性 父子进程相同点 父子进程不同点
代码段 fork() 瞬间完全相同 之后可以执行不同的代码路径(通过 if-else 判断)
数据段、堆、栈 fork() 瞬间拥有相同的内容 之后由于 写时复制,对变量的修改相互独立
环境变量 继承父进程的环境 可独立修改(如使用 setenv),不影响对方
文件描述符 共享 相同的打开文件表项。文件偏移指针会相互影响 拥有独立的文件描述符编号集合,但指向相同的内核文件对象
信号处理 继承父进程的信号处理设置(如 ignore, handler) 可以独立设置新的信号处理方式
进程属性 - PIDPPID、进程运行时间、未决信号集等不同
锁状态 - 子进程不会继承父进程通过 fcntl 创建的锁

共享的文件描述符:

如果父进程打开了一个文件,然后调用 fork(),父子进程可以对同一个文件进行读写。它们的操作会相互影响文件偏移指针。例如,如果父进程写入 “Hello”,子进程写入 “World”,那么文件内容将是 “HelloWorld” 而不是随机的覆盖。


「Linux 系统编程」fork() 创建子进程
https://marisamagic.github.io/2025/09/01/20250901/
作者
MarisaMagic
发布于
2025年9月1日
许可协议