Home / System programming

System programming

Process management: how fork() creates a child process

How one system call splits a process in two, what the child inherits, and how the parent collects its exit status.

By LLD Coder 12 min read

What fork() does

On Linux, every program runs inside a process: a running instance with its own memory, open files and process ID (PID). The fork() system call creates a new process by duplicating the one that calls it. The caller is the parent and the new process is the child.

The strange part is that fork() is called once but returns twice, once in each process:

  • In the parent, it returns the PID of the new child.
  • In the child, it returns 0.
  • On failure, it returns -1 in the parent, no child is created, and errno is set.

Your code uses that return value to decide which process it is and what each one should do. This is how a shell starts every command you type.

Your first fork

The program below creates a child. The child prints its PID and exits with status 7. The parent waits for it and reads that status.

fork_demo.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

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

    if (pid < 0) {
        perror("fork");
        return EXIT_FAILURE;
    }

    if (pid == 0) {
        /* child */
        printf("child:  pid=%d parent=%d
", getpid(), getppid());
        exit(7);
    }

    /* parent */
    printf("parent: pid=%d created child %d
", getpid(), pid);

    int status;
    if (waitpid(pid, &status, 0) == -1) {
        perror("waitpid");
        return EXIT_FAILURE;
    }
    if (WIFEXITED(status))
        printf("parent: child exited with status %d
", WEXITSTATUS(status));

    return EXIT_SUCCESS;
}
terminal
gcc -Wall -Wextra -o fork_demo fork_demo.c
./fork_demo
parent: pid=4242 created child 4243
child: pid=4243 parent=4242
parent: child exited with status 7

Your PIDs will differ. The order of the first two lines can also change between runs, because the kernel does not guarantee whether the parent or the child runs first. Never write code that depends on it. If you need an order, use wait or another form of synchronization.

Separate memory, copy-on-write

The child gets a copy of the parent’s memory: variables, the heap and the stack. After the fork the two processes live in separate address spaces, so a change in one is never seen by the other.

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

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

    if (pid == 0) {
        counter = 200;
        printf("child:  counter=%d
", counter);
        return 0;
    }

    wait(NULL);
    printf("parent: counter=%d
", counter);
    return 0;
}
child: counter=200
parent: counter=100

Copying all memory on every fork would be slow, so Linux uses copy-on-write. At first, parent and child share the same physical pages, marked read-only. The kernel makes a private copy of a page only when one of the processes writes to it. According to the Linux man page, the real cost of fork() is therefore just the time and memory needed to duplicate the parent’s page tables and create a new task structure for the child.

What the child inherits

The child is an almost exact duplicate of the parent. It inherits things like your variables, environment, current directory and open file descriptors. A few details matter in practice:

  • Different IDs. The child has its own unique PID, and its parent PID is the PID of the process that forked it.
  • Shared open files. Inherited file descriptors point to the same open files as the parent’s, so they share the file offset. If both write to the same file, their output interleaves instead of overwriting.
  • One thread only. The child has a single thread: the one that called fork(), even if the parent had many.
  • Things not inherited. Pending signals, timers, memory locks and resource usage counters start fresh in the child.
  • Exit signal. When the child terminates, the parent receives SIGCHLD.

Read the manualRun man 2 fork for the full list of what is and isn’t inherited. It also lists the possible errors, such as EAGAIN when a process or memory limit is reached.

fork and exec

A copy of the same program is rarely what you want. Usually you want to run a different program. So the child calls one of the exec functions, which replaces its memory with a new program. The PID stays the same. This fork-then-exec pair is how shells launch commands.

run_ls.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>

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

    if (pid < 0) {
        perror("fork");
        return EXIT_FAILURE;
    }

    if (pid == 0) {
        execlp("ls", "ls", "-l", (char *)NULL);
        perror("execlp");      /* only reached if exec failed */
        _exit(127);
    }

    int status;
    waitpid(pid, &status, 0);
    return WIFEXITED(status) ? WEXITSTATUS(status) : EXIT_FAILURE;
}

If exec succeeds it never returns, so any code after it runs only on failure. In that case the child should call _exit, not exit, so it doesn’t flush buffered data that belongs to the parent.

wait, zombies and orphans

When a child ends, the kernel keeps a small record of it, including its exit status, until the parent asks for it with wait or waitpid. Two special states come from this:

  • Zombie. A child that has exited but hasn’t been waited for. It uses almost no memory, but it still holds its PID. Many zombies can use up the process table. The fix is for the parent to call wait, or to exit itself.
  • Orphan. A child whose parent exited first. The kernel gives it a new parent, normally init (PID 1) or a designated subreaper, which then cleans it up.

You can see a zombie yourself. Write a parent that forks, then sleeps for 30 seconds without calling wait while the child exits right away. In another terminal, run:

terminal
ps -o pid,ppid,stat,cmd --ppid <parent-pid>
PID PPID STAT CMD
5121 5120 Z+ [zombie] <defunct>

The Z in the STAT column means zombie. Once the parent exits or reaps the child, the entry disappears.

Common mistakes

Forgetting to check for -1

If fork() fails and you don’t check, -1 is non-zero, so your code treats it like a parent. Always test for failure first.

Duplicated buffered output

The stdio buffer is part of the process memory, so it gets copied too. Look at this program:

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

int main(void)
{
    printf("hello
");
    fork();
    return 0;
}

On a terminal, stdout is line-buffered, so hello prints once. When the output goes to a pipe or file, it is fully buffered. The text is still waiting in the buffer when you fork, and both processes flush it on exit:

$ ./buf
hello
$ ./buf | cat
hello
hello

Call fflush(stdout) before fork() to avoid this.

Forking in a loop without control

A loop that forks, where the children also keep looping, grows exponentially and can freeze a machine. Make sure only the parent continues the loop and each child exits or calls exec. Practice in a virtual machine.

Forking in a multithreaded program

Only the calling thread exists in the child, but it inherits locks held by other threads at that moment. Until the child calls exec, keep it to simple, async-signal-safe operations.

Where to go next

You now know the full life cycle: fork creates the child, exec optionally replaces its program, and wait collects its exit status. Good next steps:

  1. Connect parent and child with pipe() and send data between them
  2. Handle SIGCHLD to reap children without blocking
  3. Read about vfork() and clone(), which Linux uses underneath fork()
  4. Build a tiny shell that reads a line, forks, and runs it
LLD Coder

Writes about Linux, system programming, device drivers and Python.