fork系统调用及示例

fork系统调用及示例

data-ad-format="fluid" data-ad-layout-key="-7k+ex-4a-9w+4a">

我们继续按照您的列表顺序,介绍下一个函数。在 clone 之后,根据您提供的列表,下一个函数是 fork。

相关阅读:fork系统调用及示例-CSDN博客 vfork系统调用及示例 ptrace系统调用及示例

1. 函数介绍

fork 是 Linux 和所有 Unix-like 系统中最基本、最重要的进程创建系统调用之一。它的功能非常直接:创建一个调用进程(父进程)

你可以把 fork 想象成一个生物细胞的有丝分裂过程:

  • 你有一个原始细胞(父进程)。

  • fork 调用就像触发了一次分裂。

  • 分裂完成后,你得到了两个细胞(两个进程):一个原始的(父进程)和一个全新的、几乎完全一样的(子进程)。

  • 两个细胞(进程)从分裂完成的那一刻起,开始独立地执行后续代码。

fork 是多进程编程的基石。几乎所有需要创建新进程的 Unix/Linux 程序(服务器、shell、构建工具等)都会用到它。

2. 函数原型

1
2
3
4
#include <unistd.h> // 必需

pid_t fork(void);

3. 功能

  • 创建新进程: 请求操作系统内核创建一个新的进程(子进程)。

复制父进程: 内核会创建一个调用进程(父进程)的副本(子进程)。

子进程拥有父进程在调用 fork 时的几乎全部状态:

  • 代码段(.text)

  • 数据段(.data, .bss)

  • 堆(heap)

  • 栈(stack)

  • 打开的文件描述符及其状态(偏移量等)

  • 环境变量

  • 当前工作目录

  • 用户 ID 和组 ID

  • 等等…

独立执行: 从 fork 返回后,父进程和子进程在操作系统调度下独立运行。

4. 参数

  • void: fork 函数不接受任何参数。

5. 返回值

fork 的返回值是其最独特和关键的特性,因为它在父进程和子进程中返回不同的值:

在父进程中:

  • 成功: 返回新创建子进程的进程 ID (PID)。这是一个正整数。

  • 失败: 返回 -1,并设置全局变量 errno 来指示错误原因(例如 EAGAIN 系统资源不足,ENOMEM 内存不足)。

在子进程中:

  • 成功: 返回 0。

  • (理论上不会发生): 如果子进程创建失败,则不会执行到返回这一步。

**失败时 **(父进程)

  • 返回 -1,并且没有子进程被创建。

关键理解点: 一个 fork 调用,一次调用,两次返回。

6. 相似函数,或关联函数

  • vfork: 类似于 fork,但在子进程调用 exec 或 _exit 之前会暂停父进程。现在通常不推荐使用,posix_spawn 或直接 fork + exec 更安全。

  • clone: Linux 特有的、更灵活和底层的进程/线程创建函数。fork 和 vfork 在底层都可以通过调用 clone 来实现。

  • _exit / exit: 子进程通常调用这两个函数之一来终止自己。

  • wait / waitpid: 父进程使用这些函数来等待子进程结束,并获取子进程的退出状态。

  • exec 系列函数 (execl, execv, execve 等): 通常在 fork 之后,由子进程调用,用一个新的程序镜像替换当前进程的镜像。

7. 示例代码

示例 1:基本的 fork 使用

这个例子演示了 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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
// fork_basic.c
#include <unistd.h> // fork, getpid, sleep
#include <sys/wait.h> // wait
#include <stdio.h> // printf, perror
#include <stdlib.h> // exit

int main() {
pid_t pid;

printf("Before fork: Process ID (PID) is %d\n", getpid());

// --- 关键: 调用 fork ---
pid = fork();

// --- fork 之后,代码被父进程和子进程同时执行 ---

if (pid == -1) {
// fork 失败,只在父进程中执行
perror("fork failed");
exit(EXIT_FAILURE);

} else if (pid == 0) {
// --- 子进程执行的代码 ---
printf("This is the CHILD process.\n");
printf(" Child's PID is %d\n", getpid());
printf(" Child's parent PID (PPID) is %d\n", getppid());
printf(" fork() returned %d in the child.\n", pid); // pid is 0 here

// 子进程可以执行自己的任务
printf(" Child process is sleeping for 2 seconds...\n");
sleep(2);
printf(" Child process woke up and is exiting.\n");

// 子进程结束
exit(EXIT_SUCCESS);

} else {
// --- 父进程执行的代码 ---
printf("This is the PARENT process.\n");
printf(" Parent's PID is %d\n", getpid());
printf(" Parent's child PID is %d (returned by fork)\n", pid);
printf(" fork() returned %d in the parent.\n", pid);

// 父进程可以执行自己的任务
printf(" Parent process is waiting for child to finish...\n");
int status;
// wait() 会挂起父进程,直到任意一个子进程结束
wait(&status); // status 用于获取子进程的退出信息

if (WIFEXITED(status)) {
int exit_status = WEXITSTATUS(status);
printf(" Parent: Child exited normally with status %d.\n", exit_status);
} else {
printf(" Parent: Child did not exit normally.\n");
}

printf(" Parent process is exiting.\n");
}

return 0;
}

代码解释:

程序开始执行,打印父进程的 PID。

关键步骤: 调用 pid = fork();。

这个调用之后,操作系统创建了一个子进程,它是父进程的一个副本。

在父进程和子进程中,代码都从 fork() 之后的下一行开始继续执行。

程序立即使用 if 语句检查 pid 的值来区分父进程和子进程:

  • if (pid == -1): fork 调用失败。只有父进程会进入这个分支。

  • else if (pid == 0): 这段代码只在子进程中执行。fork 在子进程中返回 0。

  • else (即 pid > 0): 这段代码只在父进程中执行。fork 在父进程中返回新创建子进程的 PID。

子进程打印自己的 PID (getpid()) 和父进程的 PID (getppid()),然后睡眠 2 秒并退出。

父进程调用 wait(&status) 等待子进程结束。wait 会挂起父进程,直到子进程调用 exit 或 _exit。

子进程退出后,wait 返回。父进程检查子进程的退出状态 (status) 并打印相关信息,然后退出。

示例 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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
// fork_multiple.c
#include <unistd.h>
#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>

#define NUM_CHILDREN 5

int main() {
pid_t pid;
int i;

printf("Parent process (PID: %d) is creating %d children.\n", getpid(), NUM_CHILDREN);

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

if (pid == -1) {
perror("fork failed");
// 可以选择继续创建其他子进程或退出
exit(EXIT_FAILURE);

} else if (pid == 0) {
// --- 子进程 ---
printf("Child %d (PID: %d) created. Doing work...\n", i, getpid());

// 模拟工作:睡眠不同的时间
sleep(i + 1);
printf("Child %d (PID: %d) finished work and is exiting with code %d.\n", i, getpid(), i);
exit(i); // 用 i 作为退出码

}
// --- 父进程 ---
// 父进程从 if-else 结构出来,继续循环
// printf("Parent (PID: %d) created child %d with PID %d\n", getpid(), i, pid);
// 注意:如果父进程在这里也打印,输出会和子进程的输出混合
}

// --- 所有子进程创建完毕,父进程等待它们 ---
printf("Parent (PID: %d) has created all children. Now waiting for them to finish...\n", getpid());

// 等待所有子进程结束
for (i = 0; i < NUM_CHILDREN; i++) {
int status;
pid_t waited_pid = wait(&status); // waitpid(-1, &status, 0) 等价

if (waited_pid == -1) {
perror("wait failed");
} else {
if (WIFEXITED(status)) {
int exit_code = WEXITSTATUS(status);
printf("Parent: Child with PID %d exited normally with code %d.\n", waited_pid, exit_code);
} else {
printf("Parent: Child with PID %d did not exit normally.\n", waited_pid);
}
}
}

printf("Parent (PID: %d) finished. All children have been reaped.\n", getpid());
return 0;
}

代码解释:

定义要创建的子进程数量 NUM_CHILDREN。

父进程进入一个循环 for (i = 0; i < NUM_CHILDREN; i++)。

在每次循环中调用 fork()。

在子进程中 (pid == 0):

  • 打印信息。

  • 根据 i 的值睡眠不同时间(模拟不同长度的工作)。

  • 调用 exit(i) 退出,并将循环变量 i 作为退出码。

在父进程中 (pid > 0):

  • 循环继续下一次迭代,创建下一个子进程。

当循环结束时,所有子进程都已启动。

父进程进入第二个循环 for (i = 0; i < NUM_CHILDREN; i++)。

在这个循环中,父进程调用 wait(&status) 五次。

每次 wait 返回,就表示有一个子进程结束了。父进程获取其 PID 和退出状态并打印。

所有子进程都被 wait 回收后,父进程结束。

示例 3:fork + exec 创建新程序

这个例子演示了经典的 fork + exec 模式,这是 Unix/Linux 系统中启动新程序的标准方法。

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
// fork_exec.c
#include <unistd.h>
#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main() {
pid_t pid;

printf("Parent process (PID: %d) is about to fork.\n", getpid());

pid = fork();

if (pid == -1) {
perror("fork failed");
exit(EXIT_FAILURE);

} else if (pid == 0) {
// --- 子进程 ---
printf("Child process (PID: %d) created.\n", getpid());

// 准备 execvp 所需的参数
// execvp(const char *file, char *const argv&#91;]);
char *args&#91;] = { "ls", "-l", "/tmp", NULL }; // argv&#91;] 必须以 NULL 结尾

printf("Child (PID: %d) is about to execute 'ls -l /tmp'.\n", getpid());

// 调用 execvp 执行新的程序
// 如果 execvp 成功,下面的代码将不会被执行
// 因为当前进程的镜像已被替换
execvp(args&#91;0], args);

// --- 如果代码执行到这里,说明 execvp 失败了 ---
perror("execvp failed"); // 打印错误信息
_exit(EXIT_FAILURE); // 子进程失败退出,使用 _exit

} else {
// --- 父进程 ---
printf("Parent process (PID: %d) created child (PID: %d).\n", getpid(), pid);

// 父进程等待子进程结束
int status;
if (waitpid(pid, &status, 0) == -1) { // 等待特定的子进程
perror("waitpid failed");
exit(EXIT_FAILURE);
}

if (WIFEXITED(status)) {
int exit_status = WEXITSTATUS(status);
printf("Parent: Child (PID: %d) exited with status %d.\n", pid, exit_status);
} else if (WIFSIGNALED(status)) {
int sig = WTERMSIG(status);
printf("Parent: Child (PID: %d) was killed by signal %d.\n", pid, sig);
} else {
printf("Parent: Child (PID: %d) did not exit normally.\n", pid);
}

printf("Parent process (PID: %d) finished.\n", getpid());
}

return 0;
}

代码解释:

父进程调用 fork() 创建子进程。

在子进程中 (pid == 0):

  • 准备要执行的命令及其参数。这里准备执行 ls -l /tmp。

关键步骤: 调用 execvp(“ls”, args)。

  • execvp 会用 ls 程序的镜像替换当前子进程的内存镜像。

  • 如果 execvp 成功,它永远不会返回。子进程的代码从 ls 程序的入口点开始执行。

  • 如果 execvp 失败(例如,找不到 ls 命令),它会返回 -1。

如果 execvp 返回了(即失败了),打印错误信息并调用 _exit(EXIT_FAILURE) 退出子进程。

在父进程中 (pid > 0):

  • 打印信息。

  • 调用 waitpid(pid, &status, 0) 等待特定的子进程(由 pid 指定)结束。

  • 子进程执行 ls 命令并退出。

waitpid 返回,父进程检查子进程的退出状态。

  • WIFEXITED: 检查子进程是否正常退出(通过 exit 或 return)。

  • WIFSIGNALED: 检查子进程是否被信号终止。

打印子进程的退出信息。

父进程退出。

重要提示与注意事项:

一次调用,两次返回: 理解 fork 在父进程和子进程中返回不同值是掌握其用法的关键。

区分父子进程: 必须使用 if (pid == 0) {…} else if (pid > 0) {…} else {…} 模式来区分父进程和子进程,并执行不同的代码逻辑。

错误处理: 始终检查 fork 的返回值是否为 -1,以处理可能的失败情况(如资源不足)。

僵尸进程: 当子进程结束而父进程尚未调用 wait 或 waitpid 来获取其状态时,子进程会变成僵尸进程(Zombie Process)。这会浪费一个进程表项。因此,父进程必须等待子进程。

孤儿进程: 如果父进程在子进程之前结束,子进程会变成孤儿进程(Orphan Process),并被 init 进程(PID 1)收养。

文件描述符: fork 之后,父进程和子进程共享所有的文件描述符。它们指向同一个内核的“打开文件描述”(open file description),因此共享文件偏移量等。如果需要独立的文件描述符,子进程需要在 fork 后显式地 close 和 open。

_exit vs exit: 在 fork 之后的子进程中,通常推荐使用 _exit() 而不是 exit() 来终止。因为 exit() 会执行一些清理工作(如刷新 stdio 缓冲区),这些操作在多进程环境下可能导致意外行为(例如,缓冲区被刷新两次)。_exit() 直接终止进程。

fork + exec 模式: 这是 Unix/Linux 系统中创建并运行新程序的标准方法。先 fork 创建子进程,然后在子进程中调用 exec 系列函数加载新程序。

总结:

fork 是 Unix/Linux 系统编程的基础。它通过创建当前进程的副本来实现多进程。理解其“一次调用,两次返回”的特性以及如何正确地区分和管理父、子进程,是编写健壮的多进程应用程序的关键。它通常与 wait/waitpid 和 exec 系列函数结合使用。

https://www.calcguide.tech/2025/08/26/linux开源软件路线图/

data-ad-format="auto" data-full-width-responsive="true">