Systems programming · lesson 05

Processes and fork

A process is a running program plus everything the OS tracks for it: address space, file descriptors, signal handlers, user ID, working directory. <code>fork()</code> duplicates all of that in a single syscall.

in progress
14 min

What a process is

A program is a file on disk. A process is that program loaded into memory and actively executing. The OS tracks each process with a Process Control Block (PCB): the current register values, the page table, the open fd table, signal masks, the working directory, the user and group IDs, and more. Each process gets a unique Process ID (PID).

On Linux, process 1 (init or systemd) is the ancestor of every other process. Every process except process 1 has a parent. This parent-child relationship forms a tree — and it has practical consequences you'll feel immediately when working with fork.

fork: one call, two returns

fork() is the Unix mechanism for creating a new process. It clones the current process — copying its address space, fd table, signal state — and returns twice: once in the parent (returning the child's PID), and once in the child (returning 0). The two processes then execute independently from the point of the fork call.

c
#include <unistd.h> #include <stdio.h> #include <sys/wait.h> int main(void) { pid_t pid = fork(); if (pid < 0) { perror("fork"); // fork failed return 1; } else if (pid == 0) { // child process printf("child: pid=%d\n", getpid()); return 0; } else { // parent process: pid = child's PID printf("parent: child pid=%d\n", pid); wait(NULL); // wait for child to finish } return 0; }
💡
fork is copy-on-write. The child doesn't immediately get its own physical copy of every page. The OS marks all shared pages read-only and only copies a page when either process writes to it. This makes fork cheap even for large processes — you only pay for the pages you actually modify after the fork.

exec: replacing the process image

fork creates a child that runs the same program. Usually you want the child to run a different program. That's what exec does: it replaces the current process's address space with a new program loaded from a file. The PID stays the same; everything else (text, data, stack, heap) is replaced.

exec does not return on success — there's nothing to return to, since the calling program's code no longer exists. It only returns (with -1) if the exec fails.

c
#include <unistd.h> #include <sys/wait.h> int main(void) { pid_t pid = fork(); if (pid == 0) { // child: replace with /bin/ls char *argv[] = { "ls", "-l", NULL }; execvp("ls", argv); perror("exec"); // only reached if exec fails _exit(1); } waitpid(pid, NULL, 0); return 0; }

This fork-exec pattern is how every shell runs a command. The shell forks itself, the child sets up any redirections (using dup2), then calls exec. The parent waits. The child's program runs as a fresh process image but inherits the carefully prepared fd table.

wait: reaping children

When a child process exits, it doesn't fully disappear. Its PID and exit status remain in the kernel process table — a zombie — until the parent collects them with wait or waitpid. This lets the parent check whether the child succeeded.

⚠️
Unreaped children become zombies. If a parent never calls wait, its children stay in zombie state indefinitely, consuming a PID slot. On a server that forks thousands of workers, failing to reap them eventually exhausts the PID namespace. Servers handle this by waiting in a SIGCHLD signal handler or using waitpid(-1, ..., WNOHANG) to reap non-blockingly.

_exit vs exit

In the child process after fork, use _exit() instead of exit(). exit() flushes stdio buffers and runs atexit handlers. Since the child shares the same buffers as the parent at the time of fork, calling exit() in the child can flush and duplicate output that the parent will also write. _exit() terminates immediately without touching stdio.

one-line takeaway

fork() clones the process, exec() replaces it with a new program, and wait() reaps the result — these three syscalls are the entire mechanism behind how a shell runs commands.