PHP多進程系列筆記(二)


上一篇文章講解了pcntl_forkpcntl_wait兩個函數的使用,本篇繼續講解PHP多進程相關新知識。

僵屍(zombie)進程

這里說下僵屍進程:

僵屍進程是指的父進程已經退出,而該進程dead之后沒有進程接受,就成為僵屍進程(zombie)進程。任何進程在退出前(使用exit退出) 都會變成僵屍進程(用於保存進程的狀態等信息),然后由init進程接管。如果不及時回收僵屍進程,那么它在系統中就會占用一個進程表項,如果這種僵屍進程過多,最后系統就沒有可以用的進程表項,於是也無法再運行其它的程序。

通過如下命令查看是否有僵屍進程,如果有,類似下面這樣:

$ ps -A -o stat,ppid,pid,cmd | grep -e '^[Zz]'
Z+     282   283 [php] <defunct>

或者使用top命令查看:

$ top

top - 08:26:38 up 57 min,  1 user,  load average: 0.29, 0.37, 0.41
Tasks: 274 total,   1 running, 218 sleeping,   0 stopped,   1 zombie

也會顯示 1 zombie

什么情況下會出現僵屍進程呢?

  • 如果子進程還沒有結束時,父進程就結束了,那么init進程會自動接手這個子進程,進行回收。
  • 如果父進程是循環,又沒有安裝SIGCHLD信號處理函數調用waitwaitpid()等待子進程結束。那么子進程結束后,沒有回收,就產生僵屍進程了。

示例:
fork_zombie.php

<?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){sleep(3);} //#1
}else{
    $id = getmypid();   
    echo "Child process,pid {$id}\n";   
    sleep(2); 
    exit();
}

命令行里運行程序,然后新終端查看:

$ ps -A -o stat,ppid,pid,cmd | grep -e '^[Zz]'
Z+    7252  7253 [php] <defunct>

出現了一個僵屍進程。這時候就算手動結束腳本程序也無法關閉這個僵屍子進程了。需要使用kill -9關閉。

pcntl_signal

bool pcntl_signal ( int $signo , callback $handler [, bool $restart_syscalls = true ] )

該函數為signo指定的信號安裝一個新的信號處理器。

安裝SIGCHLD信號

上一節里,我們講到僵屍進程產生的原因:

如果父進程是循環,又沒有安裝SIGCHLD信號處理函數調用waitwaitpid()等待子進程結束。那么子進程結束后,沒有回收,就產生僵屍進程了。

本小節我們通過安裝SIGCHLD信號處理函數來解決僵屍進程問題。示例:


<?php 

//表示每執行一條低級指令,就檢查一次信號,如果檢測到注冊的信號,就調用其信號處理器
declare(ticks = 1);

//安裝SIGCHLD信號
pcntl_signal(SIGCHLD, function(){
    echo "SIGCHLD \r\n";
    pcntl_wait($status);
}); //#2

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

    //先sleep一下,否則代碼一直循環,無法處理信號接收
    while(1){sleep(3);} //#1
}else{
    $id = getmypid();   
    echo "Child process,pid {$id}\n";   
    sleep(2); 
    exit();
}

第一次注釋掉#1#2處的代碼,父進程提前結束,子進程被init進程接手,所以沒有產生僵屍進程。
第二次我們注釋掉#2處的代碼,開啟#1處的代碼,即父進程是個死循環,又沒有回收子進程,就產生僵屍進程了。
第三次我們開啟#1處和#2處的代碼,父進程由於安裝了信號處理,並調用wait函數等待子進程結束,所以也沒有產生僵屍進程。

對子進程的結束不感興趣
如果父進程不關心子進程什么時候結束,那么可以用pcntl_signal(SIGCHLD, SIG_IGN)通知內核,自己對子進程的結束不感興趣,那么子進程結束后,內核會回收,並不再給父進程發送信號。這樣我們就不寫子進程退出的處理函數了。

說明:

如果去掉declare( ticks = 1 );無法響應信號。因php的信號處理函數是基於ticks來實現的,而不是注冊到真正系統底層的信號處理函數中。

安裝其他信號

我們可以在主進程安裝更多信號,例如:

<?php
declare( ticks  =  1 );

//信號處理函數
function  sig_handler ( $signo )
{

     switch ( $signo ) {
         case  SIGTERM :
              // 處理SIGTERM信號
              exit;
             break;
         case  SIGHUP :
              //處理SIGHUP信號
              break;
         case  SIGUSR1 :
             echo  "Caught SIGUSR1...\n" ;
             break;
         default:
              // 處理所有其他信號
      }

}

echo  "Installing signal handler...\n" ;

//安裝信號處理器
pcntl_signal ( SIGTERM ,  "sig_handler" );
pcntl_signal ( SIGHUP ,   "sig_handler" );
pcntl_signal ( SIGUSR1 ,  "sig_handler" );

echo  "Generating signal SIGTERM to self...\n" ;

//向當前進程發送SIGUSR1信號
posix_kill ( posix_getpid (),  SIGUSR1 );

echo  "Done\n"

注:通過 kill -l 可以看到Linux下所有的信號常量。

$ kill -l
 1) SIGHUP	 2) SIGINT	 3) SIGQUIT	 4) SIGILL	 5) SIGTRAP
 6) SIGABRT	 7) SIGBUS	 8) SIGFPE	 9) SIGKILL	10) SIGUSR1
11) SIGSEGV	12) SIGUSR2	13) SIGPIPE	14) SIGALRM	15) SIGTERM
16) SIGSTKFLT	17) SIGCHLD	18) SIGCONT	19) SIGSTOP	20) SIGTSTP
21) SIGTTIN	22) SIGTTOU	23) SIGURG	24) SIGXCPU	25) SIGXFSZ
26) SIGVTALRM	27) SIGPROF	28) SIGWINCH	29) SIGIO	30) SIGPWR
31) SIGSYS	34) SIGRTMIN	35) SIGRTMIN+1	36) SIGRTMIN+2	37) SIGRTMIN+3
38) SIGRTMIN+4	39) SIGRTMIN+5	40) SIGRTMIN+6	41) SIGRTMIN+7	42) SIGRTMIN+8
43) SIGRTMIN+9	44) SIGRTMIN+10	45) SIGRTMIN+11	46) SIGRTMIN+12	47) SIGRTMIN+13
48) SIGRTMIN+14	49) SIGRTMIN+15	50) SIGRTMAX-14	51) SIGRTMAX-13	52) SIGRTMAX-12
53) SIGRTMAX-11	54) SIGRTMAX-10	55) SIGRTMAX-9	56) SIGRTMAX-8	57) SIGRTMAX-7
58) SIGRTMAX-6	59) SIGRTMAX-5	60) SIGRTMAX-4	61) SIGRTMAX-3	62) SIGRTMAX-2
63) SIGRTMAX-1	64) SIGRTMAX	

防盜版聲明:本文系原創文章,發布於公眾號飛鴻影的博客(fhyblog)博客園,轉載需作者同意。


ticks相關

PHP的 ticks=1 表示每執行1行PHP代碼就回調此函數(指的pcntl_signal_dispatch)。實際上大部分時間都沒有信號產生,但ticks的函數一直會執行。如果一個服務器程序1秒中接收1000次請求,平均每個請求要執行1000行PHP代碼。那么PHP的pcntl_signal,就帶來了額外的 1000 * 1000,也就是100萬次空的函數調用。這樣會浪費大量的CPU資源。
(摘自:韓天峰(Rango)的博客 » PHP官方的pcntl_signal性能極差
http://rango.swoole.com/archives/364)

pcntl_signal_dispatch的作用就是查看是否收到了信號需要處理,如果有信號的話,就調用相應的信號處理函數。

所以上述問題比較好的做法是去掉ticks,轉而手動調用pcntl_signal_dispatch,在代碼循環中自行處理信號。

我們把上一小節的例子改改,不使用ticks:

<?php 

//declare( ticks  =  1 );

//信號處理函數
function  sig_handler ( $signo )
{

     switch ( $signo ) {
         case  SIGUSR1 :
             echo  "Caught SIGUSR1...\n" ;
             break;
         default:
              // 處理所有其他信號
      }

}

echo  "Installing signal handler...\n" ;

//安裝信號處理器
pcntl_signal ( SIGUSR1 ,  "sig_handler" );

echo  "Generating signal SIGTERM to self...\n" ;

//向當前進程發送SIGUSR1信號
posix_kill ( posix_getpid (),  SIGUSR1 );
pcntl_signal_dispatch();

echo  "Done\n";

運行結果:

Installing signal handler...
Generating signal SIGTERM to self...
Caught SIGUSR1...
Done

相比每執行一條php語句都會調用 pcntl_signal_dispatch 一次,效率好多了。

pcntl_alarm

int pcntl_alarm ( int $seconds )

該函數創建一個計時器,在指定的秒數后向進程發送一個 SIGALRM 信號。每次對 pcntl_alarm() 的調用都會取消之前設置的alarm信號。注意不是定時器,只會運行一次。

下面是一個隔5秒發送一個SIGALRM信號,並由signal_handler函數獲取,然后打印一個 SIGALRM 的例子:

<?php 
declare(ticks = 1);

//安裝SIGALRM信號
pcntl_signal(SIGALRM, function(){
    echo "SIGALRM\n";
    pcntl_alarm(5);  //再次調用,會重新發送一個SIGALRM信號
});
pcntl_alarm(5);//發送一個SIGALRM信號

echo "run...\n";

//死循環,否則進程會退出
while(1){sleep(1);}

注:如果不想使用ticks,那么需要在主循環里主動增加pcntl_signal_dispatch()調用。

(未完待續)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM