本系列文章將向大家講解pcntl_*
系列函數,從而更深入的理解進程相關知識。
PCNTL在PHP中進程控制支持默認是關閉的。您需要使用
--enable-pcntl
配置選項重新編譯PHP的 CGI或CLI版本以打開進程控制支持。
如果自帶的PHP沒有安裝pcntl擴展,可以下載相同版本的源碼,進入ext/pcntl
使用phpize
編譯安裝。
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_wait
和pcntl_waitpid
兩個函數中的$status
中存了子進程的狀態信息,這個參數可以用於 pcntl_wifexited
、pcntl_wifstopped
、pcntl_wifsignaled
、pcntl_wexitstatus
、 pcntl_wtermsig
、pcntl_wstopsig
、pcntl_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;
}
pcntl_wifexited
— 檢查狀態代碼是否代表一個正常的退出。當子進程狀態代碼代表正常退出時返回 TRUE ,其他情況返回 FALSE 。pcntl_wexitstatus()
返回一個中斷的子進程的返回代碼。這個函數僅在函數pcntl_wifexited()
返回 TRUE 時有效。pcntl_wifsignaled
— 檢查子進程狀態碼是否代表由於某個信號而中斷。如果子進程是由於某個未捕獲的信號退出的返回 TRUE ,其他情況返回 FALSE 。cntl_wtermsig
返回導致子進程中斷的信號編號。這個函數僅在pcntl_wifsignaled()
返回 TRUE 時有效。pcntl_wifstopped
— 檢查子進程當前是否已經停止。如果子進程當前是停止的返回 TRUE ,其他情況返回 FALSE 。pcntl_wstopsig ()
返回導致子進程停止的信號編號。這個函數僅在pcntl_wifstopped()
返回 TRUE 時有效。
參考
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