PHP Forking: Kill child when it becomes a zombie
Asked Answered
O

2

6

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;  
        }
    }
}   
Oleaster answered 6/2, 2012 at 19:36 Comment(6)
Awesome title...Francescafrancesco
Children become zombies because they haven't been reaped, not because they're still alive...Smidgen
@Ignacio - there are instances when say, I am checking a proxy via curl. There will be times when that child becomes unresponsive because Curl has become unstuck and then if I am checking 1000 proxies sooner or later all my children are zombified. So that's one instance where I need to know how long they have been running for so I can kill them and make new children.Oleaster
My comment still stands.Smidgen
Sure, but in my experience. You are wrong :)Oleaster
Ignore last comment, SO didn't let me edit it. Looking at your first comment again, well DUH! I know this is the case, I want to be able to kill the children after a set period of time, say 2 minutes or whatever. You really aren't helping here.Oleaster
S
3

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);
   }
Schooner answered 7/2, 2012 at 20:1 Comment(2)
Thank you for your effort, I really appreciate it. I'll take a look at this soon and will come back.Oleaster
Note the unset($this->children[$pid]; with case 0: should be in the if statement... Just edited the answer, otherwise you would unset the whole array indiscriminately.Schooner
D
0

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.

Dominion answered 6/8, 2015 at 1:10 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.