fork系统调用及示例
fork系统调用及示例
我们继续按照您的列表顺序,介绍下一个函数。在 clone 之后,根据您提供的列表,下一个函数是 fork。
相关阅读:fork系统调用及示例-CSDN博客 vfork系统调用及示例 ptrace系统调用及示例
1. 函数介绍
fork 是 Linux 和所有 Unix-like 系统中最基本、最重要的进程创建系统调用之一。它的功能非常直接:创建一个调用进程(父进程)
你可以把 fork 想象成一个生物细胞的有丝分裂过程:
你有一个原始细胞(父进程)。
fork 调用就像触发了一次分裂。
分裂完成后,你得到了两个细胞(两个进程):一个原始的(父进程)和一个全新的、几乎完全一样的(子进程)。
两个细胞(进程)从分裂完成的那一刻起,开始独立地执行后续代码。
fork 是多进程编程的基石。几乎所有需要创建新进程的 Unix/Linux 程序(服务器、shell、构建工具等)都会用到它。
2. 函数原型
1 | |
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 | |
代码解释:
程序开始执行,打印父进程的 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 | |
代码解释:
定义要创建的子进程数量 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 | |
代码解释:
父进程调用 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开源软件路线图/