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 是多进程编程的基石。几乎所有需要创建新进程的 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. 参数
5. 返回值 fork 的返回值是其最独特和关键的特性,因为它在父进程和子进程中返回不同的值:
在父进程中:
在子进程中:
**失败时 **(父进程)
关键理解点: 一个 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):
在父进程中 (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[]); char *args[] = { "ls", "-l", "/tmp", NULL }; // argv[] 必须以 NULL 结尾 printf("Child (PID: %d) is about to execute 'ls -l /tmp'.\n", getpid()); // 调用 execvp 执行新的程序 // 如果 execvp 成功,下面的代码将不会被执行 // 因为当前进程的镜像已被替换 execvp(args[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 返回,父进程检查子进程的退出状态。
打印子进程的退出信息。
父进程退出。
重要提示与注意事项: 一次调用,两次返回: 理解 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开源软件路线图/