1. 程式人生 > >PHP多進程系列筆記(一)

PHP多進程系列筆記(一)

could 僵屍進程 代碼 得到 擴展 系統 需要 all RR

本系列文章將向大家講解pcntl_*系列函數,從而更深入的理解進程相關知識。

PCNTL在PHP中進程控制支持默認是關閉的。您需要使用 --enable-pcntl 配置選項重新編譯PHP的 CGI或CLI版本以打開進程控制支持。

Note: 此擴展在 Windows 平臺上不可用。

pcntl_fork

int pcntl_fork ( void )

用於創建子進程。成功時,在父進程執行線程內返回產生的子進程的PID,在子進程執行線程內返回0。失敗時,在父進程上下文返回-1,不會創建子進程,並且會引發一個PHP錯誤。

fork.php

<?php 

$pid = pcntl_fork
(); if($pid == -1){ //錯誤處理:創建子進程失敗時返回-1. die( 'could not fork' ); }elseif($pid){ //父進程會得到子進程號,所以這裏是父進程執行的邏輯 $id = getmypid(); echo "Parent process,pid {$id}, child pid {$pid}\n"; }else{ //子進程得到的$pid為0, 所以這裏是子進程執行的邏輯 $id = getmypid(); echo "Child process,pid
{$id}\n"; sleep(10); }

命令行運行:

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

該例裏父進程還沒有來得及等子進程運行完畢就自動退出了,子進程由 init進程接管。通過 ps -ef | grep php 看到子進程還在運行:

[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

子進程成為孤立進程,ppid(父進程id)變成1了。如果在父進程裏也加個sleep(5),你會看到子進程ppid本來是大於1的,後來就變成1了。

註:如果是docker環境,孤立進程的ppid可能是0。

pcntl_wait

pcntl_wait()函數用來讓父進程等待子進程退出,默認情況下會阻塞主進程。

阻塞模式

緊接著上面的例子,如果想等子進程運行結束後父進程再退出,該怎麽辦?那就用到pcntl_wait了。

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

該函數阻塞當前進程,只到當前進程的一個子進程退出或者收到一個結束當前進程的信號。

我們修改代碼:

<?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); 
}

此時再次運行程序,父進程就會一直等待子進程運行結束然後退出。

pcntl_waitpid()pcntl_wait()功能相同。前者第一個參數支持指定pid參數,當指定-1作為pid的值等同於後者。

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

當已知子進程pid的時候,可以使用pcntl_waitpid()

這兩個函數返回退出的子進程進程號(>1),發生錯誤時返回-1,如果提供了 WNOHANG 作為option(wait3可用的系統)並且沒有可用子進程時返回0。

返回值為退出的子進程進程號時,想了解如何退出,可以通過 $status狀態碼反應。

非阻塞模式

pcntl_wait()默認情況下會阻塞主進程,直到子進程執行完畢才繼續往下運行。如果設置最後一個參數為常量WNOHANG,那麽就不會阻塞主進程,而是繼續執行後續代碼, 此時 pcntl_waitpid 就會返回0。

示例:

<?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);//此處為了方便看效果,實際不需要
            break;
        }
    } 
}else{
    $id = getmypid();   
    echo "Child process,pid {$id}\n";   
    sleep(2); 
}

該示例裏只有一個子進程,看不出來非阻塞的好處,我們修改一下:

<?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 故意設置時間不一樣
        exit();//#2 子進程需要exit,防止子進程也進入for循環
    }
}

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
        }
    }
    
} 

#3處首先先去掉WNOHANG參數,運行:

$ 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

我們看到,6603號進程運行時間最短,但是是最後回收。我們再加上WNOHANG參數,運行:

$ 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進程最先回收!說明確實是異步非阻塞的。感興趣的朋友還可以開啟#4處代碼,未使用WNOHANG參數的時候,裏面的代碼是不會運行的。

註意:#2處需要註意子進程需要exit,防止子進程也進入for循環。如果沒有exit(),最終創建的子進程不只3個。

檢測status函數

pcntl_waitpcntl_waitpid兩個函數中的$status中存了子進程的狀態信息,這個參數可以用於 pcntl_wifexitedpcntl_wifstoppedpcntl_wifsignaledpcntl_wexitstatuspcntl_wtermsigpcntl_wstopsigpcntl_waitpid這些函數。

代碼片段:

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

    if(!pcntl_wifexited($status)){
        //進程非正常退出
        echo "service exit unusally; pid is $pid\n";
    }else{
        //獲取進程終端的退出狀態碼;
        $code = pcntl_wexitstatus($status);
        echo "service exit code: $code;pid is $pid \n";
    }

    if(pcntl_wifsignaled($status)){
        //不是通過接受信號中斷
        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;
}

參考

1、php多進程 防止出現僵屍進程
https://www.cnblogs.com/jkko123/p/6351615.html?utm_source=itdadao&utm_medium=referral
2、PCNTL函數族--PHP多進程編程 (轉)
https://www.cnblogs.com/zox2011/archive/2013/02/19/2917448.html

PHP多進程系列筆記(一)