Articles

How Fork Works in Linux: A Deep Dive into Process Creation with Code Examples

If you have ever wondered how a single Linux process can spawn another one almost instantly, the answer lies in a small but powerful system call: fork(). Understanding how fork works in Linux is essential for anyone writing system software, building servers, or simply trying to grasp what happens under the hood when you type a command in your shell.

In this deep dive, we will walk through the mechanics of process creation, explain copy-on-write (COW) semantics, look at exactly what gets duplicated between parent and child, and review annotated C examples. We will also cover common pitfalls, including the infamous fork bomb.

What is fork() in Linux?

The fork() system call creates a new process by duplicating the calling process. The calling process becomes the parent, and the newly created process is the child. After the call, both processes continue executing the same program, starting from the instruction right after the fork() call.

The key idea: fork() is called once but returns twice. Once in the parent (returning the child’s PID), and once in the child (returning 0). If something goes wrong, it returns -1 in the parent and no child is created.

The function signature

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

pid_t fork(void);
linux terminal process

A Minimal fork() Example in C

Let us start with the simplest possible example to see fork in action.

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

int main(void) {
    pid_t pid = fork();   // Process splits here

    if (pid < 0) {
        perror("fork failed");
        return 1;
    } else if (pid == 0) {
        // Child branch: fork() returned 0
        printf("Child  : my PID is %d, parent PID is %d\n", getpid(), getppid());
    } else {
        // Parent branch: fork() returned the child's PID
        printf("Parent : my PID is %d, child PID is %d\n", getpid(), pid);
    }

    return 0;
}

Compile and run:

gcc fork_demo.c -o fork_demo
./fork_demo

You will see two lines of output, one from each process. The order is non-deterministic because the kernel scheduler decides who runs first.

Understanding fork() Return Values

This is the part that confuses most beginners. Here is a clear breakdown:

Return value Where you see it Meaning
> 0 Parent process The PID of the newly created child
0 Child process Indicates this branch is running in the child
-1 Parent process (no child created) Fork failed. errno is set (EAGAIN, ENOMEM, etc.)
linux terminal process

Copy-on-Write: The Secret Behind Fast Forks

In the early days of Unix, fork actually copied the entire memory of the parent process into the child. That was slow and wasteful, especially when the child was about to call exec() and throw away that memory anyway.

Modern Linux uses copy-on-write (COW). Here is the trick:

  1. When fork is called, the kernel does not physically duplicate the parent’s memory pages.
  2. Instead, both processes share the same physical pages, but those pages are marked read-only in the page tables.
  3. As long as neither process writes to a page, they keep sharing it. Reads are essentially free.
  4. The moment one of them tries to write, the CPU triggers a page fault. The kernel then allocates a fresh copy of that single page for the writing process, marks it writable, and execution resumes.

This makes fork() extremely cheap, even for processes using hundreds of megabytes of memory.

What Gets Duplicated Between Parent and Child?

The child is almost an identical clone of the parent, but not entirely. Here is what is shared, copied, and reset.

Duplicated in the child

  • Address space (virtually, via COW): code, data, heap, stack
  • File descriptors (the descriptor numbers and their offsets in the open file table)
  • Environment variables
  • Working directory and root directory
  • Signal handlers and signal mask
  • User and group IDs
  • Resource limits (rlimits)

Different in the child

  • PID: a brand new process ID
  • PPID: set to the PID of the calling parent
  • Pending signals are cleared
  • File locks are not inherited
  • Process CPU time counters are reset
  • Timers (alarm, setitimer) are not inherited
  • Memory locks (mlock) and async I/O operations are not inherited

fork() with Shared File Descriptors

This is a frequent source of subtle bugs. Both parent and child share the same open file description, including the file offset. Watch what happens:

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/wait.h>

int main(void) {
    int fd = open("shared.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);

    if (fork() == 0) {
        write(fd, "CHILD\n", 6);
        _exit(0);
    } else {
        write(fd, "PARENT\n", 7);
        wait(NULL);
    }

    close(fd);
    return 0;
}

Because the file offset is shared, the two writes do not overwrite each other. The output file will contain both lines, in an order that depends on scheduling. If each process had reopened the file independently, the offsets would be separate and one write could clobber the other.

linux terminal process

The fork() + exec() Pattern

On Linux, you never “start” a brand new program out of thin air. You always fork first, then exec. The shell does this every time you run a command:

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

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

    if (pid == 0) {
        // Child replaces itself with /bin/ls
        execlp("ls", "ls", "-l", NULL);
        perror("exec failed");  // Only reached if exec fails
        _exit(1);
    } else if (pid > 0) {
        int status;
        waitpid(pid, &status, 0);
        printf("Child exited with status %d\n", WEXITSTATUS(status));
    }

    return 0;
}

Thanks to copy-on-write, the wasted work of “duplicating” the parent’s memory just to throw it away in exec() is essentially zero.

Common Pitfalls When Using fork()

1. Forgetting to wait() leads to zombie processes

When a child terminates, it stays in the process table as a zombie until the parent calls wait() or waitpid() to collect its exit status. If you spawn many children and never reap them, you will leak entries in the kernel process table.

2. Orphan processes get adopted by init (or systemd)

If the parent dies before the child, the child is reparented to PID 1. This is not a bug per se, but it can be surprising in daemon code.

3. Buffered I/O can produce duplicated output

printf("Hello ");   // Goes into stdio buffer
fork();
// Both parent and child now hold the same buffered "Hello "
// Each will flush it on exit, so you see "Hello" twice

Fix: call fflush(stdout) before forking, or use unbuffered write().

4. The Fork Bomb

A fork bomb is a process that recursively forks itself until the system runs out of PIDs or memory. The classic shell version is the cryptic :(){ :|:& };:. In C it looks like this (do not run this without limits):

// WARNING: this will freeze an unprotected system
#include <unistd.h>
int main(void) {
    while (1) fork();
}

Protect yourself with ulimit -u (max user processes) or by setting LimitNPROC in systemd units. On modern distributions, cgroups v2 with pids.max is the recommended defense.

linux terminal process

fork() vs vfork() vs clone()

Call Memory behavior Typical use
fork() Full address space duplicated via COW General process creation
vfork() Child borrows parent’s memory; parent is suspended until exec or exit Legacy optimization, mostly obsolete with COW
clone() Fine-grained control over what is shared (memory, FDs, namespaces, etc.) Threads (pthread_create), containers, namespaces

Under the hood, fork() in glibc is implemented as a call to clone() with a specific set of flags.

How to Inspect Forks in Practice

If you want to actually see fork happening on a running system, try these tools:

  • strace -f ./your_program: traces system calls and follows children
  • ps -ef –forest: shows the process tree
  • pstree -p: a more compact tree view with PIDs
  • perf trace or bpftrace: modern kernel tracing of fork/clone events

FAQ

Is fork() a system call or a library function?

It is both, in a sense. fork() is a C library wrapper that ultimately invokes the clone system call in the Linux kernel.

Why does fork() return twice?

Because after the call, two processes exist. Each one returns from fork() independently, with its own return value: the child’s PID for the parent, and 0 for the child.

Does fork() copy the entire memory of the parent?

Not physically. Thanks to copy-on-write, pages are shared read-only until one process writes, at which point only the modified page is duplicated.

Are threads created by fork()?

No. Threads on Linux are created with clone() (used internally by pthread_create), which shares memory and file descriptors with the caller. Fork creates a separate process with its own address space.

What happens to threads when a multithreaded program calls fork()?

Only the thread that called fork() exists in the child. All other threads disappear, which can leave mutexes locked and data structures in inconsistent states. Use pthread_atfork() handlers carefully, or prefer posix_spawn() when forking from threaded programs.

How can I prevent a fork bomb on my server?

Set per-user process limits with ulimit -u, configure /etc/security/limits.conf, or use cgroups v2 with pids.max to cap the number of processes in a control group.

Conclusion

Understanding how fork works in Linux unlocks a clearer picture of the entire Unix process model. From the elegance of “call once, return twice” to the efficiency of copy-on-write, fork is a beautifully designed primitive that has powered Unix systems for more than fifty years and still does on every Linux box running today.

Master fork, pair it with exec() and wait(), stay mindful of buffered I/O and process limits, and you will be well equipped to build robust system software on Linux.

Latest Posts

No Posts Found!

Banner of the Day

NewsLetter

Do not miss our news
Sign up and receive the latest news of our company
Newsletter

Contact Info
Copyright © 2022 I-4 Linux. All Rights Reserved.