fork系统调用及示例
我们继续按照您的列表顺序,介绍下一个函数。在 clone
之后,根据您提供的列表,下一个函数是 fork
。
相关阅读:fork系统调用及示例-CSDN博客 vfork系统调用及示例 ptrace系统调用及示例
1. 函数介绍
fork
是 Linux 和所有 Unix-like 系统中最基本、最重要的进程创建系统调用之一。它的功能非常直接:创建一个调用进程(父进程)
你可以把 fork
想象成一个生物细胞的有丝分裂过程:
- 你有一个原始细胞(父进程)。
fork
调用就像触发了一次分裂。- 分裂完成后,你得到了两个细胞(两个进程):一个原始的(父进程)和一个全新的、几乎完全一样的(子进程)。
- 两个细胞(进程)从分裂完成的那一刻起,开始独立地执行后续代码。
fork
是多进程编程的基石。几乎所有需要创建新进程的 Unix/Linux 程序(服务器、shell、构建工具等)都会用到它。
2. 函数原型
#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
最基本的用法,以及如何区分父进程和子进程。
// 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
在一个循环中创建多个子进程。
// 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 系统中启动新程序的标准方法。
// 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(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
vsexit
: 在fork
之后的子进程中,通常推荐使用_exit()
而不是exit()
来终止。因为exit()
会执行一些清理工作(如刷新 stdio 缓冲区),这些操作在多进程环境下可能导致意外行为(例如,缓冲区被刷新两次)。_exit()
直接终止进程。fork
+exec
模式: 这是 Unix/Linux 系统中创建并运行新程序的标准方法。先fork
创建子进程,然后在子进程中调用exec
系列函数加载新程序。
总结:
fork
是 Unix/Linux 系统编程的基础。它通过创建当前进程的副本来实现多进程。理解其“一次调用,两次返回”的特性以及如何正确地区分和管理父、子进程,是编写健壮的多进程应用程序的关键。它通常与 wait
/waitpid
和 exec
系列函数结合使用。