溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

PHP多進(jìn)程、信號(hào)量及孤兒進(jìn)程和僵尸進(jìn)程是什么

發(fā)布時(shí)間:2020-11-10 12:00:55 來(lái)源:億速云 閱讀:257 作者:小新 欄目:編程語(yǔ)言

PHP多進(jìn)程、信號(hào)量及孤兒進(jìn)程和僵尸進(jìn)程是什么?這個(gè)問(wèn)題可能是我們?nèi)粘W(xué)習(xí)或工作經(jīng)常見(jiàn)到的。希望通過(guò)這個(gè)問(wèn)題能讓你收獲頗深。下面是小編給大家?guī)?lái)的參考內(nèi)容,讓我們一起來(lái)看看吧!

PHP多進(jìn)程、信號(hào)量及孤兒進(jìn)程和僵尸進(jìn)程

實(shí)際上PHP是有多線程的,只是很多人不常用。使用PHP的多線程首先需要下載安裝一個(gè)線程安全版本(ZTS版本)的PHP,然后再安裝pecl的 pthread 擴(kuò)展。

實(shí)際上PHP是有多進(jìn)程的,有一些人再用,總體來(lái)說(shuō)php的多進(jìn)程還算湊合,只需要在安裝PHP的時(shí)候開(kāi)啟pcntl模塊(是不是跟UNIX中的fcntl有點(diǎn)兒…. ….)即可。在*NIX下,在終端命令行下使用php -m就可以看到是否開(kāi)啟了pcntl模塊。

所以我們只說(shuō)php的多進(jìn)程,至于php多線程就暫時(shí)放到一邊兒。

注意:不要在apache或者fpm環(huán)境下使用php多進(jìn)程,這將會(huì)產(chǎn)生不可預(yù)估的后果。

PHP多進(jìn)程初探

進(jìn)程是程序執(zhí)行的實(shí)例,舉個(gè)例子有個(gè)程序叫做 “ 病毒.exe ”,這個(gè)程序平時(shí)是以文件形式存儲(chǔ)在硬盤(pán)上,當(dāng)你雙擊運(yùn)行后,就會(huì)形成一個(gè)該程序的進(jìn)程。系統(tǒng)會(huì)給每一個(gè)進(jìn)程分配一個(gè)唯一的非負(fù)整數(shù)用來(lái)標(biāo)記進(jìn)程,這個(gè)數(shù)字稱(chēng)作進(jìn)程ID。當(dāng)該進(jìn)程被殺死或終止后,其進(jìn)程ID就會(huì)被系統(tǒng)回收,然后分配給新的其余的進(jìn)程。

說(shuō)了這么多,這鬼東西有什么用嗎?我平時(shí)用CI、YII寫(xiě)個(gè)CURD跟這個(gè)也沒(méi)啥關(guān)聯(lián)啊。實(shí)際上,如果你了解APACHE PHP MOD或者FPM就知道這些東西就是多進(jìn)程實(shí)現(xiàn)的。以FPM為例,一般都是nginx作為http服務(wù)器擋在最前面,靜態(tài)文件請(qǐng)求則nginx自行處理,遇到php動(dòng)態(tài)請(qǐng)求則轉(zhuǎn)發(fā)給php-fpm進(jìn)程來(lái)處理。如果你的php-fpm配置只開(kāi)了5個(gè)進(jìn)程,如果處理任意一個(gè)用戶的請(qǐng)求都需要1秒鐘,那么5個(gè)fpm進(jìn)程1秒中就最多只能處5個(gè)用戶的請(qǐng)求。所以結(jié)論就是:如果要單位時(shí)間內(nèi)干活更快更多,就需要更多的進(jìn)程,總之一句話就是多進(jìn)程可以加快任務(wù)處理速度。

在php中我們使用pcntl_fork()來(lái)創(chuàng)建多進(jìn)程(在*NIX系統(tǒng)的C語(yǔ)言編程中,已有進(jìn)程通過(guò)調(diào)用fork函數(shù)來(lái)產(chǎn)生新的進(jìn)程)。fork出來(lái)新進(jìn)程則成為子進(jìn)程,原進(jìn)程則成為父進(jìn)程,子進(jìn)程擁有父進(jìn)程的副本。這里要注意:

  • 子進(jìn)程與父進(jìn)程共享程序正文段

  • 子進(jìn)程擁有父進(jìn)程的數(shù)據(jù)空間和堆、棧的副本,注意是副本,不是共享

  • 父進(jìn)程和子進(jìn)程將繼續(xù)執(zhí)行fork之后的程序代碼

  • fork之后,是父進(jìn)程先執(zhí)行還是子進(jìn)程先執(zhí)行無(wú)法確認(rèn),取決于系統(tǒng)調(diào)度(取決于信仰)

這里說(shuō)子進(jìn)程擁有父進(jìn)程數(shù)據(jù)空間以及堆、棧的副本,實(shí)際上,在大多數(shù)的實(shí)現(xiàn)中也并不是真正的完全副本。更多是采用了COW(Copy On Write)即寫(xiě)時(shí)復(fù)制的技術(shù)來(lái)節(jié)約存儲(chǔ)空間。簡(jiǎn)單來(lái)說(shuō),如果父進(jìn)程和子進(jìn)程都不修改這些 數(shù)據(jù)、堆、棧 的話,那么父進(jìn)程和子進(jìn)程則是暫時(shí)共享同一份 數(shù)據(jù)、堆、棧。只有當(dāng)父進(jìn)程或者子進(jìn)程試圖對(duì) 數(shù)據(jù)、堆、棧 進(jìn)行修改的時(shí)候,才會(huì)產(chǎn)生復(fù)制操作,這就叫做寫(xiě)時(shí)復(fù)制。

在調(diào)用完pcntl_fork()后,該函數(shù)會(huì)返回兩個(gè)值。在父進(jìn)程中返回子進(jìn)程的進(jìn)程ID,在子進(jìn)程內(nèi)部本身返回?cái)?shù)字0。由于多進(jìn)程在apache或者fpm環(huán)境下無(wú)法正常運(yùn)行,所以大家一定要在php cli環(huán)境下執(zhí)行下面php代碼。

第一段代碼,我們來(lái)說(shuō)明在程序從pcntl_fork()后父進(jìn)程和子進(jìn)程將各自繼續(xù)往下執(zhí)行代碼:

$pid = pcntl_fork();
if( $pid > 0 ){
  echo "我是父親".PHP_EOL;
  } else if( 0 == $pid ) {
    echo "我是兒子".PHP_EOL;
  } else {
      echo "fork失敗".PHP_EOL;
  }

將文件保存為test.php,然后在使用cli執(zhí)行,結(jié)果如下圖所示:

第二段代碼,用來(lái)說(shuō)明子進(jìn)程擁有父進(jìn)程的數(shù)據(jù)副本,而并不是共享:

 // 初始化一個(gè) number變量 數(shù)值為1
 $number = 1;
 $pid = pcntl_fork(); if( $pid > 0 ){
   $number += 1;
   echo "我是父親,number+1 : { $number }".PHP_EOL;
 } else if( 0 == $pid ) {
   $number += 2;
   echo "我是父親,number+2 : { $number }".PHP_EOL;
 } else {   echo "fork失敗".PHP_EOL;
 }

第三段代碼,比較容易讓人思維混亂,pcntl_fork()配合for循環(huán)來(lái)做些東西,問(wèn)題來(lái)了:會(huì)顯示幾次 “ 兒子 ”?

for( $i = 1; $i <= 3 ; $i++ ){
    $pid = pcntl_fork();    if( $pid > 0 ){       // do nothing ...
    } else if( 0 == $pid ){
        echo "兒子".PHP_EOL;
    }
}

上面代碼執(zhí)行結(jié)果如下:

仔細(xì)數(shù)數(shù),竟然是顯示了7次 “ 兒子 ”。好奇怪,難道不是3次嗎?… …
下面我修改一下代碼,結(jié)合下面的代碼,再思考一下為什么會(huì)產(chǎn)生7次而不是3次。

for( $i = 1; $i <= 3 ; $i++ ){
     $pid = pcntl_fork();     if( $pid > 0 ){        // do nothing ...
     } else if( 0 == $pid ){
         echo "兒子".PHP_EOL;
         exit;
     }
 }

執(zhí)行結(jié)果如下圖所示:

前面強(qiáng)調(diào)過(guò):父進(jìn)程和子進(jìn)程將繼續(xù)執(zhí)行fork之后的程序代碼。這里就不解釋?zhuān)瑢?shí)在想不明白的,可以動(dòng)手自己畫(huà)畫(huà)思考一下。

孤兒與僵尸進(jìn)程

實(shí)際上,你們一定要記?。篜HP的多進(jìn)程是非常值得應(yīng)用于生產(chǎn)環(huán)境具備高價(jià)值的生產(chǎn)力工具。

但我認(rèn)為在正式開(kāi)始吹牛之前還是要說(shuō)兩個(gè)基本概念:孤兒進(jìn)程、僵尸進(jìn)程。

上文我整篇尬聊的都是pcntl_fork(),只管fork生產(chǎn),不管產(chǎn)后護(hù)理,實(shí)際上這樣并不符合主流價(jià)值觀,而且,操作系統(tǒng)本身資源有限,這樣無(wú)限生產(chǎn)不顧護(hù)理,操作系統(tǒng)也會(huì)吃不消的。

孤兒進(jìn)程是指父進(jìn)程在fork出子進(jìn)程后,自己先完了。這個(gè)問(wèn)題很尷尬,因?yàn)樽舆M(jìn)程從此變得無(wú)依無(wú)靠、無(wú)家可歸,變成了孤兒。用術(shù)語(yǔ)來(lái)表達(dá)就是,父進(jìn)程在子進(jìn)程結(jié)束之前提前退出,這些子進(jìn)程將由init(進(jìn)程ID為1)進(jìn)程收養(yǎng)并完成對(duì)其各種數(shù)據(jù)狀態(tài)的收集。init進(jìn)程是Linux系統(tǒng)下的奇怪進(jìn)程,這個(gè)進(jìn)程是以普通用戶權(quán)限運(yùn)行但卻具備超級(jí)權(quán)限的進(jìn)程,簡(jiǎn)單地說(shuō),這個(gè)進(jìn)程在Linux系統(tǒng)啟動(dòng)的時(shí)候做初始化工作,比如運(yùn)行g(shù)etty、比如會(huì)根據(jù)/etc/inittab中設(shè)置的運(yùn)行等級(jí)初始化系統(tǒng)等等,當(dāng)然了,還有一個(gè)作用就是如上所說(shuō)的:收養(yǎng)孤兒進(jìn)程。

僵尸進(jìn)程是指父進(jìn)程在fork出子進(jìn)程,而后子進(jìn)程在結(jié)束后,父進(jìn)程并沒(méi)有調(diào)用wait或者waitpid等完成對(duì)其清理善后工作,導(dǎo)致改子進(jìn)程進(jìn)程ID、文件描述符等依然保留在系統(tǒng)中,極大浪費(fèi)了系統(tǒng)資源。所以,僵尸進(jìn)程是對(duì)系統(tǒng)有危害的,而孤兒進(jìn)程則相對(duì)來(lái)說(shuō)沒(méi)那么嚴(yán)重。在Linux系統(tǒng)中,我們可以通過(guò)ps -aux來(lái)查看進(jìn)程,如果有[Z+]標(biāo)記就是僵尸進(jìn)程。

在PHP中,父進(jìn)程對(duì)子進(jìn)程的狀態(tài)收集等是通過(guò)pcntl_wait()和pcntl_waitpid()等完成的。依然還是要通過(guò)代碼還演示說(shuō)明:
演示并說(shuō)明孤兒進(jìn)程的出現(xiàn),并演示孤兒進(jìn)程被init進(jìn)程收養(yǎng):

$id = pcntl_fork();if( $pid > 0 ){    // 顯示父進(jìn)程的進(jìn)程ID,這個(gè)函數(shù)可以是getmypid(),也可以用posix_getpid()
    echo "Father PID:".getmypid().PHP_EOL;    // 讓父進(jìn)程停止兩秒鐘,在這兩秒內(nèi),子進(jìn)程的父進(jìn)程ID還是這個(gè)父進(jìn)程
    sleep( 2 );
} else if( 0 == $pid ) {    // 讓子進(jìn)程循環(huán)10次,每次睡眠1s,然后每秒鐘獲取一次子進(jìn)程的父進(jìn)程進(jìn)程ID
    for( $i = 1; $i <= 10; $i++ ){
        sleep( 1 );        // posix_getppid()函數(shù)的作用就是獲取當(dāng)前進(jìn)程的父進(jìn)程進(jìn)程ID
        echo posix_getppid().PHP_EOL;
    }
} else {    echo "fork error.".PHP_EOL;
}

運(yùn)行結(jié)果如下圖:

可以看到,前兩秒內(nèi),子進(jìn)程的父進(jìn)程進(jìn)程ID為4129,但是從第三秒開(kāi)始,由于父進(jìn)程已經(jīng)提前退出了,子進(jìn)程變成孤兒進(jìn)程,所以init進(jìn)程收養(yǎng)了子進(jìn)程,所以子進(jìn)程的父進(jìn)程進(jìn)程ID變成了1。(php視頻教程)

演示并說(shuō)明僵尸進(jìn)程的出現(xiàn),并演示僵尸進(jìn)程的危害:

$pid = pcntl_fork(); if( $pid > 0 ){     // 下面這個(gè)函數(shù)可以更改php進(jìn)程的名稱(chēng)
     cli_set_process_title('php father process');     // 讓主進(jìn)程休息60秒鐘
     sleep(60);
 } else if( 0 == $pid ) {
     cli_set_process_title('php child process');     // 讓子進(jìn)程休息10秒鐘,但是進(jìn)程結(jié)束后,父進(jìn)程不對(duì)子進(jìn)程做任何處理工作,這樣這個(gè)子進(jìn)程就會(huì)變成僵尸進(jìn)程
     sleep(10);
 } else {     exit('fork error.'.PHP_EOL);
 }

運(yùn)行結(jié)果如下圖:

通過(guò)執(zhí)行ps -aux命令可以看到,當(dāng)程序在前十秒內(nèi)運(yùn)行的時(shí)候,php child process的狀態(tài)列為[S+],然而在十秒鐘過(guò)后,這個(gè)狀態(tài)變成了[Z+],也就是變成了危害系統(tǒng)的僵尸進(jìn)程。

那么,問(wèn)題來(lái)了?如何避免僵尸進(jìn)程呢?PHP通過(guò)pcntl_wait()和pcntl_waitpid()兩個(gè)函數(shù)來(lái)幫我們解決這個(gè)問(wèn)題。了解Linux系統(tǒng)編程的應(yīng)該知道,看名字就知道這其實(shí)就是PHP把C語(yǔ)言中的wait()和waitpid()包裝了一下。

通過(guò)代碼演示pcntl_wait()來(lái)避免僵尸進(jìn)程,在開(kāi)始之前先簡(jiǎn)單普及一下pcntl_wait()的相關(guān)內(nèi)容:這個(gè)函數(shù)的作用就是 “ 等待或者返回子進(jìn)程的狀態(tài) ”,當(dāng)父進(jìn)程執(zhí)行了該函數(shù)后,就會(huì)阻塞掛起等待子進(jìn)程的狀態(tài)一直等到子進(jìn)程已經(jīng)由于某種原因退出或者終止。換句話說(shuō)就是如果子進(jìn)程還沒(méi)結(jié)束,那么父進(jìn)程就會(huì)一直等等等,如果子進(jìn)程已經(jīng)結(jié)束,那么父進(jìn)程就會(huì)立刻得到子進(jìn)程狀態(tài)。這個(gè)函數(shù)返回退出的子進(jìn)程的進(jìn)程ID或者失敗返回-1。

我們將第二個(gè)案例中代碼修改一下:

$pid = pcntl_fork();if( $pid > 0 ){    // 下面這個(gè)函數(shù)可以更改php進(jìn)程的名稱(chēng)
    cli_set_process_title('php father process');    // 返回$wait_result,就是子進(jìn)程的進(jìn)程號(hào),如果子進(jìn)程已經(jīng)是僵尸進(jìn)程則為0
    // 子進(jìn)程狀態(tài)則保存在了$status參數(shù)中,可以通過(guò)pcntl_wexitstatus()等一系列函數(shù)來(lái)查看$status的狀態(tài)信息是什么
    $wait_result = pcntl_wait( $status );
    print_r( $wait_result );
    print_r( $status );    // 讓主進(jìn)程休息60秒鐘    sleep(60);
} else if( 0 == $pid ) {
    cli_set_process_title('php child process');    // 讓子進(jìn)程休息10秒鐘,但是進(jìn)程結(jié)束后,父進(jìn)程不對(duì)子進(jìn)程做任何處理工作,這樣這個(gè)子進(jìn)程就會(huì)變成僵尸進(jìn)程    sleep(10);
} else {    exit('fork error.'.PHP_EOL);
}

將文件保存為wait.php,然后php wait.php,在另外一個(gè)終端中通過(guò)ps -aux查看,可以看到在前十秒內(nèi),php child process是[S+]狀態(tài),然后十秒鐘過(guò)后進(jìn)程消失了,也就是被父進(jìn)程回收了,沒(méi)有變成僵尸進(jìn)程。

但是,pcntl_wait()有個(gè)很大的問(wèn)題,就是阻塞。父進(jìn)程只能掛起等待子進(jìn)程結(jié)束或終止,在此期間父進(jìn)程什么都不能做,這并不符合多快好省原則,所以pcntl_waitpid()閃亮登場(chǎng)。pcntl_waitpid( $pid, &$status, $option = 0 )的第三個(gè)參數(shù)如果設(shè)置為WNOHANG,那么父進(jìn)程不會(huì)阻塞一直等待到有子進(jìn)程退出或終止,否則將會(huì)和pcntl_wait()的表現(xiàn)類(lèi)似。

修改第三個(gè)案例的代碼,但是,我們并不添加WNOHANG,演示說(shuō)明pcntl_waitpid()功能:

$pid = pcntl_fork(); if( $pid > 0 ){     // 下面這個(gè)函數(shù)可以更改php進(jìn)程的名稱(chēng)
     cli_set_process_title('php father process');     // 返回值保存在$wait_result中
     // $pid參數(shù)表示 子進(jìn)程的進(jìn)程ID
     // 子進(jìn)程狀態(tài)則保存在了參數(shù)$status中
     // 將第三個(gè)option參數(shù)設(shè)置為常量WNOHANG,則可以避免主進(jìn)程阻塞掛起,此處父進(jìn)程將立即返回繼續(xù)往下執(zhí)行剩下的代碼
     $wait_result = pcntl_waitpid( $pid, $status );
     var_dump( $wait_result );
     var_dump( $status );     // 讓主進(jìn)程休息60秒鐘
     sleep(60);
 } else if( 0 == $pid ) {
     cli_set_process_title('php child process');     // 讓子進(jìn)程休息10秒鐘,但是進(jìn)程結(jié)束后,父進(jìn)程不對(duì)子進(jìn)程做任何處理工作,這樣這個(gè)子進(jìn)程就會(huì)變成僵尸進(jìn)程
     sleep(10);
 } else {     exit('fork error.'.PHP_EOL);
 }

下面是運(yùn)行結(jié)果,一個(gè)執(zhí)行php程序的終端窗口,另一個(gè)是ps -aux終端窗口。實(shí)際上可以看到主進(jìn)程是被阻塞的,一直到第十秒子進(jìn)程退出了,父進(jìn)程不再阻塞:

那么我們修改第四段代碼,添加第三個(gè)參數(shù)WNOHANG,代碼如下:

$pid = pcntl_fork(); if( $pid > 0 ){     // 下面這個(gè)函數(shù)可以更改php進(jìn)程的名稱(chēng)
     cli_set_process_title('php father process');     // 返回值保存在$wait_result中
     // $pid參數(shù)表示 子進(jìn)程的進(jìn)程ID
     // 子進(jìn)程狀態(tài)則保存在了參數(shù)$status中
     // 將第三個(gè)option參數(shù)設(shè)置為常量WNOHANG,則可以避免主進(jìn)程阻塞掛起,此處父進(jìn)程將立即返回繼續(xù)往下執(zhí)行剩下的代碼
     $wait_result = pcntl_waitpid( $pid, $status, WNOHANG );
     var_dump( $wait_result );
     var_dump( $status );     echo "不阻塞,運(yùn)行到這里".PHP_EOL;     // 讓主進(jìn)程休息60秒鐘
     sleep(60);
 } else if( 0 == $pid ) {
     cli_set_process_title('php child process');     // 讓子進(jìn)程休息10秒鐘,但是進(jìn)程結(jié)束后,父進(jìn)程不對(duì)子進(jìn)程做任何處理工作,這樣這個(gè)子進(jìn)程就會(huì)變成僵尸進(jìn)程
     sleep(10);
 } else {     exit('fork error.'.PHP_EOL);
 }

下面是運(yùn)行結(jié)果,一個(gè)執(zhí)行php程序的終端窗口,另一個(gè)是ps -aux終端窗口。實(shí)際上可以看到主進(jìn)程是被阻塞的,一直到第十秒子進(jìn)程退出了,父進(jìn)程不再阻塞:

問(wèn)題出現(xiàn)了,竟然php child process進(jìn)程狀態(tài)竟然變成了[Z+],這是怎么搞得?回頭分析一下代碼:
我們看到子進(jìn)程是睡眠了十秒鐘,而父進(jìn)程在執(zhí)行pcntl_waitpid()之前沒(méi)有任何睡眠且本身不再阻塞,所以,主進(jìn)程自己先執(zhí)行下去了,而子進(jìn)程在足足十秒鐘后才結(jié)束,進(jìn)程狀態(tài)自然無(wú)法得到回收。如果我們將代碼修改一下,就是在主進(jìn)程的pcntl_waitpid()前睡眠15秒鐘,這樣就可以回收子進(jìn)程了。但是即便這樣修改,細(xì)心想的話還是會(huì)有個(gè)問(wèn)題,那就是在子進(jìn)程結(jié)束后,在父進(jìn)程執(zhí)行pcntl_waitpid()回收前,有五秒鐘的時(shí)間差,在這個(gè)時(shí)間差內(nèi),php child process也將會(huì)是僵尸進(jìn)程。那么,pcntl_waitpid()如何正確使用?。窟@樣用,看起來(lái)畢竟不太科學(xué)。

那么,是時(shí)候引入信號(hào)量了!

PHP 信號(hào)量

信號(hào)是一種軟件中斷,也是一種非常典型的異步事件處理方式。在NIX系統(tǒng)誕生的混沌之初,信號(hào)的定義是比較混亂的,而且最關(guān)鍵是不可靠,這是一個(gè)很?chē)?yán)重的問(wèn)題。所以在后來(lái)的POSIX標(biāo)準(zhǔn)中,對(duì)信號(hào)做了標(biāo)準(zhǔn)化同時(shí)也各個(gè)發(fā)行版的NIX也都提供大量可靠的信號(hào)。每種信號(hào)都有自己的名字,大概如SIGTERM、SIGHUP、SIGCHLD等等,在*NIX中,這些信號(hào)本質(zhì)上都是整形數(shù)字(游有心情的可以參觀一下signal.h系列頭文件)。

信號(hào)的產(chǎn)生是有多種方式的,下面是常見(jiàn)的幾種:

  • 鍵盤(pán)上按某些組合鍵,比如Ctrl+C或者Ctrl+D等,會(huì)產(chǎn)生SIGINT信號(hào)。

  • 使用posix kill調(diào)用,可以向某個(gè)進(jìn)程發(fā)送指定的信號(hào)。

  • 遠(yuǎn)程ssh終端情況下,如果你在服務(wù)器上執(zhí)行了一個(gè)阻塞的腳本,正在阻塞過(guò)程中你關(guān)閉了終端,可能就會(huì)產(chǎn)生SIGHUP信號(hào)。

  • 硬件也會(huì)產(chǎn)生信號(hào),比如OOM了或者遇到除0這種情況,硬件也會(huì)向進(jìn)程發(fā)送特定信號(hào)。

而進(jìn)程在收到信號(hào)后,可以有如下三種響應(yīng):

  • 直接忽略,不做任何反映。就是俗稱(chēng)的完全不鳥(niǎo)。但是有兩種信號(hào),永遠(yuǎn)不會(huì)被忽略,一個(gè)是SIGSTOP,另一個(gè)是SIGKILL,因?yàn)檫@兩個(gè)進(jìn)程提供了向內(nèi)核最后的可靠的結(jié)束進(jìn)程的辦法。

  • 捕捉信號(hào)并作出相應(yīng)的一些反應(yīng),具體響應(yīng)什么可以由用戶自己通過(guò)程序自定義。

  • 系統(tǒng)默認(rèn)響應(yīng)。大多數(shù)進(jìn)程在遇到信號(hào)后,如果用戶也沒(méi)有自定義響應(yīng),那么就會(huì)采取系統(tǒng)默認(rèn)響應(yīng),大多數(shù)的系統(tǒng)默認(rèn)響應(yīng)就是終止進(jìn)程。

用人話來(lái)表達(dá),就是說(shuō)假如你是一個(gè)進(jìn)程,你正在干活,突然施工隊(duì)的喇叭里沖你嚷了一句:“吃飯了!”,于是你就放下手里的活兒去吃飯。你正在干活,突然施工隊(duì)的喇叭里沖你嚷了一句:“發(fā)工資了!”,于是你就放下手里的活兒去領(lǐng)工資。你正在干活,突然施工隊(duì)的喇叭里沖你嚷了一句:“有人找你!”,于是你就放下手里的活兒去看看是誰(shuí)找你什么事情。當(dāng)然了,你很任性,那是完全可以不鳥(niǎo)喇叭里喊什么內(nèi)容,也就是忽略信號(hào)。也可以更任性,當(dāng)喇叭里沖你嚷“吃飯”的時(shí)候,你去就不去吃飯,你去睡覺(jué),這些都可以由你來(lái)。而你在干活過(guò)程中,從來(lái)不會(huì)因?yàn)橐饶硞€(gè)信號(hào)就不干活了一直等信號(hào),而是信號(hào)隨時(shí)隨地都可能會(huì)來(lái),而你只需要在這個(gè)時(shí)候作出相應(yīng)的回應(yīng)即可,所以說(shuō),信號(hào)是一種軟件中斷,也是一種異步的處理事件的方式。

回到上文所說(shuō)的問(wèn)題,就是子進(jìn)程在結(jié)束前,父進(jìn)程就已經(jīng)先調(diào)用了pcntl_waitpid(),導(dǎo)致子進(jìn)程在結(jié)束后依然變成了僵尸進(jìn)程。實(shí)際上在父進(jìn)程不斷while循環(huán)調(diào)用pcntl_waitpid()是個(gè)解決辦法,大概代碼如下:

$pid = pcntl_fork();if (0 > $pid) {    exit('fork error.' . PHP_EOL);
} else {    if (0 < $pid) {        // 在父進(jìn)程中
        cli_set_process_title('php father process');        // 父進(jìn)程不斷while循環(huán),去反復(fù)執(zhí)行pcntl_waitpid(),從而試圖解決已經(jīng)退出的子進(jìn)程
        while (true) {
            sleep(1);
            pcntl_waitpid($pid, &$status, WNOHANG);
        }
    } else {        if (0 == $pid) {            // 在子進(jìn)程中
            // 子進(jìn)程休眠3秒鐘后直接退出
            cli_set_process_title('php child process');
            sleep(20);            exit;
        }
    }
}

下圖是運(yùn)行結(jié)果:

解析一下這個(gè)結(jié)果,我先后三次執(zhí)行了ps -aux | grep php去查看這兩個(gè)php進(jìn)程。

  • 第一次:子進(jìn)程正在休眠中,父進(jìn)程依舊在循環(huán)中。

  • 第二次:子進(jìn)程已經(jīng)退出了,父進(jìn)程依舊在循環(huán)中,但是代碼還沒(méi)有執(zhí)行到pcntl_waitpid(),所以在子進(jìn)程退出后到父進(jìn)程執(zhí)行回收前這段空隙內(nèi)子進(jìn)程變成了僵尸進(jìn)程。

  • 第三次:此時(shí)父進(jìn)程已經(jīng)執(zhí)行了pcntl_waitpid(),將已經(jīng)退出的子進(jìn)程回收,釋放了pid等資源。

但是這樣的代碼有一個(gè)缺陷,實(shí)際上就是子進(jìn)程已經(jīng)退出的情況下,主進(jìn)程還在不斷while pcntl_waitpid()去回收子進(jìn)程,這是一件很奇怪的事情,并不符合社會(huì)主義主流價(jià)值觀,不低碳不節(jié)能,代碼也不優(yōu)雅,不好看。所以,應(yīng)該考慮用更好的方式來(lái)實(shí)現(xiàn)。那么,我們篇頭提了許久的信號(hào)終于概要出場(chǎng)了。

現(xiàn)在讓我們考慮一下,為何信號(hào)可以解決“不低碳不節(jié)能,代碼也不優(yōu)雅,不好看”的問(wèn)題。子進(jìn)程在退出的時(shí)候,會(huì)向父進(jìn)程發(fā)送一個(gè)信號(hào),叫做SIGCHLD,那么父進(jìn)程一旦收到了這個(gè)信號(hào),就可以作出相應(yīng)的回收動(dòng)作,也就是執(zhí)行pcntl_waitpid(),從而解決掉僵尸進(jìn)程,而且還顯得我們代碼優(yōu)雅好看節(jié)能環(huán)保。

梳理一下流程,子進(jìn)程向父進(jìn)程發(fā)送SIGCHLD信號(hào)是對(duì)人們來(lái)說(shuō)是透明的,也就是說(shuō)我們無(wú)須關(guān)心。但是,我們需要給父進(jìn)程安裝一個(gè)響應(yīng)SIGCHLD信號(hào)的處理器,除此之外,還需要讓這些信號(hào)處理器運(yùn)行起來(lái),安裝上了不運(yùn)行是一件尷尬的事情。那么,在php里給進(jìn)程安裝信號(hào)處理器使用的函數(shù)是pcntl_signal(),讓信號(hào)處理器跑起來(lái)的函數(shù)是pcntl_signal_dispatch()。

  • pcntl_signal(),安裝一個(gè)信號(hào)處理器,具體說(shuō)明是pcntl_signal ( int $signo , callback $handler [, bool $restart_syscalls = true ] ),參數(shù)signo就是信號(hào),callback則是響應(yīng)該信號(hào)的代碼段,返回bool值。

  • pcntl_signal_dispatch(),調(diào)用每個(gè)等待信號(hào)通過(guò)pcntl_signal() 安裝的處理器,參數(shù)為void,返回bool值。

下面結(jié)合新引入的兩個(gè)函數(shù)來(lái)解決一下樓上的丑陋代碼:

$pid = pcntl_fork();if( 0 > $pid ){    exit('fork error.'.PHP_EOL);
} else if( 0 < $pid ) {    // 在父進(jìn)程中
    // 給父進(jìn)程安裝一個(gè)SIGCHLD信號(hào)處理器
    pcntl_signal( SIGCHLD, function() use( $pid ) {        echo "收到子進(jìn)程退出".PHP_EOL;
        pcntl_waitpid( $pid, $status, WNOHANG );
    } );
    cli_set_process_title('php father process');    // 父進(jìn)程不斷while循環(huán),去反復(fù)執(zhí)行pcntl_waitpid(),從而試圖解決已經(jīng)退出的子進(jìn)程
    while( true ){
        sleep( 1 );        // 注釋掉原來(lái)老掉牙的代碼,轉(zhuǎn)而使用pcntl_signal_dispatch()
        //pcntl_waitpid( $pid, &$status, WNOHANG );
        pcntl_signal_dispatch();
    }
} else if( 0 == $pid ) {    // 在子進(jìn)程中
    // 子進(jìn)程休眠3秒鐘后直接退出
    cli_set_process_title('php child process');
    sleep( 20 );    exit;
}

感謝各位的閱讀!看完上述內(nèi)容,你們對(duì)PHP多進(jìn)程、信號(hào)量及孤兒進(jìn)程和僵尸進(jìn)程是什么大概了解了嗎?希望文章內(nèi)容對(duì)大家有所幫助。如果想了解更多相關(guān)文章內(nèi)容,歡迎關(guān)注億速云行業(yè)資訊頻道。

向AI問(wèn)一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

php
AI