from the process tree to a hung PHP-FPM worker
Every Linux process carries a unique PID and points to its parent via its PPID, which naturally forms a traceable tree of parent and child processes. This article shows how to read that tree with ps and pstree, why orphaned processes get reparented to PID 1, and how to track down and terminate a hung PHP-FPM worker in practice.
Table of Contents
- 1. The Linux process model: PID and PPID
- 2. The process tree: parent-child relationships in detail
- 3. Inspecting processes with ps
- 4. Visualizing the process tree with pstree, top, and htop
- 5. Understanding process states: R, S, D, Z, T
- 6. Orphaned processes and reparenting to PID 1
- 7. Zombie processes: the difference from orphans
- 8. Practice: debugging a hung PHP-FPM worker
- 9. Process control: signals, kill, and comparing methods
- 10. Summary
- 11. FAQ
1. The Linux process model: PID and PPID
Every running process on Linux originates from the fork() system call, which duplicates an existing process and assigns the child a new, unique PID (Process ID). The kernel hands out PIDs sequentially from a bounded number space, whose upper limit lives in /proc/sys/kernel/pid_max. Internally, the kernel tracks every process through a task_struct data structure, which holds not just the PID but also state, open file descriptors, memory mappings, and resource limits.
The PPID (Parent Process ID) points to exactly the process that created the current one via fork(). This parent-child relationship is strictly hierarchical: every process except the very first has exactly one parent, never more than one. The first process after boot receives PID 1, usually systemd or another init system, and forms the root of the entire process tree. Without that root node there would be no defined starting point for the whole process hierarchy.
At runtime, every process can be inspected through its virtual directory under /proc/<pid>/. Files such as status, cmdline, and the exe symlink expose PID, PPID, the command line, and the path to the executable, with no extra tooling required. These are the exact same files that ps reads behind the scenes whenever you query a process list.
2. The process tree: parent-child relationships in detail
During fork(), the kernel does not initially copy the parent's entire memory but relies on copy-on-write: parent and child share the same memory pages until one of them actually modifies a page. Only then does duplication happen. Right after fork(), the child is almost an exact copy of the parent, with its own PID but identical program code. If the child then calls execve(), its entire memory image is replaced by a new program, while the PID stays the same. Every shell uses this fork-exec pattern whenever it launches a command.
Every process additionally belongs to a process group (PGID) and a session (SID), which matters for job control and for delivering signals to entire groups at once. This structure makes the process tree directly visible, either through the standard tool ps --forest or with pstree, which renders the hierarchy more compactly and clearly.
# Show the process tree in indented form, including PID and PPID
ps -e -o pid,ppid,pgid,sid,cmd --forest | head -n 20
# Example output (excerpt)
# PID PPID PGID SID CMD
# 1 0 1 1 /sbin/init
# 842 1 842 842 /lib/systemd/systemd-journald
# 1210 1 1210 1210 /usr/sbin/nginx -g daemon off;
# 1215 1210 1210 1210 \_ nginx: worker process
# 1340 1 1340 1340 php-fpm: master process (/etc/php/8.4/fpm/php-fpm.conf)
# 1341 1340 1340 1340 \_ php-fpm: pool www
# 1342 1340 1340 1340 \_ php-fpm: pool www
3. Inspecting processes with ps
ps supports two historically grown syntax styles: the UNIX form ps -ef and the BSD form ps aux. Both surface similar information but differ in column names and options. ps -ef shows UID, PID, PPID, start time, and command line, while ps aux additionally reports CPU and memory usage as a percentage right in the default output. For debugging purposes, the freely configurable output via -o is usually more precise, since you choose exactly the columns you actually need.
The STAT column shows the process state as a letter code, augmented with modifiers such as < for high priority or + for foreground processes of a terminal session. Combined with sorting by CPU or memory consumption via --sort, outliers can be spotted in seconds, without any interactive tool at all. This exact pattern is the entry point into every process investigation, well before reaching for specialized tools like strace.
# Custom columns: pid, ppid, state, cpu%, mem%, elapsed time, command
ps -eo pid,ppid,stat,pcpu,pmem,etime,cmd --sort=-pcpu | head -n 10
# Find every child process of a specific parent PID
ps -eo pid,ppid,cmd --ppid 1340
# Show only processes in uninterruptible sleep (D) or zombie (Z) state
ps -eo pid,ppid,stat,cmd | awk '$3 ~ /D|Z/'
4. Visualizing the process tree with pstree, top, and htop
pstree collapses identical sibling processes by default and shows them as a count, for instance php-fpm: pool www---7*[php-fpm: pool www]. The -p flag additionally displays PIDs, and -a shows the full command lines including arguments. That makes pstree -p <pid> the fastest way to see how many child processes a given service is currently holding open, without having to scan the entire system list by hand.
For ongoing observation, top and htop are better suited because they refresh periodically. In htop, pressing t toggles between a flat list and a tree view, so parent-child relationships can be followed in real time as CPU or memory usage changes. Both tools visually highlight processes in state D, which immediately catches the eye when hunting for blocked I/O operations, well before you have even read a single line closely.
5. Understanding process states: R, S, D, Z, T
The STAT column of ps encodes each process's state as a single letter. R (Running) means the process is actively executing on the CPU or sitting runnable in the scheduler queue. S (Interruptible Sleep) is the most common idle state, occurring for instance when a process waits on network I/O or a timer event and can be woken by a signal at any time. T (Stopped) occurs when a process has been paused via SIGSTOP or halted by a debugger through ptrace.
More critical is D (Uninterruptible Sleep): the process is waiting on a kernel operation, usually disk or NFS I/O, and in this state cannot be interrupted by any signal, not even SIGKILL. Many simultaneous D processes almost always point to an I/O bottleneck at the storage backend, not a problem in the application itself. Z (Zombie), finally, denotes a process that has already terminated but whose exit status has not yet been collected by its parent, covered in more detail in the next section.
6. Orphaned processes and reparenting to PID 1
A process is considered orphaned as soon as its original parent terminates while the child itself keeps running. The kernel never allows a PPID gap: every orphaned process is automatically reparented, that is, assigned to a new parent. Historically that was always PID 1. Since Linux 3.4, any process can mark itself as a subreaper with PR_SET_CHILD_SUBREAPER, so that, for instance, a container init process or a process supervisor catches orphaned grandchild processes before they are passed all the way up to PID 1. Docker init systems such as tini rely on exactly this behavior.
In practice, reparenting is frequently used deliberately: a process started with nohup command & survives closing the terminal session because it is decoupled from SIGHUP. Once the shell exits, ps -o pid,ppid for that process immediately shows PPID 1 as soon as reparenting has completed. That is not a bug, it is exactly the intended behavior for long-running background services.
# Start a long-running process detached from the terminal session
nohup sleep 600 &
child_pid=$!
ps -o pid,ppid,cmd -p "$child_pid"
# PID PPID CMD
# 4821 4711 sleep 600 <- PPID is still the current shell
# Close the terminal / kill the parent shell, then check again
kill -HUP 4711
sleep 1
ps -o pid,ppid,cmd -p 4821
# PID PPID CMD
# 4821 1 sleep 600 <- reparented to PID 1 (init/systemd)
7. Zombie processes: the difference from orphans
A zombie process is by no means the same as an orphaned process, even though the two terms are frequently confused. A zombie has already fully terminated and no longer executes any code, holds no more memory, and has no open file descriptors left. All that remains is an entry in the kernel's process table carrying the exit code and resource usage statistics, for as long as the parent process has not collected that status via wait() or waitpid(). In ps, a zombie shows up with state Z and the suffix <defunct> after the command name.
Because a zombie is already dead, it cannot be terminated by any signal, even kill -9 has no effect. The only fix is getting the parent process to collect the exit status, either through a code fix that correctly calls waitpid(), or pragmatically by restarting the parent. If the parent terminates, the kernel reparents the zombie to PID 1, and systemd automatically calls wait() for that orphaned zombie, which makes the entry disappear.
8. Practice: debugging a hung PHP-FPM worker
A typical symptom in production: the website responds with a 504 Gateway Timeout even though nginx and the database are fundamentally reachable. The most common cause is a PHP-FPM pool that has hit its pm.max_children limit because individual workers are stuck in a blocking operation, for example a slow external API or a locked database row. The first step is querying the pool status and identifying worker PIDs with unusually long runtimes.
ps -eo pid,ppid,etime,pcpu,cmd | grep php-fpm immediately surfaces workers with a suspiciously long runtime (etime), while fresh requests normally finish in milliseconds. For the actual root-cause analysis, attach to the hung worker with strace -p <pid> and see straight away which system call it is blocked on, for instance recvfrom() for a hung network connection or flock() for a file-lock conflict.
If strace is not enough, gdb -p <pid> -batch -ex "bt" yields a PHP-internal backtrace, provided debug symbols are available. That usually pinpoints the exact line of code where the worker is stuck. As a preventive measure, request_slowlog_timeout belongs in every production configuration, so FPM automatically logs suspiciously long requests before they turn into a manual debugging session.
; /etc/php/8.4/fpm/pool.d/www.conf
; Pool tuning to detect and contain runaway workers early
[www]
pm = dynamic
pm.max_children = 12
pm.start_servers = 4
pm.min_spare_servers = 2
pm.max_spare_servers = 6
pm.max_requests = 500
; Log any request that runs longer than 5 seconds, with a full backtrace
request_slowlog_timeout = 5s
slowlog = /var/log/php-fpm/www-slow.log
; Hard kill a worker that exceeds the configured wall-clock limit
request_terminate_timeout = 30s
; Emit the FPM status page for automated monitoring
pm.status_path = /fpm-status
9. Process control: signals, kill, and comparing methods
Despite its name, the kill command does not necessarily send a termination signal, it can send any POSIX signal. SIGTERM (the default signal for kill without options) asks the process for an orderly shutdown, which it can catch and handle cleanly, for example closing open files and tearing down connections. SIGKILL (kill -9), by contrast, is handled directly by the kernel without informing the process at all, and can therefore leave it in an inconsistent state, such as half-written files or orphaned locks.
For an automated watchdog that detects and controllably terminates hung PHP-FPM workers, a structured audit log is worthwhile, so every intervention decision remains traceable afterward instead of leaving just a single line in syslog.
{
"timestamp": "2026-07-12T09:41:18Z",
"watchdog": "fpm-worker-guard",
"event": "runaway_worker_terminated",
"pid": 18422,
"ppid": 1340,
"pool": "www",
"state_before": "R",
"etime_seconds": 47,
"signal_sequence": ["SIGTERM", "SIGKILL"],
"reason": "exceeded request_terminate_timeout",
"stack_hint": "blocked in mysqli_query() on locked row"
}
To terminate not just a single process but an entire process group, for instance a shell script along with every child process it started, kill with a negative PID targets the whole group: kill -- -PGID. The table below contrasts common but risky approaches with the recommended alternatives.
| Task | Risky / imprecise | Recommended approach | Benefit |
|---|---|---|---|
| Terminate a process | kill -9 immediately |
kill -TERM, then -9 after a timeout |
Orderly cleanup, no corrupted files |
| Restart PHP-FPM | systemctl restart php-fpm |
pm.max_requests + reload |
No full outage across all pools |
| "Remove" a zombie process | kill -9 <zombie-pid> |
Fix or restart the parent process | Zombies are already dead, signals have no effect |
| Stop an entire process tree | Kill only the main PID | kill -- -PGID or pkill -P |
No orphaned child processes left behind |
| Find a hung worker | Check CPU usage only | Filter ps -eo stat,etime,ppid for D/Z |
Also catches blocked I/O processes |
Mironsoft
Server administration, debugging, and deployment infrastructure for Magento stores
PHP-FPM workers hanging, server running hot?
We analyze process trees, PHP-FPM pools, and database locks in your production environment, find the root cause of hung workers, and set up monitoring that automatically detects and documents such incidents going forward.
Server Diagnostics
Process tree analysis with ps, pstree, and strace during acute performance issues
PHP-FPM Tuning
Pool configuration, slowlogs, and timeouts tuned to your traffic
Monitoring Setup
Automated detection of hung workers and structured audit logs
10. Summary
Every Linux process carries a unique PID and a PPID that ties it to its parent, which makes it possible to trace the entire process tree back to PID 1. Tools like ps --forest and pstree -p make that hierarchy visible, while top and htop let you observe it in real time. If a parent process dies before its child, the kernel automatically reparents the child to PID 1 or to a registered subreaper, rather than leaving a gap in the tree.
The D state signals blocked I/O and cannot be interrupted by any signal, while a zombie has already fully terminated and is only waiting on its parent's wait() call. In production practice, the combination of ps -eo stat,etime,ppid, strace -p, and a properly configured request_slowlog_timeout is the most reliable way to find hung PHP-FPM workers before they cause a full outage.
Linux Processes: PID, PPID, Process Tree: Key Takeaways
PID & PPID
Every process has exactly one PID and exactly one PPID. PID 1 (systemd or init) is the root of the entire process tree.
Reading the process tree
pstree -p or ps --forest instantly visualize parent-child relationships, no manual PID mapping needed.
Orphans & zombies
Orphans get reparented to PID 1 and keep running. Zombies are already dead and just waiting for wait() from the parent.
PHP-FPM debugging
ps -eo pid,ppid,etime,stat,cmd, then strace -p or gdb -p for the hung worker's backtrace.