Linux syscalls - fork

At its core, fork() is the system call used in Unix/Linux to create a new process.

Instead of starting a fresh program from scratch, fork() creates an exact clone of the current process.

The Fundamental Rule: fork() Returns Twice

fork() is unique because you call it once, but it returns twice: once in the parent process and once in the newly created child process.

       Parent Process
             |
        fork() called
          /     \
         /       \
  Returns PID    Returns 0
       |             |
 Parent Branch  Child Branch

The return value tells your code which process it is running in:

Return Value from fork() Meaning
> 0 (Positive Integer) You are in the Parent process. The value is the Process ID (PID) of the new child.
0 You are in the Child process.
-1 fork() failed (e.g., system out of memory or process limit reached).

Basic C Example

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

int main() {
    pid_t pid = fork();

    if (pid < 0) {
        // Fork failed
        perror("fork failed");
        return 1;
    } else if (pid == 0) {
        // Child Process
        printf("Hello from Child! (PID: %d, Parent PID: %d)\n", getpid(), getppid());
    } else {
        // Parent Process
        printf("Hello from Parent! (Child PID: %d, My PID: %d)\n", pid, getpid());
    }

    return 0;
}

Does that mean the code below fork() will run twice?

Yes, with one major exception: the code will run twice unless one of the processes calls exec(), exit(), or terminates early.

How It Works

Because fork() clones the entire process—including the Program Counter (the instruction pointer that tracks where the CPU is currently executing)—execution resumes for both processes at the exact instruction immediately following fork().

#include <stdio.h>
#include <unistd.h>

int main() {
    printf("Line 1: Before fork\n"); // Runs ONCE (Parent only)

    fork();

    printf("Line 2: After fork\n");  // Runs TWICE (Parent & Child)
    return 0;                        // Runs TWICE
}

Output:

Line 1: Before fork
Line 2: After fork
Line 2: After fork

The Three Scenarios Where Code Doesn't Run Twice

1. Conditional Branching (if/else)

Since fork() returns 0 to the child and a positive PID to the parent, if/else blocks route the execution down separate paths:

pid_t pid = fork();

if (pid == 0) {
    // ONLY the child runs this
    printf("Child path\n");
} else {
    // ONLY the parent runs this
    printf("Parent path\n");
}

// Both processes reach this line!
printf("Shared path\n");

2. Calling execve() in the Child

If the child process calls an exec() variant (like execvp() or execl()), its memory, stack, and execution pointer are overwritten by a new program.

The code below exec() inside the child simply disappears:

if (fork() == 0) {
    execlp("ls", "ls", NULL);

    // This line ONLY runs if execlp FAILS!
    perror("exec failed");
    _exit(1);
}

// Only the PARENT ever reaches here (because the child became 'ls')
printf("Parent continues...\n");

3. Calling exit() or Terminating

If either process calls exit() or _exit(), or encounters a fatal crash/signal (like a segmentation fault), that specific process stops immediately, and the remaining lines won't execute for it.

A Common Pitfall: Unflushed I/O Buffers

Consider this subtle bug with C's standard library output buffering (printf):

#include <stdio.h>
#include <unistd.h>

int main() {
    printf("Hello"); // NO newline '\n'!
    fork();
    return 0;
}

Output:

HelloHello

Why did "Hello" print twice if it was BEFORE fork()?

Because printf buffers output in memory until it sees a newline (\n). When fork() executed, the buffer containing "Hello" was cloned into the child's memory. When both processes exited, both flushed their identical copies of the buffer!

Tip: Call fflush(stdout); right before calling fork() if you have un-flushed output.

What Does the Child Inherit?

When fork() executes, the OS duplicates almost the entire process state for the child:

  1. Virtual Address Space: The child gets a duplicate copy of the parent's stack, heap, data segment, and code.
  2. File Descriptors: Open files, sockets, and pipes are duplicated into the child's file descriptor table. The child and parent share the underlying Open File Description (so they share file read/write offsets).
  3. Execution State: Both processes resume execution at the exact same instruction—the return point of fork().

Memory Efficiency: Copy-On-Write (COW)

Physical memory is not instantly copied upon calling fork(). Modern kernels use Copy-On-Write:

  • Both parent and child point to the same physical memory pages marked as read-only.
  • Memory pages are only copied when either the parent or child tries to modify (write to) a page.

Key Concepts & Pitfalls

1. Independent Memory Spaces

Changes to variables after fork() are not shared:

int x = 10;
pid_t pid = fork();

if (pid == 0) {
    x = 20; // Modifies child's copy of x
    printf("Child x = %d\n", x); // Output: 20
} else {
    sleep(1); // Give child time to modify x
    printf("Parent x = %d\n", x); // Output: 10 (unchanged!)
}

2. Process Scheduling Order Is Non-Deterministic

The kernel's CPU scheduler decides whether the parent or child executes first after fork(). Never assume the parent runs before the child, or vice versa.

3. Zombie Processes & Reaping

When a child process terminates, it doesn't vanish completely. Its exit status remains in the kernel process table until the parent reads it using wait() or waitpid().

  • If the parent fails to call wait(), the finished child becomes a Zombie process (<defunct>).
  • If the parent exits before the child, the child becomes an Orphan process and is adopted by init / systemd (PID 1), which automatically reaps it.

Common Usage Pattern: fork() + exec()

fork() is rarely used alone when running external programs. It is typically combined with execve():

  1. fork() creates a child process (cloning the shell or parent app).
  2. The Child process rebinds standard file descriptors (e.g., using dup2 for I/O redirection) and then calls execve() to replace its memory image with a new executable (e.g., /bin/ls).
  3. The Parent process calls waitpid() to wait for the child to complete.
Loading...