Introduction

PHP's pcntl_fork() creates child processes for parallel execution, but when the parent does not call pcntl_wait() or pcntl_waitpid() to collect the child's exit status, the child becomes a zombie process -- an entry in the process table that consumes a PID slot but no memory. Over time, zombie processes accumulate, eventually exhausting available PIDs and preventing new processes from being created. The fix requires proper signal handling for SIGCHLD or explicit wait calls in the parent process.

Symptoms

bash
# Check for zombie processes
ps aux | grep Z
# Output shows zombie processes:
# www-data 12345  0.0  0.0    0     0 ?        Z    10:00   0:00 [php] <defunct>

Or:

bash
pcntl_fork(): fork failed - Cannot allocate memory
# Actually means: no more PIDs available (not actual memory issue)

Common Causes

  • No pcntl_wait() call: Parent does not collect child exit status
  • SIGCHLD ignored: Signal handler not set to reap children
  • Web server context: PHP-FPM/Apache may not support pcntl properly
  • Child exits without parent waiting: Parent continues without wait
  • Multiple forks without cleanup: Loop forks children but never waits
  • pcntl not available: Extension not compiled or disabled

Step-by-Step Fix

Step 1: Use pcntl_wait to collect child status

```php $children = [];

for ($i = 0; $i < 5; $i++) { $pid = pcntl_fork();

if ($pid === -1) { die("Could not fork child $i\n"); } elseif ($pid === 0) { // Child process doWork($i); exit(0); // Always exit child explicitly } else { // Parent process $children[] = $pid; } }

// Wait for all children foreach ($children as $pid) { pcntl_waitpid($pid, $status); if (pcntl_wifexited($status)) { $exitCode = pcntl_wexitstatus($status); echo "Child $pid exited with code $exitCode\n"; } } ```

Step 2: Use SIGCHLD signal handler for automatic reaping

```php // Install SIGCHLD handler to automatically reap children pcntl_signal(SIGCHLD, function ($signo, $status) { while (($pid = pcntl_waitpid(-1, $status, WNOHANG)) > 0) { if (pcntl_wifexited($status)) { $code = pcntl_wexitstatus($status); error_log("Child $pid exited with code $code"); } elseif (pcntl_wifsignaled($status)) { $code = pcntl_wtermsig($status); error_log("Child $pid killed by signal $code"); } } });

// Fork children - SIGCHLD handler will reap them automatically for ($i = 0; $i < 10; $i++) { $pid = pcntl_fork(); if ($pid === 0) { doWork($i); exit(0); } }

// Parent can continue working - children reaped automatically // But must dispatch signals while ($stillWorking) { pcntl_signal_dispatch(); // Process pending signals usleep(100000); }

// Wait for any remaining children while (pcntl_waitpid(-1, $status, WNOHANG) > 0); ```

Step 3: Double fork for daemon processes

```php function daemonize(): void { // First fork $pid = pcntl_fork(); if ($pid === -1) { die("Cannot fork\n"); } elseif ($pid > 0) { // Parent exits exit(0); }

// Detach from terminal if (posix_setsid() === -1) { die("Cannot setsid\n"); }

// Second fork - ensures the process cannot reacquire a terminal $pid = pcntl_fork(); if ($pid === -1) { die("Cannot fork again\n"); } elseif ($pid > 0) { exit(0); }

// Grandchild is now a daemon - init will reap it chdir('/'); umask(0);

// Close standard file descriptors fclose(STDIN); fclose(STDOUT); fclose(STDERR); } ```

Prevention

  • Always call pcntl_waitpid() for every child process you fork
  • Use SIGCHLD signal handler with pcntl_waitpid(-1, ..., WNOHANG) for automatic reaping
  • Always exit child processes explicitly with exit() -- never let them return
  • Call pcntl_signal_dispatch() regularly to process pending signals
  • Use the double fork pattern for daemon processes
  • Monitor zombie count with ps aux | grep Z in production
  • Do not use pcntl_fork() in web request context -- use queues instead