fork系统调用及示例

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. 参数

  • voidfork 函数不接受任何参数。

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 系列函数 (execlexecvexecve 等): 通常在 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;
}

代码解释:

  1. 程序开始执行,打印父进程的 PID。
  2. 关键步骤: 调用 pid = fork();
  3. 这个调用之后,操作系统创建了一个子进程,它是父进程的一个副本。
  4. 在父进程和子进程中,代码都从 fork() 之后的下一行开始继续执行
  5. 程序立即使用 if 语句检查 pid 的值来区分父进程和子进程:
    • if (pid == -1)fork 调用失败。只有父进程会进入这个分支。
    • else if (pid == 0): 这段代码只在子进程中执行fork 在子进程中返回 0。
    • else (即 pid > 0): 这段代码只在父进程中执行fork 在父进程中返回新创建子进程的 PID。
  6. 子进程打印自己的 PID (getpid()) 和父进程的 PID (getppid()),然后睡眠 2 秒并退出。
  7. 父进程调用 wait(&status) 等待子进程结束。wait 会挂起父进程,直到子进程调用 exit 或 _exit
  8. 子进程退出后,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;
}

代码解释:

  1. 定义要创建的子进程数量 NUM_CHILDREN
  2. 父进程进入一个循环 for (i = 0; i < NUM_CHILDREN; i++)
  3. 在每次循环中调用 fork()
  4. 在子进程中 (pid == 0):
    • 打印信息。
    • 根据 i 的值睡眠不同时间(模拟不同长度的工作)。
    • 调用 exit(i) 退出,并将循环变量 i 作为退出码。
  5. 在父进程中 (pid > 0):
    • 循环继续下一次迭代,创建下一个子进程。
  6. 当循环结束时,所有子进程都已启动。
  7. 父进程进入第二个循环 for (i = 0; i < NUM_CHILDREN; i++)
  8. 在这个循环中,父进程调用 wait(&status) 五次
  9. 每次 wait 返回,就表示有一个子进程结束了。父进程获取其 PID 和退出状态并打印。
  10. 所有子进程都被 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;
}

代码解释:

  1. 父进程调用 fork() 创建子进程。
  2. 在子进程中 (pid == 0):
    • 准备要执行的命令及其参数。这里准备执行 ls -l /tmp
    • 关键步骤: 调用 execvp("ls", args)
      • execvp 会用 ls 程序的镜像替换当前子进程的内存镜像。
      • 如果 execvp 成功,它永远不会返回。子进程的代码从 ls 程序的入口点开始执行。
      • 如果 execvp 失败(例如,找不到 ls 命令),它会返回 -1。
    • 如果 execvp 返回了(即失败了),打印错误信息并调用 _exit(EXIT_FAILURE) 退出子进程。
  3. 在父进程中 (pid > 0):
    • 打印信息。
    • 调用 waitpid(pid, &status, 0) 等待特定的子进程(由 pid 指定)结束。
    • 子进程执行 ls 命令并退出。
    • waitpid 返回,父进程检查子进程的退出状态。
      • WIFEXITED: 检查子进程是否正常退出(通过 exit 或 return)。
      • WIFSIGNALED: 检查子进程是否被信号终止。
    • 打印子进程的退出信息。
    • 父进程退出。

重要提示与注意事项:

  1. 一次调用,两次返回: 理解 fork 在父进程和子进程中返回不同值是掌握其用法的关键。
  2. 区分父子进程: 必须使用 if (pid == 0) {...} else if (pid > 0) {...} else {...} 模式来区分父进程和子进程,并执行不同的代码逻辑。
  3. 错误处理: 始终检查 fork 的返回值是否为 -1,以处理可能的失败情况(如资源不足)。
  4. 僵尸进程: 当子进程结束而父进程尚未调用 wait 或 waitpid 来获取其状态时,子进程会变成僵尸进程(Zombie Process)。这会浪费一个进程表项。因此,父进程必须等待子进程。
  5. 孤儿进程: 如果父进程在子进程之前结束,子进程会变成孤儿进程(Orphan Process),并被 init 进程(PID 1)收养。
  6. 文件描述符fork 之后,父进程和子进程共享所有的文件描述符。它们指向同一个内核的“打开文件描述”(open file description),因此共享文件偏移量等。如果需要独立的文件描述符,子进程需要在 fork 后显式地 close 和 open
  7. _exit vs exit: 在 fork 之后的子进程中,通常推荐使用 _exit() 而不是 exit() 来终止。因为 exit() 会执行一些清理工作(如刷新 stdio 缓冲区),这些操作在多进程环境下可能导致意外行为(例如,缓冲区被刷新两次)。_exit() 直接终止进程。
  8. fork + exec 模式: 这是 Unix/Linux 系统中创建并运行新程序的标准方法。先 fork 创建子进程,然后在子进程中调用 exec 系列函数加载新程序。

总结:

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

此条目发表在linux文章分类目录。将固定链接加入收藏夹。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注