Introduction to vfork()

Introduction to vfork()
Topic 5 → Subtopic 1  |  What vfork() is and why it was created
Topic 5
vfork()
Subtopic 1
of 3
3
Code Examples

What is vfork()?

vfork() is a variant of fork() designed for the specific case where the child process will immediately call exec() or _exit(). It was created in the days before copy-on-write existed, when fork() literally copied all of the parent’s memory — which was extremely slow. Today, with CoW, vfork() is largely obsolete but still appears in legacy code and embedded systems.

Keywords:

vfork() suspend parent shared address space no CoW exec() immediately _exit() only POSIX no-MMU systems

🕐 Historical Context: Why vfork() Was Invented
Era fork() behavior Problem Solution
Pre-CoW (1970s) Copied ALL parent pages immediately Expensive if immediately followed by exec() vfork() created to avoid copy entirely
With CoW (1980s+) Defers copy until write (fast) vfork() still slightly faster (no page table setup) vfork() still used in some embedded code
Modern Linux fork() is very fast with CoW vfork() dangerous, rarely useful POSIX marks vfork() obsolescent

🔨 vfork() Signature and Semantics
#include <unistd.h>
pid_t vfork(void);
/* Same return values as fork():
   Parent: returns child's PID (after child calls exec or _exit)
   Child : returns 0
   Error : -1                                                    */

What vfork() guarantees (3 unique behaviors):

1
No copy of address space
Child runs in the SAME virtual address space as the parent. No page tables copied, no CoW setup. Zero memory overhead at creation time.
2
Parent is suspended
The parent process is suspended (blocked) until the child calls exec() or _exit(). They cannot run simultaneously — there is only one address space.
3
Child MUST call exec() or _exit()
Any other action (modifying variables, calling most functions, returning from the function that called vfork) causes undefined behavior.

💻 Code Example 1: Basic vfork() Usage
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

int main(void)
{
    printf("[Parent PID=%d] Before vfork\n", getpid());

    pid_t pid = vfork();
    if (pid == -1) { perror("vfork"); exit(1); }

    if (pid == 0) {
        /* CHILD: runs in parent's address space.
           Parent is suspended until we exec or _exit. */
        printf("[Child PID=%d] In child, about to exec\n",
               getpid());

        /* MUST call exec() or _exit() — nothing else! */
        char *argv[] = { "echo", "Hello from exec'd process", NULL };
        execvp("echo", argv);

        /* Only reached if execvp fails */
        perror("execvp");
        _exit(EXIT_FAILURE);  /* MUST use _exit(), NOT exit() */
    }

    /* Parent resumes here AFTER child calls exec or _exit */
    printf("[Parent] Child done (PID=%d). Parent resumed.\n",
           (int)pid);
    wait(NULL);
    return 0;
}
Critical rules in vfork() child:
1. Call execvp() (or any exec variant) — or
2. Call _exit() (NOT exit(), NOT return)
Any other action = undefined behavior, likely crashes both processes.

💻 Code Example 2: vfork() with _exit()
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

int main(void)
{
    int shared_value = 100;  /* on parent's stack */

    pid_t pid = vfork();
    if (pid == -1) { perror("vfork"); exit(1); }

    if (pid == 0) {
        /* WARNING: modifying shared_value here modifies
           the PARENT's variable (same address space)!   */
        /* This is dangerous — shown only for education  */
        shared_value = 999;  /* modifies parent's stack! */

        printf("[Child] shared_value = %d (PARENT's var!)\n",
               shared_value);

        /* _exit() resumes the parent WITHOUT calling
           atexit handlers or flushing stdio */
        _exit(42);
    }

    /* Parent resumes. shared_value was modified by child! */
    int status;
    wait(&status);
    printf("[Parent] shared_value = %d (was modified by child!)\n",
           shared_value);
    printf("[Parent] Child exit status = %d\n",
           WEXITSTATUS(status));
    return 0;
}
⚠ Warning: This example shows that in vfork(), the child modifies the parent’s stack variable. This is precisely why vfork() is dangerous. Never rely on this behavior — it demonstrates why vfork() children must immediately exec or _exit.

💻 Code Example 3: The ONLY Correct vfork() Pattern
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

/* The ONLY safe vfork() pattern:
   1. vfork()
   2. In child: exec() immediately
   3. On exec failure: _exit() only
   4. No local variable modification
   5. No function calls that use stack   */

int run_program(const char *path, char *const argv[])
{
    pid_t pid;
    int status;

    pid = vfork();
    if (pid == -1) return -1;

    if (pid == 0) {
        /* Child: exec immediately, nothing else */
        execv(path, argv);
        /* exec failed */
        _exit(127);  /* 127 = "command not found" convention */
    }

    /* Parent: wait for child */
    if (waitpid(pid, &status, 0) == -1) return -1;
    return WIFEXITED(status) ? WEXITSTATUS(status) : -1;
}

int main(void)
{
    char *argv[] = { "date", NULL };
    int ret = run_program("/bin/date", argv);
    printf("date exited with: %d\n", ret);

    char *argv2[] = { "ls", "/tmp", NULL };
    ret = run_program("/bin/ls", argv2);
    printf("ls exited with: %d\n", ret);

    return 0;
}

🅾 Interview Questions
Q1: What is the difference between fork() and vfork()?

fork() creates a child with its own copy-on-write virtual address space; both processes run concurrently. vfork() creates a child that shares the parent’s address space directly; the parent is suspended until the child calls exec() or _exit(). vfork() has no CoW setup overhead but is extremely dangerous if misused.

Q2: Why must a vfork() child call _exit() instead of exit()?

exit() runs atexit() handlers and flushes stdio buffers. Since the child shares the parent’s address space, running atexit handlers would execute the parent’s cleanup code. Flushing stdio buffers would duplicate output. _exit() skips all cleanup and terminates immediately, leaving the parent’s state intact.

Q3: Why was vfork() invented and is it still useful today?

vfork() was invented before copy-on-write, when fork() copied all memory — making fork+exec expensive. Today, with CoW fork(), vfork() offers minimal advantage on modern Linux. It remains useful mainly in embedded systems without MMU (where fork() is impossible) and in performance-critical code measured to benefit from it. POSIX marks it obsolescent.

Series Navigation
Topic 5 → Subtopic 1 of 3

← Previous Next: vfork() vs fork() → Index

Leave a Reply

Your email address will not be published. Required fields are marked *