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。 _exitvsexit: 在fork之后的子进程中,通常推荐使用_exit()而不是exit()来终止。因为exit()会执行一些清理工作(如刷新 stdio 缓冲区),这些操作在多进程环境下可能导致意外行为(例如,缓冲区被刷新两次)。_exit()直接终止进程。fork+exec模式: 这是 Unix/Linux 系统中创建并运行新程序的标准方法。先fork创建子进程,然后在子进程中调用exec系列函数加载新程序。
总结:
fork 是 Unix/Linux 系统编程的基础。它通过创建当前进程的副本来实现多进程。理解其“一次调用,两次返回”的特性以及如何正确地区分和管理父、子进程,是编写健壮的多进程应用程序的关键。它通常与 wait/waitpid 和 exec 系列函数结合使用。