skip to content

In PHP's pcntl extension, what does pcntl_fork() return, and how do the parent and the child tell which one they are?

level: juniorimportance: nice to knowfreq 18%

answer

  1. one call, two processes return
  2. positive integer in one, zero in other
  3. -1 plus an E_WARNING on failure
  4. posix_getpid() and posix_getppid()
  5. child must exit() itself

basics

~20 s

pcntl_fork() returns the new child's PID in the parent, 0 in the child, and -1 in the parent if the fork failed. Both processes continue from the line after the call, each with its own copy of every variable.

solid answer

~40 s

`pcntl_fork()` duplicates the running CLI process and returns in both copies. The parent gets the child's PID (a positive `int`), the child gets `0`, and on failure only the parent exists and gets `-1` with an `E_WARNING` such as "Reached the maximum limit of number of processes". So you branch on the return value: `-1` handle the error, `0` do the child's work and call `exit()`, anything else remember the PID so you can reap it later. The child finds its own PID with `posix_getpid()` and its parent's with `posix_getppid()`. Forgetting `exit()` in the child is the classic bug: the child falls through into the parent's code and may fork again. The extension is CLI-only and absent on Windows.

code

php · 21 lines
php
<?php
declare(strict_types=1);

$children = [];
foreach (['a', 'b', 'c'] as $task) {
    $pid = pcntl_fork();
    if ($pid === -1) {
        fwrite(STDERR, 'fork failed: ' . pcntl_strerror(pcntl_get_last_error()) . PHP_EOL);
        exit(1);
    }
    if ($pid === 0) {
        // child: do one task, then leave so the loop does not continue here
        printf("task %s in pid %d (parent %d)\n", $task, posix_getpid(), posix_getppid());
        exit(0);
    }
    $children[] = $pid; // parent: remember the PID
}

foreach ($children as $pid) {
    pcntl_waitpid($pid, $status);
}

go deeper

for a junior

Remember the three return values: child PID in the parent, 0 in the child, -1 on failure. Then remember that the child must call exit() when its work is done.

for a middle

Explain that both processes continue from the same line with copies of memory, and show how the child uses posix_getpid() and posix_getppid() to identify itself and its parent.

for a senior

Point out what is copied and what is shared: variables are copies, but file descriptors such as sockets and database connections are the same objects in both processes, which is where forked workers break.

for a principal

Weigh whether forking from PHP is the right tool at all versus separate processes started by a supervisor, given pcntl is CLI-only, Unix-only and must be explicitly compiled in.

## What `pcntl_fork()` does `pcntl_fork()` is PHP's wrapper around the Unix `fork()` system call, provided by the **pcntl** (process control) extension. It takes no arguments and has the signature `pcntl_fork(): int` in `ext/pcntl/pcntl.stub.php`. When it succeeds, the operating system creates a second process that is an almost exact copy of the first: the same script, the same position in the script, and a copy of every variable, object and open file descriptor. The unusual part is that **one call returns twice** — once in each process. The only way the code can tell which copy it is running in is the return value. ## The three possible return values | Return value | Where you are | What it means | |---|---|---| | a positive `int` | the parent | the fork worked; the value is the child's process ID (PID) | | `0` | the child | you are the new process | | `-1` | the parent | the fork failed, no child exists, and PHP raised an `E_WARNING` | The warning text on failure depends on the reason the operating system gave. The pinned source prints, for example on Linux, `Error 11: Reached the maximum limit of number of processes` when the per-user process limit is hit, or `Insufficient memory` when the kernel could not allocate the copy. The last error number is also stored and can be read with `pcntl_get_last_error()` and turned into text with `pcntl_strerror()`. Note that the parent does **not** get `false` on failure: the return type is `int`, so the check is `=== -1`, not `=== false`. ## Branching on the result The standard shape is a three-way branch: 1. `$pid === -1` — log the failure and decide whether to retry or stop. 2. `$pid === 0` — run the child's work, then call `exit()` with a status code. 3. otherwise — you are the parent; store `$pid` so you can wait for that child later. The `exit()` in the child branch is not optional. Without it the child finishes its work, leaves the `if` block and keeps executing whatever the parent would execute next. If the fork sits inside a loop, the child starts forking children of its own, and the process count grows geometrically. ## Identifying yourself after the fork The return value tells the parent who the child is, but the child receives only `0`. Two functions from the **posix** extension fill the gap: - `posix_getpid(): int` returns the calling process's own PID — in the child this is the same number the parent received from `pcntl_fork()`. - `posix_getppid(): int` returns the parent's PID, which a child can use to check whether its parent is still the process that started it. - `getmypid()` from the standard library also reads the current PID, so code without the posix extension can still log it. ## What the child inherits The child starts with **copies** of the parent's state, not shared access to it: - every PHP variable and object: changing `$counter` in the child does not change it in the parent; - open file descriptors, including sockets and database connections, which are the same underlying descriptors and are therefore genuinely shared; - installed `pcntl_signal()` handlers; - the output buffer, so text buffered with `ob_start()` before the fork can be printed twice. That split between copied memory and shared descriptors is where most real bugs with forking come from. ## Where it works - **CLI only.** The build system compiles pcntl into the CLI and CGI binaries (`--enable-pcntl`, described as "CLI/CGI only"), and the manual says process control should not be enabled in a web server environment. Forking a PHP-FPM worker in the middle of a request is not a supported design. - **Not on Windows.** The extension does not exist there. - **Must be enabled.** It is not part of a default build, so a portable script checks `extension_loaded('pcntl')` or `function_exists('pcntl_fork')` first. ## A short example ```php <?php $pid = pcntl_fork(); if ($pid === -1) { exit(1); } if ($pid === 0) { echo 'child ', posix_getpid(), PHP_EOL; exit(0); } pcntl_waitpid($pid, $status); ``` The parent's final call waits for the child to finish; reaping children properly is its own topic, but a parent should never simply forget the PIDs it created.

  • What happens if the child branch has no exit() and the fork is inside a foreach loop?
    The child finishes its work and continues the loop exactly as the parent would, so it calls `pcntl_fork()` on the next iteration and creates grandchildren. Each of those does the same, so three iterations can produce up to eight processes instead of four. Ending the child branch with `exit()` is what keeps one fork equal to one worker.
  • Can a PHP-FPM request call pcntl_fork() to run a slow task in the background?
    No, not as a design. pcntl is intended for CLI scripts; a static build only compiles it into CLI and CGI, and the manual warns that using it inside a web server gives unexpected results. A forked FPM worker would share the request's socket and output. Hand slow work to a queue consumed by a CLI worker instead.

saying these in an interview costs you the question

  • pcntl_fork() returns false when the fork fails
  • The child receives its own PID from pcntl_fork()
  • The child shares the parent's variables, so changes are visible to both
  • Only the child continues after the call; the parent waits
  • pcntl_fork() works the same inside a PHP-FPM web request