vfork系统调用及示例

在 fork 之后,根据您提供的列表,下一个函数是 vfork


1. 函数介绍

vfork 是一个历史悠久的 Linux/Unix 系统调用,它的设计目的是为了优化 fork 在特定场景下的性能。vfork 的行为与 fork 非常相似,但也存在关键的区别

核心思想:

当一个进程调用 fork 后,最常见的操作是在子进程中立即调用 exec 系列函数来执行一个全新的程序。在标准的 fork 实现中,内核会完整地复制父进程的地址空间(页表、内存页等)给子进程。但是,如果子进程紧接着就调用 exec,这些刚复制的内存很快就会被新程序的内存镜像完全覆盖,那么这次复制操作就是浪费的。

vfork 就是为了解决这个“先 fork 再 exec”的常见模式下的性能浪费问题。

你可以把 vfork 想象成借用自己的身体来打电话

  • 你(父进程)需要让别人(子进程)去隔壁房间打一个重要的电话(exec)。
  • 用 fork 就像你复制了一个自己的身体(克隆人),然后让克隆人去隔壁打电话。但克隆人刚出门,你就把他的身体销毁了,因为用完就没了。
  • 用 vfork 就像你暂时把自己的身体借给那个人,让他去隔壁打电话。在打电话的这段时间(从 vfork 返回到 exec 或 _exit 被调用),你(父进程)必须一动不动地等着,因为你把身体借出去了。
  • 一旦那个人打完电话(调用 exec 或 _exit),你的身体就回来了,你可以继续做自己的事。

2. 函数原型

#include <unistd.h> // 必需

pid_t vfork(void);

3. 功能

  • 创建新进程: 与 fork 类似,vfork 也用于创建一个新的子进程。
  • 共享地址空间: 与 fork 不同,vfork 创建的子进程暂时与父进程共享相同的地址空间(内存、栈等)。这意味着子进程对内存的任何修改在父进程中都是可见的。
  • 挂起父进程: 调用 vfork 后,父进程会被挂起(暂停执行),直到子进程调用 exec 系列函数或 _exit 为止。
  • 子进程限制: 在子进程中,从 vfork 返回到调用 exec 或 _exit 之间,只能执行这两个操作或修改局部变量后直接返回。执行任何其他操作(如修改全局变量、调用可能修改内存的库函数、返回到 vfork 调用之前的函数栈帧)都可能导致未定义行为

4. 参数

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

5. 返回值

vfork 的返回值语义与 fork 完全相同

  • 在父进程中:
    • 成功: 返回新创建**子进程的进程 ID **(PID)。
    • 失败: 返回 -1,并设置 errno
  • 在子进程中:
    • 成功: 返回 0。
  • **失败时 **(父进程)
    • 返回 -1,且没有子进程被创建。

6. 相似函数,或关联函数

  • forkvfork 的“兄弟”。vfork 是 fork 的一个变种,旨在优化“fork-then-exec”模式。
  • clone: Linux 特有的底层系统调用。vfork 在底层可以通过特定的 clone 标志 (CLONE_VFORK | CLONE_VM) 来实现。
  • exec 系列函数vfork 通常与 exec 系列函数结合使用。
  • _exit: 子进程在 vfork 后通常调用 _exit 来终止,而不是 exit

7. 示例代码

示例 1:基本的 vfork + exec 使用

这个例子演示了 vfork 最经典和推荐的用法:创建子进程并立即执行新程序。

// vfork_exec.c
#include <unistd.h>   // vfork, _exit
#include <sys/wait.h> // wait
#include <stdio.h>    // printf, perror
#include <stdlib.h>   // exit

int global_var = 100; // 全局变量,用于演示共享地址空间

int main() {
    pid_t pid;
    int local_var = 200; // 局部变量

    printf("Before vfork: Parent PID: %d\n", getpid());
    printf("  Global var: %d, Local var: %d\n", global_var, local_var);

    // --- 关键: 调用 vfork ---
    pid = vfork();

    if (pid == -1) {
        // vfork 失败
        perror("vfork failed");
        exit(EXIT_FAILURE);

    } else if (pid == 0) {
        // --- 子进程 ---
        printf("Child process (PID: %d) created by vfork.\n", getpid());

        // 在 vfork 的子进程中,修改局部变量通常是安全的
        // (只要不返回到 vfork 之前的栈帧)
        local_var = 250;
        printf("  Child modified local var to: %d\n", local_var);

        // 修改全局变量也是可以的,但这会影响父进程看到的值
        // 这只是为了演示共享内存,实际使用中要非常小心
        global_var = 150;
        printf("  Child modified global var to: %d\n", global_var);

        // --- 关键: 子进程必须立即调用 exec 或 _exit ---
        printf("  Child is about to exec 'echo'.\n");

        // 准备 execv 所需的参数
        char *args[] = { "echo", "Hello from exec'd process!", NULL };

        // 调用 execv 执行新的程序
        execv("/bin/echo", args);

        // --- 如果代码执行到这里,说明 execv 失败了 ---
        perror("execv failed in child");
        // 在 vfork 的子进程中,失败时必须使用 _exit,而不是 exit
        _exit(EXIT_FAILURE);

    } else {
        // --- 父进程 ---
        // vfork 会挂起父进程,直到子进程调用 exec 或 _exit
        printf("Parent process (PID: %d) resumed after child's exec.\n", getpid());

        // 父进程现在可以安全地访问自己的变量了
        // 注意:由于子进程修改了 global_var,在 exec 之前,
        // 父进程看到的 global_var 值可能已经被改变了
        // (但这依赖于具体实现和时机,不应依赖此行为)
        printf("  Parent sees global var as: %d (may be modified by child)\n", global_var);
        printf("  Parent sees local var as: %d (should be unchanged)\n", local_var);

        // 等待子进程结束 (子进程 exec 后变成了新的程序,最终会退出)
        int status;
        if (wait(&status) == -1) {
            perror("wait failed");
            exit(EXIT_FAILURE);
        }

        if (WIFEXITED(status)) {
            int exit_code = WEXITSTATUS(status);
            printf("Parent: Child (new process) exited with status %d.\n", exit_code);
        } else {
            printf("Parent: Child (new process) did not exit normally.\n");
        }

        printf("Parent process (PID: %d) finished.\n", getpid());
    }

    return 0;
}

代码解释:

  1. 定义了一个全局变量 global_var 和一个局部变量 local_var
  2. 调用 pid = vfork();
  3. 在子进程中 (pid == 0):
    • 打印信息。
    • 修改局部变量 local_var(这通常被认为是安全的)。
    • 修改全局变量 global_var(这是为了演示地址空间共享,但实际编程中非常危险且不推荐)。
    • 关键: 准备 execv 的参数并立即调用 execv("/bin/echo", args)
    • 如果 execv 失败,调用 _exit(EXIT_FAILURE) 退出。强调: 在 vfork 子进程中,失败时必须使用 _exit,而不是 exit
  4. 在父进程中 (pid > 0):
    • 程序执行到这里时,意味着子进程已经调用了 exec 或 _exit,父进程被恢复执行
    • 打印信息,并检查变量的值。
      • local_var 应该没有变化。
      • global_var 的值是不确定的,因为子进程可能修改了它。这展示了共享地址空间的风险。
    • 调用 wait 等待子进程(现在是 echo 程序)结束。
    • 打印子进程的退出状态。
    • 父进程结束。

示例 2:演示 vfork 子进程中的危险操作

这个例子(仅供演示,请勿模仿!)展示了在 vfork 子进程中执行不当操作可能导致的问题。

// vfork_danger.c
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int global_counter = 0;

// 一个简单的函数
void dangerous_function() {
    global_counter++; // 修改全局变量
    printf("Dangerous function called, global_counter: %d\n", global_counter);
    // 如果这个函数还调用了其他库函数,或者有复杂的返回路径,
    // 在 vfork 子进程中调用它会非常危险。
}

int main() {
    pid_t pid;

    printf("Parent PID: %d, Global counter: %d\n", getpid(), global_counter);

    pid = vfork();

    if (pid == -1) {
        perror("vfork failed");
        exit(EXIT_FAILURE);
    } else if (pid == 0) {
        // --- vfork 子进程 ---
        printf("Child PID: %d\n", getpid());

        // --- 危险操作 1: 调用非 async-signal-safe 的函数 ---
        // printf 通常被认为是安全的,但更复杂的函数可能不是。
        // dangerous_function(); // 取消注释这行可能会导致问题

        // --- 危险操作 2: 从 vfork 子进程中返回 ---
        // return 0; // 这是非常危险的!绝对不要这样做!
        // 从 vfork 子进程中返回会导致返回到父进程的栈帧,
        // 而父进程的栈可能已经被子进程修改或破坏。

        // --- 危险操作 3: 修改复杂的数据结构 ---
        // 任何涉及 malloc/free, stdio buffers, 等的操作都可能不安全。

        // 正确的做法:只调用 exec 或 _exit
        // 为了演示,我们在这里直接 _exit
        printf("Child is exiting via _exit().\n");
        _exit(EXIT_SUCCESS);

    } else {
        // --- 父进程 ---
        // 父进程在这里恢复
        printf("Parent PID: %d resumed. Global counter: %d\n", getpid(), global_counter);

        // 简单等待,实际程序中应使用 wait
        sleep(1);
        printf("Parent finished.\n");
    }

    return 0;
}

代码解释:

  1. 该示例旨在说明在 vfork 子进程中的限制。
  2. dangerous_function 是一个示例函数,它修改全局变量并调用 printf
  3. 代码注释中指出了几种在 vfork 子进程中不应该做的事情:
    • 调用复杂的库函数。
    • 从子进程函数中返回(return)。
    • 修改复杂的数据结构。
  4. 强调了在 vfork 子进程中只应执行 exec 或 _exit

重要提示与注意事项:

  1. 已过时/不推荐: 在现代 Linux 编程中,vfork通常被认为是过时的,并且不推荐使用。原因如下:
    • 复杂且易出错: 子进程的行为受到严格限制,很容易因违反规则而导致程序崩溃或数据损坏。
    • 优化不再显著: 现代操作系统的 fork 实现(利用写时复制 Copy-On-Write, COW 技术)已经非常高效。当 fork 后立即 exec 时,内核几乎不需要复制任何实际的物理内存页,因为 exec 会立即替换整个地址空间。因此,vfork 带来的性能提升非常有限。
    • 更安全的替代方案posix_spawn() 是一个更现代、更安全、更可移植的创建并执行新进程的方式,它旨在提供 fork + exec 的功能,同时避免 vfork 的陷阱。
  2. vfork vs fork + COW:
    • 传统的 fork 确实会复制页表。
    • 但是现代的 fork 实现使用写时复制(COW)。这意味着 fork 调用本身很快,因为它只复制页表,而物理内存页在父子进程之间是共享的。只有当任一进程尝试修改某页时,内核才会复制该页。如果子进程紧接着调用 exec,那么大部分(甚至全部)页面都无需复制。
    • 因此,vfork 的性能优势在现代系统上已经大大减弱。
  3. 严格的使用规则: 如果你必须使用 vfork(例如,为了兼容非常老的系统或特殊需求),必须严格遵守其规则:
    • 子进程只能调用 exec 或 _exit
    • 子进程不能修改除局部变量外的任何数据。
    • 子进程不能返回到 vfork 调用之前的任何函数栈帧。
    • 子进程不能调用任何非异步信号安全(async-signal-safe)的函数(除了 exec 和 _exit)。
  4. _exit vs exit: 与 fork 子进程一样,在 vfork 子进程中,如果需要终止,应使用 _exit() 而不是 exit()
  5. 可移植性vfork 不是 POSIX 标准的一部分,尽管在很多类 Unix 系统上都可用。fork 和 posix_spawn 具有更好的可移植性。

总结:

vfork 是一个为特定场景(fork 后立即 exec)优化的系统调用。它通过让父子进程共享地址空间并挂起父进程来避免内存复制的开销。然而,由于其使用规则极其严格且容易出错,加上现代 fork 实现(COW)已经非常高效,vfork 在现代编程实践中已基本被弃用。推荐使用 fork + exec 或更现代的 posix_spawn 来创建和执行新进程。理解 vfork 的原理和历史意义仍然重要,但应避免在新代码中使用它。

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

发表回复

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