PHP Multiprocess Pen Series

Keywords: PHP Windows Docker

This series of articles will explain the pcntl_* series of functions, so as to better understand the process-related knowledge.

PCNTL process control support in PHP is turned off by default. You need to recompile the CGI or CLI version of PHP with the -- enable-pcntl configuration option to turn on process control support.

If the built-in PHP does not install the pcntl extension, you can download the same version of the source code and go to ext/pcntl to compile and install using phpize.

Note: This extension is not available on the Windows platform.

pcntl_fork

int pcntl_fork ( void )

Used to create subprocesses. When successful, the PID of the generated child process is returned within the parent process execution thread and 0 is returned within the child process execution thread. When it fails, it returns - 1 in the parent process context, does not create a child process, and causes a PHP error.

fork.php

<?php 

$pid = pcntl_fork();

if($pid == -1){
    //Error handling: Returns - 1 when the creation of a child process fails.
    die( 'could not fork' );
}elseif($pid){
    //The parent process will get the child process number, so here is the logic for the parent process to execute.
    $id = getmypid();   
    echo "Parent process,pid {$id}, child pid {$pid}\n";   
}else{
    //The child process gets $pid of 0, so here is the logic for the child process to execute.
    $id = getmypid();   
    echo "Child process,pid {$id}\n";   
    sleep(10); 
}

Command line run:

$ php fork.php
Parent process,pid 98, child pid 99
Child process,pid 99

In this case, the parent process automatically exits before the child process has finished running, and the child process is taken over by the init process. Through ps-ef | grep php, you can see that the child process is still running:

[root@9355490fe5da /]# ps -ef | grep php
root       105     1  0 16:46 pts/0    00:00:00 php fork.php
root       107    27  0 16:46 pts/1    00:00:00 grep php

The child process becomes an isolated process, and the ppid (parent process id) becomes 1. If you add sleep(5) to the parent process, you will see that the ppid of the child process was originally larger than 1, and then it became 1.

Note: In a docker environment, the ppid of an isolated process might be 0.

pcntl_wait

The pcntl_wait() function is used to allow the parent process to wait for the child process to exit, blocking the main process by default.

Blocking mode

Following the example above, what if you want to wait for the parent process to exit after the child process runs? Then use pcntl_wait.

int pcntl_wait ( int &$status [, int $options = 0 ] )

This function blocks the current process until only one subprocess of the current process exits or receives a signal to terminate the current process.

We modify the code:

<?php 

$pid = pcntl_fork();
if($pid == -1){
    exit("fork fail");
}elseif($pid){
    $id = getmypid();   
    echo "Parent process,pid {$id}, child pid {$pid}\n";   
    pcntl_wait($status);
    //pcntl_waitpid($pid, $status);
}else{
    $id = getmypid();   
    echo "Child process,pid {$id}\n";   
    sleep(10); 
}

When the program is run again, the parent process waits for the child process to finish running and then exits.

The functions of pcntl_waitpid() and pcntl_wait() are the same. The first parameter of the former supports specifying pid parameters, when specifying - 1 as the value of pid is equivalent to the latter.
int pcntl_waitpid ( int $pid , int &$status [, int $options = 0 ] )
When the child process pid is known, pcntl_waitpid() can be used.

These two functions return the exit child process number (> 1), return - 1 when an error occurs, and return 0 if WNOHANG is provided as an option (wait3 available system) and no child process is available.

When the return value is the exit child process number, you can respond by the $status status status code to learn how to exit.

Non-blocking mode

By default, pcntl_wait() blocks the main process until the child process has finished executing. If the last parameter is set to a constant WNOHANG, the main process will not be blocked, but the subsequent code will continue to execute, at which point pcntl_waitpid will return 0.

Example:

<?php 

$pid = pcntl_fork();
if($pid == -1){
    exit("fork fail");
}elseif($pid){
    $id = getmypid();   
    echo "Parent process,pid {$id}, child pid {$pid}\n";   

    while(1){
        $res = pcntl_wait($status, WNOHANG);
        //$res = pcntl_waitpid($pid, $status, WNOHANG);
        if ($res == -1 || $res > 0){
            sleep(10);//In order to see the effect conveniently, there is no need for it.
            break;
        }
    } 
}else{
    $id = getmypid();   
    echo "Child process,pid {$id}\n";   
    sleep(2); 
}

There is only one sub-process in this example, which does not show the benefits of non-blocking. Let's modify it:

<?php 

$child_pids = [];

for($i=0;$i<3; $i++){
    $pid = pcntl_fork();
    if($pid == -1){
        exit("fork fail");
    }elseif($pid){
        $child_pids[] = $pid;

        $id = getmypid();   
        echo time()." Parent process,pid {$id}, child pid {$pid}\n";   
    }else{
        $id = getmypid(); 
        $rand =   rand(1,3);
        echo time()." Child process,pid {$id},sleep $rand\n";   
        sleep($rand); //# 1 Deliberately setting time is different
        exit();//# 2. Subprocesses need exit to prevent subprocesses from entering the for loop
    }
}

while(count($child_pids)){
    foreach ($child_pids as $key => $pid) {
        // $res = pcntl_wait($status, WNOHANG);
        $res = pcntl_waitpid($pid, $status, WNOHANG);//#3
        if ($res == -1 || $res > 0){
            echo time()." Child process exit,pid {$pid}\n";   
            unset($child_pids[$key]);
        }else{
            // echo time()." Wait End,pid {$pid}\n";   //#4
        }
    }
    
} 

# Firstly, the WNOHANG parameters are removed and run:

$ php fork.1.php 
1528637334 Parent process,pid 6600, child pid 6601
1528637334 Child process,pid 6601,sleep 2
1528637334 Parent process,pid 6600, child pid 6602
1528637334 Child process,pid 6602,sleep 2
1528637334 Parent process,pid 6600, child pid 6603
1528637334 Child process,pid 6603,sleep 1
1528637336 Child process exit,pid 6601
1528637336 Child process exit,pid 6602
1528637336 Child process exit,pid 6603

As we can see, process 6603 runs the shortest time, but it is the last recycle. We add the WNOHANG parameter to run:

$ php fork.1.php 
1528637511 Parent process,pid 6695, child pid 6696
1528637511 Child process,pid 6696,sleep 2
1528637511 Parent process,pid 6695, child pid 6697
1528637511 Child process,pid 6697,sleep 1
1528637511 Parent process,pid 6695, child pid 6698
1528637511 Child process,pid 6698,sleep 3
1528637512 Child process exit,pid 6697
1528637513 Child process exit,pid 6696
1528637514 Child process exit,pid 6698

6697 process is the first to recycle! This means that it is asynchronous and non-blocking. Interested friends can also open # 4 code, which will not run without using the WNOHANG parameter.

Note: #2 Note that the child process needs exit to prevent the child process from entering the for loop. Without exit(), more than three child processes will eventually be created.

Detecting status function

In the $status function of pcntl_wait and pcntl_waitpid, the status information of sub-processes is stored. This parameter can be used in the functions of pcntl_wifexited, pcntl_wifstopped, pcntl_wifsignaled, pcntl_wexitstatus, pcntl_wtermsig, pcntl_wstopsig, pcntl_waitpid.

Code snippet:

 
while(1){
$res = pcntl_wait($status);
if ($res == -1 || $res > 0){

    if(!pcntl_wifexited($status)){
        // Abnormal exit of process
        echo "service exit unusally; pid is $pid\n";
    }else{
        // Get the exit status code of the process terminal.
        $code = pcntl_wexitstatus($status);
        echo "service exit code: $code;pid is $pid \n";
    }

    if(pcntl_wifsignaled($status)){
        // Not by accepting signals to interrupt
        echo "service term not by signal;pid is $pid \n";
    }else{
        $signal = pcntl_wtermsig($status);
        echo "service term by signal $signal;pid is $pid\n";
    }

    if(pcntl_wifstopped($status)){
        echo "service stop not unusally;pid is $pid \n";
    }else{
        $signal = pcntl_wstopsig($status);
        echo "service stop by signal $signal;pid is $pid\n";
    }

    break;
}

Posted by Dumps on Thu, 19 Sep 2019 23:47:04 -0700