JOVANA
Explore Library Glossary Getting Started Three Levels Fields How it works Mission
Join the mission
All guides

wait(), Exit Status, and Reaping Children

Guide 3 left you running a brand-new program in a child process. But when that child finishes, where does the answer go — did it succeed or fail, and who picks up the body? This is the parent's other half of the deal: wait() for the child, read its exit status, and reap it so it does not linger as a zombie.

The deal fork() never finished

By the end of guide 3 you could spawn a real program: the parent calls fork(), the child calls one of the exec family, and now two processes run side by side — the parent carrying on, the child off being `ls` or `gcc` or whatever you launched. That is the fork-then-exec pattern, and it is genuinely how a shell starts every command. But it leaves a loose end. The child is going to finish — it will run `return 0` from main(), or call `exit()`, or crash. When it does, two natural questions arise that fork() and exec() never answered: did it succeed, and what happens to the process now that its program is over?

The mechanism that answers both is wait(). The parent calls `pid_t child = wait(&status)`, and the call blocks — the parent pauses right there — until one of its children terminates. When a child dies, wait() wakes the parent, returns that child's PID, and writes a small status code into the `int` you pointed it at. This is waiting and reaping: the parent collecting the news of a finished child. The relationship is deliberately one-directional and family-only — a process can wait only for its own children, never for a sibling, a parent, or an unrelated process. fork() created the parent-child bond precisely so this report can be delivered up the family line.

The exit status is a small number, packed with bits

When a program finishes normally, it hands back a single small integer — its exit status. `return 0` from main() and `exit(0)` mean the same thing: zero says "I succeeded", and any non-zero value says "something went wrong", with the particular number being the program's own code for which thing. This is a convention the whole Unix world agrees on, which is why `$ ./a.out && echo ok` only prints `ok` when a.out exits zero, and why `make` stops the build the instant a compiler returns non-zero. The status is the program's last word — its way of telling whoever launched it whether the job got done.

Here is the catch that trips up every beginner: the `int` that wait() fills in is not the exit status itself. The kernel packs more than one piece of news into that word, because a child can end in more than one way — it might have exited normally with a status, or it might have been killed by a signal (a segfault, a `kill` from the terminal). So the bits are encoded, and you must not read the raw integer directly. Instead you use a handful of standard macros to unpack it. The historic layout puts the exit code in the high byte and the killing signal (if any) in the low bits, but you should never rely on that arithmetic by hand — the macros exist exactly so you do not have to.

int status;
pid_t child = wait(&status);          /* block until a child ends */
if (child == -1) {                     /* no children, or interrupted */
    perror("wait");
} else if (WIFEXITED(status)) {         /* child called exit()/returned */
    int code = WEXITSTATUS(status);    /* the 0..255 it handed back   */
    printf("pid %d exited with %d\n", child, code);
} else if (WIFSIGNALED(status)) {       /* child was killed by a signal */
    int sig = WTERMSIG(status);
    printf("pid %d killed by signal %d\n", child, sig);
}
Always decode the status with the macros. WIFEXITED asks "did it exit on its own?"; only then is WEXITSTATUS meaningful. WIFSIGNALED asks "was it killed?"; then WTERMSIG names the signal. Reading the raw int would silently give wrong answers.

Why the parent must reap: the brief life of a zombie

Now the deeper reason wait() is not optional. When a child finishes, the kernel cannot simply throw everything away — because the exit status has to be kept somewhere until the parent asks for it, and the parent might be busy and ask much later. So the kernel tears down most of the dead child (its memory, its open files, its process image) but keeps a tiny husk: an entry in the process table holding just the PID and the exit status. A process in this in-between state — its program over, but its status not yet collected — is a zombie. It is dead but not buried, and it is waiting for its parent to read the status it left behind.

The act of calling wait() is what finally lets the kernel free that last husk — that is the reaping in "wait and reaping". When wait() returns the child's status, the kernel removes the process-table entry, the PID becomes reusable, and the zombie is gone for good. A zombie that lives for a few milliseconds between the child dying and the parent reaping it is completely normal and harmless. The danger is the parent that never calls wait(): then the husks pile up, one per finished child, each holding a PID hostage. A long-running server that forks workers and forgets to reap them will slowly leak PIDs until the system runs out and can fork nothing more. Reaping is housekeeping you cannot skip.

waitpid(): choosing which child, and not blocking

Plain wait() has two limits that bite as soon as you have more than one child. First, it waits for any child — you do not get to say which — and it returns the first one that happens to finish. Second, it always blocks, so if you have other work to do you are stuck. The fuller call waitpid(pid, &status, options) lifts both. Give it a specific PID and it waits only for that exact child; give it `-1` and it behaves like wait(), waiting for any. The third argument is where the real flexibility lives.

The option that changes everything is WNOHANG: it tells waitpid() "if no child has finished yet, do not block — just return 0 immediately and let me carry on". This turns reaping into a quick poll instead of a wall you stand against. A server can do its real work, then once in a while call `waitpid(-1, &status, WNOHANG)` in a loop to sweep up any children that have died since last time, reaping each until the call returns 0 (no more ready) or -1 (no children at all). That loop is the standard pattern behind any program that manages a pool of child processes without freezing while they run.

  1. Decide what you need: wait() for the simple one-child case ("launch it, block, read its result"); waitpid() when you have several children or must keep working.
  2. Always check the return value. wait()/waitpid() return the reaped PID, 0 (only with WNOHANG: nothing ready yet), or -1 on error — and on -1 read errno, where ECHILD means "you have no children left to wait for".
  3. Decode with the macros, in order: WIFEXITED then WEXITSTATUS for a normal exit; WIFSIGNALED then WTERMSIG for a kill. Never interpret the raw status integer yourself.
  4. To reap a whole batch without blocking, loop waitpid(-1, &status, WNOHANG) and keep reaping until it returns 0 or -1; this drains every finished child in one sweep.

The other side: exit() done right

We have stood at the parent's window the whole time; let me turn around and look from the child. Termination is how a process ends and produces the status the parent will read. The clean ways are `return n` from main() — which behaves as though main's value were passed to exit() — and calling `exit(n)` directly from anywhere. exit() does real work before the process dies: it runs functions registered with atexit(), and crucially it flushes the C standard library's I/O buffers, so anything you printf'd but that is still sitting in a buffer actually reaches the file or terminal. That flushing is why `exit()` and the lower-level `_exit()` are not interchangeable.

This matters acutely right after a fork(). The child inherits a copy of the parent's buffered output, so if a child is going to bail out instead of running its intended exec(), it should usually call _exit(), not exit() — _exit() ends the process immediately without flushing, which prevents the child from printing a second copy of buffers the parent already holds. This is one of the genuinely subtle interactions in this rung, and it is exactly the kind of detail that produces "why is this line printed twice?" bugs. We will not belabour it here, but plant the flag: in a freshly forked child that did not exec, prefer _exit().

Step back and the whole loop closes. fork() splits the process into parent and child; exec() turns the child into a new program; the child runs and finally calls exit(n), packaging its verdict into a status the kernel holds in a brief zombie; the parent calls wait(), reads that verdict, and reaps the husk. Launch, run, report, collect. Every command your shell runs travels this exact arc, and you have now seen all four corners of it — which is precisely the footing you need for the last guide, where we follow what happens when the timing goes wrong and parents and children outlive each other.