In a PHP CLI parent that forks workers with pcntl_fork(), how do you reap finished children and read how each one ended?
answer
- the parent must wait for each child
- pcntl_wait() blocks for any child
- WNOHANG returns 0 instead of blocking
- status is decoded, not read directly
- pcntl_wifexited() before pcntl_wexitstatus()
basics
~10 sCall pcntl_wait() or pcntl_waitpid() for every child: they return the PID of a child that ended and fill a status integer. Decode it with pcntl_wifexited()/pcntl_wexitstatus() or pcntl_wifsignaled()/pcntl_wtermsig(); WNOHANG makes the call non-blocking.
solid answer
~40 sA parent collects each finished child with `pcntl_wait($status)` (any child) or `pcntl_waitpid($pid, $status, $flags)` (a given PID, or `-1` for any). Both return the reaped child's PID, `-1` on error (for example when no children are left), and with the `WNOHANG` flag `0` when no child has ended yet, so a supervisor can poll in its main loop. The `$status` argument is filled by reference with a raw status word, which you decode: `pcntl_wifexited($status)` then `pcntl_wexitstatus($status)` for the exit code, or `pcntl_wifsignaled($status)` then `pcntl_wtermsig($status)` for the signal that killed it. A parent that never waits leaves finished children in the process table. To stop a child, send it `posix_kill($pid, SIGTERM)` and then wait for it.
code
php · 18 lines<?php
declare(strict_types=1);
/** @param array<int, string> $workers pid => name */
function reapFinished(array &$workers): void
{
while (($pid = pcntl_waitpid(-1, $status, WNOHANG)) > 0) {
$name = $workers[$pid] ?? '?';
unset($workers[$pid]);
if (pcntl_wifexited($status)) {
printf("%s exited with %d\n", $name, pcntl_wexitstatus($status));
} elseif (pcntl_wifsignaled($status)) {
printf("%s killed by signal %d\n", $name, pcntl_wtermsig($status));
}
}
// loop ends on 0 (children still running) or -1 (none left)
}go deeper
Recall that a parent must call pcntl_wait() or pcntl_waitpid() for each child, and that pcntl_wexitstatus() reads the exit code out of the status value.
Explain the return values (PID, 0 with WNOHANG, -1 on error), why status must be decoded with the pcntl_wif* functions, and how a non-blocking reaping loop is written.
Describe a supervisor that reaps in its main loop, distinguishes crashes by signal from clean exits, restarts workers, and escalates from SIGTERM to SIGKILL after a grace period.
Decide whether hand-written pcntl supervision is worth owning, or whether an external process manager should restart workers and PHP should run one worker per process.
## Why a parent has to wait When a child process ends, the operating system keeps a small record of it — its PID and how it ended — until the parent asks for that record. Asking is called **reaping**. A PHP parent that forks workers with `pcntl_fork()` and never reaps them leaves those records behind; a long-running supervisor that does this for days slowly fills the process table. (The process-state theory behind this belongs to the Linux process topics; here the question is which PHP functions do the reaping.) ## The two wait functions Both come from the pcntl extension and take the status by reference: - `pcntl_wait(&$status, int $flags = 0, &$resource_usage = []): int` — waits for **any** child. - `pcntl_waitpid(int $process_id, &$status, int $flags = 0, &$resource_usage = []): int` — waits for a specific child, or for any child when `$process_id` is `-1`. Other values select children by process group. Their return values are the same: | Return | Meaning | |---|---| | a positive PID | that child ended and has now been reaped; `$status` describes it | | `0` | only with `WNOHANG`: children exist but none has ended yet | | `-1` | error — most often that there are no children left to wait for | With the default `$flags = 0` the call **blocks** until a child ends. The `WNOHANG` flag turns it into a poll, which is what a supervisor with its own main loop needs. The optional `$resource_usage` array receives the child's resource usage figures, such as `ru_maxrss`, where the platform supports it. ## Decoding `$status` The integer written into `$status` is not the exit code. It packs several facts together, and pcntl provides one function per fact: 1. `pcntl_wifexited($status)` — `true` if the child ended normally through `exit()` or by reaching the end of the script. 2. `pcntl_wexitstatus($status)` — the exit code; meaningful only when `pcntl_wifexited()` is `true`. 3. `pcntl_wifsignaled($status)` — `true` if a signal killed the child. 4. `pcntl_wtermsig($status)` — which signal did it, for example `SIGKILL` from an out-of-memory kill. 5. `pcntl_wifstopped()` and `pcntl_wstopsig()` — the child was stopped rather than ended (reported only when you pass `WUNTRACED`). Reading `pcntl_wexitstatus()` without first checking `pcntl_wifexited()` gives a meaningless number for a child that was killed by a signal. ## The supervisor loop A typical pre-fork supervisor keeps a map from PID to worker slot and reaps without blocking on each pass: - call `pcntl_waitpid(-1, $status, WNOHANG)` in a `while` loop until it returns `0` or `-1`, because several children may have ended since the last pass; - for each reaped PID, remove it from the map and decide whether to start a replacement: restart after a crash, but not while shutting down; - log the exit code or terminating signal so crashes are visible. ## Stopping children To shut a pool down, the parent signals each child and then waits for it: - `posix_kill(int $process_id, int $signal): bool` sends a signal and returns `false` on failure (read the reason with `posix_get_last_error()` and `posix_strerror()`); - send `SIGTERM` first so each child can finish its current job; - then reap with a blocking `pcntl_waitpid($pid, $status)`, or poll with `WNOHANG` and escalate to `SIGKILL` if a child has not ended within your grace period; - `posix_kill($pid, 0)` sends nothing but reports whether the process exists and may be signalled, which is a cheap liveness check. ## Common mistakes - Calling `pcntl_wait()` once when several children have ended — only one is reaped per call. - Treating `-1` as "no child finished yet" and spinning on it; without `WNOHANG`, `0` is never returned, and `-1` usually means nothing is left. - Checking only exit codes, so a worker killed by a signal is reported as "exit 0". - Forgetting that each child reaps its **own** children; a parent cannot reap grandchildren.
- Why loop on pcntl_waitpid(-1, $status, WNOHANG) instead of calling it once per pass?Each call reaps at most one child. If three workers ended since the last pass, a single call collects one and leaves two unreaped until later passes. Looping until the call returns `0` (children still running) or `-1` (none left) collects everything that has finished right now.
- A worker's pcntl_wexitstatus() reads 0, yet it clearly crashed. What did the parent miss?It skipped `pcntl_wifexited()`. A child killed by a signal did not exit normally, so its exit-status bits carry no meaning. Check `pcntl_wifsignaled($status)` and read `pcntl_wtermsig($status)` — a `SIGKILL` there often means the kernel's out-of-memory killer or a supervisor's hard stop.
saying these in an interview costs you the question
- The status argument of pcntl_wait() is the child's exit code
- pcntl_waitpid() returns 0 when there are no children left
- One pcntl_wait() call reaps every child that has ended
- Finished children disappear by themselves, so a parent never needs to wait
- posix_kill() kills the process; it cannot send other signals