Systems programming with POSIX · about 65 min

Processes: fork, exec and wait

Why this matters

Every command you type in a shell becomes a new process, created in two steps that are unique to Unix: fork copies the running shell, and exec replaces the copy with the program you asked for. The same two calls start your editor's compiler, your web server's worker processes and every container in the sandbox you are using right now. Understanding them explains how shells work, why exit statuses exist, where "zombie" processes come from, and why output sometimes appears twice. The next lesson builds pipes on top of them, and the final project in this track is a tiny shell made from exactly these pieces.

What a process is

A process is a running instance of a program: its own memory (code, globals, heap, stack, as in the very first lesson), its own file descriptor table, its current directory, environment, user ID, and a unique process ID (PID). getpid() returns your PID and getppid() your parent's. Every process except the first (PID 1, init or systemd) was created by another process, so processes form a tree, which you can see with ps -ef --forest in the sandbox.

fork: one process becomes two

fork() creates a new process that is an almost exact copy of the caller: same code, a copy of all memory, copies of all file descriptors (pointing to the same open files), at the same point in the program. Both processes return from fork; the return value tells them apart:

                         pid = fork();
                              |
             +----------------+----------------+
             |                                 |
   parent: fork returns                child: fork returns 0
   the child's PID (> 0)               (getppid() is the parent)
             |                                 |
     continues, usually waits          does its job, then exits

fork returns -1 (with errno) if it fails, typically because a process limit was reached; the sandbox allows 256 processes.

The child gets a copy of memory, not shared memory: changing a variable in one process does not affect the other. Copying gigabytes would be slow, so the kernel uses copy-on-write: both processes initially share the same physical pages marked read-only, and a page is copied only when one of them writes to it.

exec: become another program

The exec family replaces the current process's program with a new one: the code, globals, heap and stack are thrown away and the new program's main starts. The PID and open file descriptors stay. exec does not return on success, because the code that called it no longer exists; if it returns, it failed.

The variants differ in how you pass arguments (list vs vector) and whether the program is looked up in PATH (p):

callexample
execl(path, arg0, arg1, ..., NULL)execl("/bin/ls", "ls", "-l", (char *)NULL)
execlp(file, arg0, ..., NULL)execlp("ls", "ls", "-l", (char *)NULL)
execv(path, argv)execv("/bin/ls", args)
execvp(file, argv)execvp("ls", args)

args is an array of strings ending with NULL, exactly like the argv your own main receives, and by convention args[0] is the program's name.

wait: collect the child

The parent learns how a child ended with waitpid(pid, &status, 0) (wait for that child) or wait(&status) (wait for any child). Both block until a child has ended and return its PID. The status int encodes how it ended; decode it with macros from <sys/wait.h>:

macromeaning
WIFEXITED(status)the child called exit or returned from main
WEXITSTATUS(status)then: its exit status, 0 to 255
WIFSIGNALED(status)a signal killed it (segmentation fault, kill)
WTERMSIG(status)then: the signal number

This is where the exit status from the first module goes: your return 3; in main reaches the parent through WEXITSTATUS. When the shell prints $? as 139 after a crash, that is 128 plus WTERMSIG = 11, SIGSEGV.

All three together

#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

int main(void)
{
    int x = 10;
    fflush(stdout);                       /* see "buffers and fork" below */
    pid_t pid = fork();
    if (pid < 0) {
        perror("fork");
        return 1;
    }
    if (pid == 0) {                       /* child */
        x += 5;
        printf("child: x = %d\n", x);
        fflush(stdout);
        char *args[] = {"echo", "hello from echo", NULL};
        execvp("echo", args);
        perror("execvp");                 /* only reached if exec failed */
        _exit(127);
    }
    int status;                           /* parent */
    if (waitpid(pid, &status, 0) < 0) {
        perror("waitpid");
        return 1;
    }
    printf("parent: x = %d\n", x);
    if (WIFEXITED(status))
        printf("child exited with status %d\n", WEXITSTATUS(status));
    return 0;
}
child: x = 15
hello from echo
parent: x = 10
child exited with status 0

The child's change to x is invisible to the parent: separate memory. The child then turned into echo, which printed its argument and exited with 0. The parent waited, so its lines come last. Without the waitpid, the order of parent and child output would depend on the scheduler.

This is precisely what a shell does for echo hello from echo: fork, exec in the child, wait in the parent, store the status in $?. The convention that "command not found" gives 127 comes from children like this one that _exit(127) after a failed exec.

Why _exit in the child?

After a failed exec, the child is still a copy of the parent. Calling exit would run the parent's atexit handlers and flush copies of the parent's stdio buffers, possibly printing the parent's pending output a second time. _exit ends the process immediately without that cleanup. Use it whenever a forked child ends without having exec'd.

Buffers and fork

Because fork copies all memory, it also copies stdio's output buffer. This program prints its line twice when stdout is a pipe:

printf("before fork\n");
pid_t pid = fork();
if (pid == 0)
    return 0;
waitpid(pid, NULL, 0);
return 0;
$ ./prog | cat
before fork
before fork

On a terminal, stdout is line buffered and the \n flushed the line before the fork, so it prints once. Through a pipe the line was still sitting in the buffer, both processes inherited it, and both flushed it at exit. The fix is one line: fflush(stdout); before every fork.

Zombies and orphans

When a child exits, the kernel keeps a small record of it (PID and exit status) until the parent calls wait. Such an exited-but-uncollected process is a zombie (<defunct> in ps). A long-running parent that never waits accumulates zombies until the system runs out of PIDs. If the parent exits first, its children become orphans and are adopted by PID 1, which waits for them. The rule: every fork needs a matching wait.

Common mistakes

Recap

Going further: read fork(2), execve(2) and wait(2) at man7.org (the sandbox lacks section 2 pages). In the sandbox terminal, run sleep 30 & and then ps -o pid,ppid,stat,cmd to see your shell as the parent of sleep.

Exercises

Exercise 1 · quiz

How many lines?

With stdout connected to a terminal, how many lines does this print?

int main(void)
{
    fork();
    fork();
    printf("hi\n");
    return 0;
}
Answer
  • 4

The first fork makes 2 processes. Both execute the second fork, making 4. All 4 print. In general n forks in a row give 2^n processes. (If stdout were a pipe and the printf came before a fork, the unflushed buffer would be copied into each child too, printing even more copies: always fflush(stdout) before fork.)

Exercise 2 · quiz

Zombies

A server forks a child for each request and never calls wait or waitpid. The children finish their work and exit. What happens?

Answer
  • Each exited child stays in the process table as a zombie, keeping its PID and exit status until the parent collects it; eventually the system can run out of PIDs

When a process exits, the kernel frees its memory but keeps a small entry (PID, exit status, resource usage) so the parent can learn how it ended. Until the parent calls wait/waitpid, that entry is a zombie, shown as <defunct> by ps. A parent that never waits leaks them. If the parent itself exits, its children are adopted by init (PID 1), which reaps them. The parent does receive SIGCHLD when a child exits, but its default action is to ignore it.

Exercise 3 · write code

Run a command and report how it ended

Write a program that runs the command given in its arguments (argv[1] is the program, the rest are its arguments) in a child process, waits for it, and reports how it ended:

Use fork, execvp and waitpid. If execvp fails in the child, print run: NAME: REASON to standard error (REASON from strerror(errno)) and end the child with _exit(127). With no arguments, print usage: run COMMAND [ARGS...] to stderr and exit 2.

$ ./prog echo hi
hi
exit status 0
$ ./prog sh -c 'exit 3'
exit status 3
$ ./prog no-such-command
run: no-such-command: No such file or directory
exit status 127

File: main.c

Starting point

#define _POSIX_C_SOURCE 200809L
#include <errno.h>
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

int main(int argc, char *argv[])
{
    if (argc < 2) {
        fprintf(stderr, "usage: run COMMAND [ARGS...]\n");
        return 2;
    }
    /* fork; in the child execvp(argv[1], &argv[1]); in the parent waitpid and report */
    return 0;
}

Example with arguments echo hi

expected output

hi
exit status 0

Example with arguments sh -c exit 3

expected output

exit status 3
Hint 1

argv is NULL-terminated, so &argv[1] is already a valid argument vector for the child: execvp(argv[1], &argv[1]).

Hint 2

execvp only returns if it failed. Right after it, print the error and call _exit(127).

Hint 3

In the parent: waitpid(pid, &status, 0); then WIFEXITED/WEXITSTATUS or WIFSIGNALED/WTERMSIG.

Reference solution
#define _POSIX_C_SOURCE 200809L
#include <errno.h>
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

int main(int argc, char *argv[])
{
    if (argc < 2) {
        fprintf(stderr, "usage: run COMMAND [ARGS...]\n");
        return 2;
    }
    fflush(stdout);
    pid_t pid = fork();
    if (pid < 0) {
        perror("fork");
        return 1;
    }
    if (pid == 0) {
        execvp(argv[1], &argv[1]);
        fprintf(stderr, "run: %s: %s\n", argv[1], strerror(errno));
        _exit(127);
    }
    int status;
    if (waitpid(pid, &status, 0) < 0) {
        perror("waitpid");
        return 1;
    }
    if (WIFEXITED(status)) {
        printf("exit status %d\n", WEXITSTATUS(status));
        return WEXITSTATUS(status);
    }
    if (WIFSIGNALED(status)) {
        printf("killed by signal %d\n", WTERMSIG(status));
        return 128 + WTERMSIG(status);
    }
    return 1;
}

Exercise 4 · write code

Workers that answer with their exit status

The input is a list of positive integers (at most 50). For each number, fork a child process that tests whether the number is prime and reports the answer purely through its exit status: exit(1) (or _exit(1)) for prime, 0 for not prime. Start all children first, then collect them all with wait or waitpid, and print how many of the numbers are prime:

$ echo "2 3 4 5 9 11 1 97" | ./prog
primes: 5

Children must not print anything, and the parent must not test primality itself: the check looks for fork and wait in your code. Think about why each child should end with _exit, or return from a function that does, rather than falling back into the parent's loop.

File: main.c

Starting point

#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

static int is_prime(long n)
{
    if (n < 2)
        return 0;
    for (long d = 2; d * d <= n; d++)
        if (n % d == 0)
            return 0;
    return 1;
}

int main(void)
{
    long n;
    int count = 0;
    while (scanf("%ld", &n) == 1)
        count += is_prime(n);      /* replace with one child per number */
    printf("primes: %d\n", count);
    return 0;
}

Example

input

2 3 4 5 9 11 1 97

expected output

primes: 5

Example

input

1000003 1000001 999983 1 2

expected output

primes: 3
Hint 1

In the loop: pid = fork(); in the child (pid == 0) call _exit(is_prime(n)) so it never continues the loop; in the parent count how many children you started.

Hint 2

After reading all input, call wait(&status) once per child and add WEXITSTATUS(status).

Hint 3

Children inherit the parent's unread stdin buffer too, which is another reason they must _exit immediately instead of returning into the loop.

Reference solution
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

static int is_prime(long n)
{
    if (n < 2)
        return 0;
    for (long d = 2; d * d <= n; d++)
        if (n % d == 0)
            return 0;
    return 1;
}

int main(void)
{
    long n;
    int started = 0;
    fflush(stdout);
    while (scanf("%ld", &n) == 1) {
        pid_t pid = fork();
        if (pid < 0) {
            perror("fork");
            break;
        }
        if (pid == 0)
            _exit(is_prime(n));
        started++;
    }
    int count = 0;
    for (int i = 0; i < started; i++) {
        int status;
        if (wait(&status) < 0) {
            perror("wait");
            return 1;
        }
        if (WIFEXITED(status))
            count += WEXITSTATUS(status);
    }
    printf("primes: %d\n", count);
    return 0;
}