Memra

A call that returns twice, and a corpse that needs collecting

◈ 12 cards

Create a child with fork, collect its status with wait, decode the status word with the macros, and both create and prevent a zombie.

fork() returns twice, and that is not a metaphor

#include <unistd.h>
pid_t fork(void);

One call, three possible return values:

  • 0 — you are in the child.
  • a positive number — you are in the parent, and this is the child's PID.
  • −1 — the fork failed; there is no child, and errno says why (EAGAIN for a process limit, ENOMEM for no swap).

The "returns twice" line sounds like a riddle until you put the events in order. fork() duplicates the calling process — address space, descriptor table, environment, working directory, umask, signal dispositions, everything — and the duplicate exists before fork() has finished executing. So the call has to finish in both processes. It does, and it hands each of them a different answer. Both then continue at the very same statement: the assignment that captures the return value.

That is why the standard shape is a three-way branch on one variable:

pid_t pid = fork();
if (pid == -1)       perror("fork");
else if (pid == 0)   /* child  */ ;
else                 /* parent */ ;

The child inherits a copy of the descriptor table, so files opened before the fork are shared through the same open-file-table entries — one offset between the two of them. It gets its own PID, a new parent, reset resource counters, and exactly one thread of execution even if the parent had many.

Collecting a child: wait and waitpid

#include <sys/wait.h>
pid_t wait(int *stat_loc);
pid_t waitpid(pid_t pid, int *stat_loc, int options);

Both return the PID of the child they collected, or −1. wait() blocks until some child terminates. waitpid() names which: −1 for any child (which makes it wait()), a positive PID for that child specifically, 0 for any child in the caller's process group. Its options argument takes WNOHANG, which turns the call non-blocking — it returns 0 immediately if there is nothing to report, instead of waiting.

The status word, and the macros that decode it

The int filled in through stat_loc is not the exit status. It is a packed word carrying two different pieces of news:

  • The high byte is the value the child passed to exit().
  • The low byte is the reason the child terminated: 0 for a normal exit, or the number of the signal that killed it, with an extra bit set if a core was dumped.

So a child that ran exit(3) leaves 0x0300, which as an integer is 768. A child killed by Ctrl-C leaves SIGINT in the low byte. Comparing the raw int against 3 is therefore wrong twice over: wrong value, and no way to tell an exit from a killing.

Decode it with the macros from <sys/wait.h> and never by hand:

int status;
pid_t child = wait(&status);

if (WIFEXITED(status))
    printf("%d exited with %d\n", child, WEXITSTATUS(status));
else if (WIFSIGNALED(status))
    printf("%d was killed by signal %d\n", child, WTERMSIG(status));

WIFEXITED is true for a normal exit and WEXITSTATUS then gives the exit() argument. WIFSIGNALED is true for a death by signal and WTERMSIG then gives the signal. They are mutually exclusive, and one of them is always true for a terminated child. Use the names, not the bit arithmetic — the layout is not identical on every system, and the macros are the portable interface to it.

Zombies

When a process exits, the kernel releases nearly everything it owned: its memory, its descriptors, its open files. One thing is kept: the proc structure, because that is where the exit status now lives, and the parent has not read it yet.

A process in that condition — terminated, status not yet collected — is a zombie. ps shows it in state Z with the command as <defunct>. It uses no CPU and no memory; what it occupies is a slot in the process table, and a program that forks in a loop without ever waiting will eventually exhaust that table.

The zombie disappears the moment its status is read. Two things can do that:

  1. The parent calls wait() or waitpid(). It reads the status, the kernel frees the proc structure, and the entry is gone.
  2. The parent dies. The orphaned zombie is adopted by init (PID 1), which does nothing all day but wait(). So no child of init ever stays a zombie — which is why killing the parent is the crude way to clear a screenful of them.

Making one on purpose therefore needs both halves: a child that exits immediately, and a parent that stays alive and does not wait. Drop either and there is no zombie — if the parent waits, the status is collected; if the parent exits, init collects it.

Preventing them in a server

A program that must keep working cannot sit in wait(). The standard fix is to reap from a SIGCHLD handler, draining every finished child without blocking on any of them:

void reap(int sig) {
    int saved = errno;
    while (waitpid(-1, NULL, WNOHANG) > 0)
        ;
    errno = saved;
}

Install it with sigaction(), and never with signal():

struct sigaction sa;
sa.sa_handler = reap;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART;
sigaction(SIGCHLD, &sa, NULL);

Under the System V semantics signal() inherited, the disposition is reset to the default the moment the handler is entered, so the second child to die is never reaped and the leak the handler exists to prevent comes straight back. sigaction() keeps the handler installed, sigemptyset(&sa.sa_mask) says no extra signal is blocked while it runs, and SA_RESTART resumes a slow system call the delivery interrupted instead of failing it with EINTR. Module 13 takes this interface apart in full.

The loop matters: several children can die while the handler is not running, and signals do not queue — one SIGCHLD may stand for three deaths. WNOHANG makes the loop terminate when there is nothing left instead of blocking on the next child. Saving and restoring errno matters too, because the handler can interrupt a system call in the main flow and clobber the value it was about to read.

Many textbook servers omit all of this and quietly accumulate zombies for as long as they run.

the call beginsthe child now existstwo different valuesparent calls fork()one processkernel duplicates the processnew PID, copied descriptor tablefork() returns twice0 in the child, the PID in the parentboth continue at the same statementtwo processes nowOn failure fork() returns -1, there is nochild, and errno holds the reason.
The child exists before `fork()` finishes, so `fork()` has to return in both processes — with a different value in each.
1514131211109876543210exit statusthe child's exit() argument8coredumped?term signal0 = normal exit7High byte = the exit() argument (WEXITSTATUS). Low byte = the reason: 0 for a normal exit (WIFEXITED),otherwise the signal number (WTERMSIG).
The word `wait()` fills in. Read it with the macros — the raw integer for `exit(3)` is 768, not 3.
t1t2t3t4t5t6parentfork()runningsleep(30)sleep(30)wait(&s)readsstatus 3childrunningexit(3)Z <defunct>reapedBetween t3 and t5 everything the child owned is released except its proc structure, which is heldbecause it carries the exit status the parent has not read.
The child is a zombie from the instant it exits until the instant its status is read.
NORMAL ~/memra/learn/comp-325/fork-wait-the-status-word-and-zombies utf-8 LF