Setting up Signal Handlers in PHP via the PCNTL extension
When running PHP through the CLI SAPI, it often becomes necessary to be able to trap UNIX signals and execute appropriate action. For example, Umgubular Slashkilter (Um-Sk) runs a parallelized crawling engine, achieved by forking itself be as need to empty out the queue. When testing Um-Sk, it was necessary to be able to gracefully kill all PHP processes without leaving the database in an inconsistent state.
The Process Control Functions extension (PCNTL) allow for signal handling easily enough on UNIX compatible platforms. I needed to catch the TERM signal which the following code achieves:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | declare(ticks = 1); // signal handler function function sig_handler($signo) { switch ($signo) { case SIGTERM: $GLOBALS["signalExit"] = TRUE; // handle shutdown tasks break; default: // handle all other signals } } // setup signal handlers pcntl_signal(SIGTERM, "sig_handler"); |
Since the parent and child processes would be executing in a loop, each iteration compares the global value signalExit and if TRUE, clean up and exit straight away. Easy enough, and with this, it is possible to use the UNIX process control utilities on your PHP scripts and have your scripts be able to react accordingly. The full range of signals that can be caught are defined in the PHP manual.
A particular signal which caught my eye was SIGCLD. Some background: Umgubular Slashkilter was becoming a messy guest on titan.gathani.org as it was leaving too many defunct child processes. These defunct child processes are processes which fork()-ed from the parent process and have exited after completing whatever they were supposed to do.
As UNIX operating systems require it, the parent process must wait() on their child processes to receive the exit status of the child process and parents who do not wait() for their child end up with zombie processes (the horror!!). It is easy enough to spot these defunct processes. Running a top -b -n1 reveals:
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
15338 ditesh 18 0 24308 3552 1204 D 0.7 1.6 0:05.51 php
5546 ditesh 17 0 23060 1804 624 S 0.0 0.8 0:01.60 php
5547 ditesh 17 0 25432 3052 920 S 0.0 1.4 0:04.48 php
5552 ditesh 18 0 22580 1120 576 S 0.0 0.5 0:11.59 php
5554 ditesh 18 0 22460 1568 828 S 0.0 0.7 0:04.65 php
5556 ditesh 16 0 0 0 0 Z 0.0 0.0 0:00.46 php <defunct>
5557 ditesh 18 0 0 0 0 Z 0.0 0.0 0:00.03 php <defunct>
5952 ditesh 18 0 0 0 0 Z 0.0 0.0 0:00.04 php <defunct>
As can clearly be seen, <defunct> processes take up no memory, no swap space, no shared libraries and certainly not any CPU power. These processes do nothing other then piss off your system administrator by filling up the process table. They are dead, as the dodo is. As top helpfully points out, these processes are labelled Z - meaning, they are Zombies in your machine! They can’t be killed; witness when running the following code:
1 2 3 4 5 6 7 | declare(ticks = 1); $pid = pcntl_fork(); if ($pid == -1) die('could not fork'); // error forking else if ($pid) sleep(100); // we are the parent else exit; // we are the child |
Then, we attempt to kill it:
[ditesh@cassini ~]$ top -b -n1 | grep php
2403 ditesh 17 0 21452 6376 3676 S 0.0 1.3 0:00.03 php
2404 ditesh 19 0 0 0 0 Z 0.0 0.0 0:00.00 php
[ditesh@cassini ~]$ kill 2404
[ditesh@cassini ~]$ top -b -n1 | grep php
2403 ditesh 17 0 21452 6376 3676 S 0.0 1.3 0:00.03 php
2404 ditesh 19 0 0 0 0 Z 0.0 0.0 0:00.00 php
[ditesh@cassini ~]$ kill -9 2404
[ditesh@cassini ~]$ top -b -n1 | grep php
2403 ditesh 17 0 21452 6376 3676 S 0.0 1.3 0:00.03 php
2404 ditesh 19 0 0 0 0 Z 0.0 0.0 0:00.00 php < defunct>
[ditesh@cassini ~]$
Clearly, zombie processes can’t be killed (d’oh). Fortunately, there is an easy workaround instead of having to wait(). The SIGCLD signal is sent to the parent process when the child exits. The parent can then catch the signal, get the exit code of the child process and allow for the UNIX grim reaper to put the child process to rest. Here is some PHP code to describe the whole shebang:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | declare(ticks = 1); // signal handler function function sig_handler($signo) { switch ($signo) { case SIGCLD: pcntl_wait($status); echo "Hallelujah - the child has exited with status $status!"; break; default: } } // setup signal handlers pcntl_signal(SIGCLD, "sig_handler"); $pid = pcntl_fork(); if ($pid == -1) die('could not fork'); // error forking else if ($pid) sleep(100); // we are the parent else exit; // we are the child |
And booyah, this shitznitz works beautifully ![]()