Reputation: 3259
I've got this block of code which works perfectly for my needs in my various php cli programs. Except that sometimes a child will become a zombie.
My question is where to place code to check if a child runs say for 5 minutes and if it's longer then to kill it?
I know about posix_kill to kill it and how to keep track of it. There are examples of taskmanagers here.
I am unsure how to combine these new features into the code. Everytime I attempt to, I just get into a mess. Maybe someone who knows about forking can fix my code up?
Ignore all the error_logs - I like to see what is happening when it runs.
public function __construct($data) {
//Keep track of all of the children processes
$this->children = Array();
//Specify the maximum number of child processes to fork at any given time
$this->max_children = 5;
}
private function process()
{
foreach ($collection as $stuff)
{
//FORK THE PROCESS
$pid = pcntl_fork();
//Something went wrong
if($pid == -1)
{
error_log ("could not fork");
die ();
}
//PARENT PROCESS
if($pid)
{
//error_log ("Parent: forked " . $pid);
$this->children[] = $pid;
}
//CHILD PROCESS
else
{
// Do stuff here
exit(); //Exit the child thread so it doesn't continue to process the data
}
//COLLECT ALL OF THE CHILDREN AS THEY FINISH
while( ($c = pcntl_wait($status, WNOHANG OR WUNTRACED) ) > 0)
{
//error_log ("Collected Child - " . $c);
$this->remove_thread($this->children, $c);
error_log ("children left: " . count($this->children));
}
//WAIT FOR A CHILD TO FINISH IF MAXIMUM PROCESSES IS EXCEEDED
if(sizeof($this->children) > $this->max_children)
{
//error_log ("Maximum children exceeded. Waiting...");
if( ($c = pcntl_wait($status, WUNTRACED) ) > 0)
{
//error_log ("Waited for Child - " . $c);
$this->remove_thread($this->children, $c);
error_log ("children left: " . count($this->children));
}
}
}
//COLLECT ALL OF THE CHILDREN PROCESSES BEFORE PROCEEDING
while( ($c = pcntl_wait($status, WUNTRACED) ) > 0){
//error_log ("Child Finished - " . $c);
$this->remove_thread($this->children, $c);
error_log ("children left: " . count($this->children));
}
}
//Function to remove elements from an array
private function remove_thread(&$Array, $Element)
{
for($i = 0; $i < sizeof($Array); $i++)
{
//Found the element to remove
if($Array[$i] == $Element){
unset($Array[$i]);
$Array = array_values($Array);
break;
}
}
}
Upvotes: 6
Views: 3515
Reputation: 12045
Here's what worked for me in getting rid of zombie processes... children can even talk to stdin, their zombies will get killed during termination (SIGCHLD). No waiting for anything, totally async.
<?php
declare(ticks = 1); // cpu directive
$max=10;
$child=0;
$children = array();
function sig_handler($signo) { // we release zombie souls in here, optimal place - shot exactly after childs death.
global $child,$children;
switch ($signo) {
case SIGCHLD:
$child -= 1;
foreach($children as $pid){
$res = pcntl_waitpid($pid,$status, WNOHANG | WUNTRACED);
if($res != 0) unset($children[$pid]);
}
}
}
pcntl_signal(SIGCHLD, "sig_handler"); // register fork signal handler to count running children
while (true){ // <main_loop> - could be whatever you want, for, while, foreach... etc.
while ($child >= $max) {
sleep(1);
}
$child++;
$pid=pcntl_fork();
if($pid == -1){
}else if($pid){
$children[$pid] = $pid; // register new born children
}else{ // <fork>
echo "HELLO DADDY! I'M ALIVE! I CAN DO WHATEVER YOU WANT, DAD.";
sleep(1); // avoid segmentation fault, when fork ends before handling signals
exit(0);
} // </fork>
// optional timer between child spawn, avoiding wakeup on SIGCHLD
$timeLeft = 5; // 5 seconds
while ($timeLeft > 0) {
$timeLeft = sleep($timeLeft);
}
} // </main_loop>
while($child != 0){
sleep(1);
}
?>
Timer has to be implemented that way, because SIGCHLD wakes up every sleep(). Creds to SztupY for info about that and an idea how to avoid it.
Upvotes: 0
Reputation: 70540
First of all: WNOHANG OR WUNTRACED
equals (bool true), WNOHANG | WUNTRACED
is int (3), makes all lot of difference, although not necessarily here.
//set maximum child time.
$maxruntime = 300;
//.....
//..... skip a lot of code, prefer trigger_error($msg,E_USER_ERROR) above die($msg) though
//.....
//if we are the parent
if($pid)
{
//store the time it started
$this->children[$pid] = time();
}
//.....
//..... skip
//.....
//COLLECT ALL OF THE CHILDREN AS THEY FINISH
while(count($this->children) > 0){
//copy array as we will unset $this->children items:
$children = $this->children;
foreach($children as $pid => $starttime){
$check = pcnt_waitpid($pid, $status, WNOHANG | WUNTRACED);
switch($check){
case $pid:
//ended successfully
unset($this->children[$pid];
break;
case 0:
//busy, with WNOHANG
if( ( $starttime + $maxruntime ) < time() || pcntl_wifstopped( $status ) ){
if(!posix_kill($pid,SIGKILL)){
trigger_error('Failed to kill '.$pid.': '.posix_strerror(posix_get_last_error()), E_USER_WARNING);
}
unset($this->children[$pid];
}
break;
case -1:
default:
trigger_error('Something went terribly wrong with process '.$pid, E_USER_WARNING);
// unclear how to proceed: you could try a posix_kill,
// simply unsetting it from $this->children[$pid]
// or dying here with fatal error. Most likely cause would be
// $pid is not a child of this process.
break;
}
// if your processes are likely to take a long time, you might
// want to increase the time in sleep
sleep(1);
}
Upvotes: 3