PHP多進程、信號量及孤兒進程和殭屍進程

PHP多進程、信號量及孤兒進程和殭屍進程

實際上PHP是有多線程的,只是不少人不經常使用。使用PHP的多線程首先須要下載安裝一個線程安全版本(ZTS版本)的PHP,而後再安裝pecl的 pthread 擴展。

實際上PHP是有多進程的,有一些人再用,整體來講php的多進程還算湊合,只須要在安裝PHP的時候開啓pcntl模塊(是否是跟UNIX中的fcntl有點兒…. ….)便可。在*NIX下,在終端命令行下使用php -m就能夠看到是否開啓了pcntl模塊。php

因此咱們只說php的多進程,至於php多線程就暫時放到一邊兒。html

注意:不要在apache或者fpm環境下使用php多進程,這將會產生不可預估的後果。node

PHP多進程初探

進程是程序執行的實例,舉個例子有個程序叫作 「 病毒.exe 」,這個程序平時是以文件形式存儲在硬盤上,當你雙擊運行後,就會造成一個該程序的進程。系統會給每個進程分配一個惟一的非負整數用來標記進程,這個數字稱做進程ID。當該進程被殺死或終止後,其進程ID就會被系統回收,而後分配給新的其他的進程。nginx

說了這麼多,這鬼東西有什麼用嗎?我平時用CI、YII寫個CURD跟這個也沒啥關聯啊。實際上,若是你瞭解APACHE PHP MOD或者FPM就知道這些東西就是多進程實現的。以FPM爲例,通常都是nginx做爲http服務器擋在最前面,靜態文件請求則nginx自行處理,遇到php動態請求則轉發給php-fpm進程來處理。若是你的php-fpm配置只開了5個進程,若是處理任意一個用戶的請求都須要1秒鐘,那麼5個fpm進程1秒中就最多隻能處5個用戶的請求。因此結論就是:若是要單位時間內幹活更快更多,就須要更多的進程,總之一句話就是多進程能夠加快任務處理速度。apache

在php中咱們使用pcntl_fork()來建立多進程(在*NIX系統的C語言編程中,已有進程經過調用fork函數來產生新的進程)。fork出來新進程則成爲子進程,原進程則成爲父進程,子進程擁有父進程的副本。這裏要注意:編程

  • 子進程與父進程共享程序正文段
  • 子進程擁有父進程的數據空間和堆、棧的副本,注意是副本,不是共享
  • 父進程和子進程將繼續執行fork以後的程序代碼
  • fork以後,是父進程先執行仍是子進程先執行沒法確認,取決於系統調度(取決於信仰)

這裏說子進程擁有父進程數據空間以及堆、棧的副本,實際上,在大多數的實現中也並非真正的徹底副本。更可能是採用了COW(Copy On Write)即寫時複製的技術來節約存儲空間。簡單來講,若是父進程和子進程都不修改這些 數據、堆、棧 的話,那麼父進程和子進程則是暫時共享同一份 數據、堆、棧。只有當父進程或者子進程試圖對 數據、堆、棧 進行修改的時候,纔會產生複製操做,這就叫作寫時複製。安全

在調用完pcntl_fork()後,該函數會返回兩個值。在父進程中返回子進程的進程ID,在子進程內部自己返回數字0。因爲多進程在apache或者fpm環境下沒法正常運行,因此你們必定要在php cli環境下執行下面php代碼。服務器

第一段代碼,咱們來講明在程序從pcntl_fork()後父進程和子進程將各自繼續往下執行代碼:多線程

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

將文件保存爲test.php,而後在使用cli執行,結果以下圖所示:ssh

第二段代碼,用來講明子進程擁有父進程的數據副本,而並非共享:

// 初始化一個 number變量 數值爲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循環來作些東西,問題來了:會顯示幾回 「 兒子 」?

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

上面代碼執行結果以下:

仔細數數,居然是顯示了7次 「 兒子 」。好奇怪,難道不是3次嗎?… …
下面我修改一下代碼,結合下面的代碼,再思考一下爲何會產生7次而不是3次。

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

執行結果以下圖所示:

前面強調過:父進程和子進程將繼續執行fork以後的程序代碼。這裏就不解釋,實在想不明白的,能夠動手本身畫畫思考一下。

孤兒與殭屍進程

實際上,大家必定要記住:PHP的多進程是很是值得應用於生產環境具有高價值的生產力工具。

但我認爲在正式開始吹牛以前仍是要說兩個基本概念:孤兒進程、殭屍進程。

上文我整篇尬聊的都是pcntl_fork(),只管fork生產,無論產後護理,實際上這樣並不符合主流價值觀,並且,操做系統自己資源有限,這樣無限生產不顧護理,操做系統也會吃不消的。

孤兒進程是指父進程在fork出子進程後,本身先完了。這個問題很尷尬,由於子進程今後變得無依無靠、無家可歸,變成了孤兒。用術語來表達就是,父進程在子進程結束以前提早退出,這些子進程將由init(進程ID爲1)進程收養並完成對其各類數據狀態的收集。init進程是Linux系統下的奇怪進程,這個進程是以普通用戶權限運行但卻具有超級權限的進程,簡單地說,這個進程在Linux系統啓動的時候作初始化工做,好比運行getty、好比會根據/etc/inittab中設置的運行等級初始化系統等等,固然了,還有一個做用就是如上所說的:收養孤兒進程。

殭屍進程是指父進程在fork出子進程,然後子進程在結束後,父進程並無調用wait或者waitpid等完成對其清理善後工做,致使改子進程進程ID、文件描述符等依然保留在系統中,極大浪費了系統資源。因此,殭屍進程是對系統有危害的,而孤兒進程則相對來講沒那麼嚴重。在Linux系統中,咱們能夠經過ps -aux來查看進程,若是有[Z+]標記就是殭屍進程。

在PHP中,父進程對子進程的狀態收集等是經過pcntl_wait()pcntl_waitpid()等完成的。依然仍是要經過代碼還演示說明:
演示並說明孤兒進程的出現,並演示孤兒進程被init進程收養:

$id = pcntl_fork();
if( $pid > 0 ){
    // 顯示父進程的進程ID,這個函數能夠是getmypid(),也能夠用posix_getpid()
    echo "Father PID:".getmypid().PHP_EOL;
    // 讓父進程中止兩秒鐘,在這兩秒內,子進程的父進程ID仍是這個父進程
    sleep( 2 );
} else if( 0 == $pid ) {
    // 讓子進程循環10次,每次睡眠1s,而後每秒鐘獲取一次子進程的父進程進程ID
    for( $i = 1; $i <= 10; $i++ ){
        sleep( 1 );
        // posix_getppid()函數的做用就是獲取當前進程的父進程進程ID
        echo posix_getppid().PHP_EOL;
    }
} else {
    echo "fork error.".PHP_EOL;
}

運行結果以下圖:

能夠看到,前兩秒內,子進程的父進程進程ID爲4129,可是從第三秒開始,因爲父進程已經提早退出了,子進程變成孤兒進程,因此init進程收養了子進程,因此子進程的父進程進程ID變成了1。

演示並說明殭屍進程的出現,並演示殭屍進程的危害:

$pid = pcntl_fork();
 if( $pid > 0 ){
     // 下面這個函數能夠更改php進程的名稱
     cli_set_process_title('php father process');
     // 讓主進程休息60秒鐘
     sleep(60);
 } else if( 0 == $pid ) {
     cli_set_process_title('php child process');
     // 讓子進程休息10秒鐘,可是進程結束後,父進程不對子進程作任何處理工做,這樣這個子進程就會變成殭屍進程
     sleep(10);
 } else {
     exit('fork error.'.PHP_EOL);
 }

運行結果以下圖:

經過執行ps -aux命令能夠看到,當程序在前十秒內運行的時候,php child process的狀態列爲[S+],然而在十秒鐘事後,這個狀態變成了[Z+],也就是變成了危害系統的殭屍進程。

那麼,問題來了?如何避免殭屍進程呢?PHP經過pcntl_wait()和pcntl_waitpid()兩個函數來幫咱們解決這個問題。瞭解Linux系統編程的應該知道,看名字就知道這其實就是PHP把C語言中的wait()和waitpid()包裝了一下。

經過代碼演示pcntl_wait()來避免殭屍進程,在開始以前先簡單普及一下pcntl_wait()的相關內容:這個函數的做用就是 「 等待或者返回子進程的狀態 」,當父進程執行了該函數後,就會阻塞掛起等待子進程的狀態一直等到子進程已經因爲某種緣由退出或者終止。換句話說就是若是子進程還沒結束,那麼父進程就會一直等等等,若是子進程已經結束,那麼父進程就會馬上獲得子進程狀態。這個函數返回退出的子進程的進程ID或者失敗返回-1。

咱們將第二個案例中代碼修改一下:

$pid = pcntl_fork();
if( $pid > 0 ){
    // 下面這個函數能夠更改php進程的名稱
    cli_set_process_title('php father process');
    // 返回$wait_result,就是子進程的進程號,若是子進程已是殭屍進程則爲0
    // 子進程狀態則保存在了$status參數中,能夠經過pcntl_wexitstatus()等一系列函數來查看$status的狀態信息是什麼
    $wait_result = pcntl_wait( $status );
    print_r( $wait_result );
    print_r( $status );
    // 讓主進程休息60秒鐘
    sleep(60);
} else if( 0 == $pid ) {
    cli_set_process_title('php child process');
    // 讓子進程休息10秒鐘,可是進程結束後,父進程不對子進程作任何處理工做,這樣這個子進程就會變成殭屍進程
    sleep(10);
} else {
    exit('fork error.'.PHP_EOL);
}

將文件保存爲wait.php,而後php wait.php,在另一個終端中經過ps -aux查看,能夠看到在前十秒內,php child process是[S+]狀態,而後十秒鐘事後進程消失了,也就是被父進程回收了,沒有變成殭屍進程。

可是,pcntl_wait()有個很大的問題,就是阻塞。父進程只能掛起等待子進程結束或終止,在此期間父進程什麼都不能作,這並不符合多快好省原則,因此pcntl_waitpid()閃亮登場。pcntl_waitpid( $pid, &$status, $option = 0 )的第三個參數若是設置爲WNOHANG,那麼父進程不會阻塞一直等待到有子進程退出或終止,不然將會和pcntl_wait()的表現相似。

修改第三個案例的代碼,可是,咱們並不添加WNOHANG,演示說明pcntl_waitpid()功能:

$pid = pcntl_fork();
 if( $pid > 0 ){
     // 下面這個函數能夠更改php進程的名稱
     cli_set_process_title('php father process');
     // 返回值保存在$wait_result中
     // $pid參數表示 子進程的進程ID
     // 子進程狀態則保存在了參數$status中
     // 將第三個option參數設置爲常量WNOHANG,則能夠避免主進程阻塞掛起,此處父進程將當即返回繼續往下執行剩下的代碼
     $wait_result = pcntl_waitpid( $pid, $status );
     var_dump( $wait_result );
     var_dump( $status );
     // 讓主進程休息60秒鐘
     sleep(60);
 } else if( 0 == $pid ) {
     cli_set_process_title('php child process');
     // 讓子進程休息10秒鐘,可是進程結束後,父進程不對子進程作任何處理工做,這樣這個子進程就會變成殭屍進程
     sleep(10);
 } else {
     exit('fork error.'.PHP_EOL);
 }

下面是運行結果,一個執行php程序的終端窗口,另外一個是ps -aux終端窗口。實際上能夠看到主進程是被阻塞的,一直到第十秒子進程退出了,父進程再也不阻塞:

那麼咱們修改第四段代碼,添加第三個參數WNOHANG,代碼以下:

$pid = pcntl_fork();
 if( $pid > 0 ){
     // 下面這個函數能夠更改php進程的名稱
     cli_set_process_title('php father process');
     // 返回值保存在$wait_result中
     // $pid參數表示 子進程的進程ID
     // 子進程狀態則保存在了參數$status中
     // 將第三個option參數設置爲常量WNOHANG,則能夠避免主進程阻塞掛起,此處父進程將當即返回繼續往下執行剩下的代碼
     $wait_result = pcntl_waitpid( $pid, $status, WNOHANG );
     var_dump( $wait_result );
     var_dump( $status );
     echo "不阻塞,運行到這裏".PHP_EOL;
     // 讓主進程休息60秒鐘
     sleep(60);
 } else if( 0 == $pid ) {
     cli_set_process_title('php child process');
     // 讓子進程休息10秒鐘,可是進程結束後,父進程不對子進程作任何處理工做,這樣這個子進程就會變成殭屍進程
     sleep(10);
 } else {
     exit('fork error.'.PHP_EOL);
 }

下面是運行結果,一個執行php程序的終端窗口,另外一個是ps -aux終端窗口。實際上能夠看到主進程是被阻塞的,一直到第十秒子進程退出了,父進程再也不阻塞:

問題出現了,居然php child process進程狀態居然變成了[Z+],這是怎麼搞得?回頭分析一下代碼:
咱們看到子進程是睡眠了十秒鐘,而父進程在執行pcntl_waitpid()以前沒有任何睡眠且自己再也不阻塞,因此,主進程本身先執行下去了,而子進程在足足十秒鐘後才結束,進程狀態天然沒法獲得回收。若是咱們將代碼修改一下,就是在主進程的pcntl_waitpid()前睡眠15秒鐘,這樣就能夠回收子進程了。可是即使這樣修改,細心想的話仍是會有個問題,那就是在子進程結束後,在父進程執行pcntl_waitpid()回收前,有五秒鐘的時間差,在這個時間差內,php child process也將會是殭屍進程。那麼,pcntl_waitpid()如何正確使用啊?這樣用,看起來畢竟不太科學。

那麼,是時候引入信號量了!

PHP 信號量

信號是一種軟件中斷,也是一種很是典型的異步事件處理方式。在NIX系統誕生的混沌之初,信號的定義是比較混亂的,並且最關鍵是不可靠,這是一個很嚴重的問題。因此在後來的POSIX標準中,對信號作了標準化同時也各個發行版的NIX也都提供大量可靠的信號。每種信號都有本身的名字,大概如SIGTERM、SIGHUP、SIGCHLD等等,在*NIX中,這些信號本質上都是整形數字(遊有心情的能夠參觀一下signal.h系列頭文件)。

信號的產生是有多種方式的,下面是常見的幾種:

  • 鍵盤上按某些組合鍵,好比Ctrl+C或者Ctrl+D等,會產生SIGINT信號。
  • 使用posix kill調用,能夠向某個進程發送指定的信號。
  • 遠程ssh終端狀況下,若是你在服務器上執行了一個阻塞的腳本,正在阻塞過程當中你關閉了終端,可能就會產生SIGHUP信號。
  • 硬件也會產生信號,好比OOM了或者遇到除0這種狀況,硬件也會向進程發送特定信號。

而進程在收到信號後,能夠有以下三種響應:

  • 直接忽略,不作任何反映。就是俗稱的徹底不鳥。可是有兩種信號,永遠不會被忽略,一個是SIGSTOP,另外一個是SIGKILL,由於這兩個進程提供了向內核最後的可靠的結束進程的辦法。
  • 捕捉信號並做出相應的一些反應,具體響應什麼能夠由用戶本身經過程序自定義。
  • 系統默認響應。大多數進程在遇到信號後,若是用戶也沒有自定義響應,那麼就會採起系統默認響應,大多數的系統默認響應就是終止進程。

用人話來表達,就是說假如你是一個進程,你正在幹活,忽然施工隊的喇叭裏衝你嚷了一句:「吃飯了!」,因而你就放下手裏的活兒去吃飯。你正在幹活,忽然施工隊的喇叭裏衝你嚷了一句:「發工資了!」,因而你就放下手裏的活兒去領工資。你正在幹活,忽然施工隊的喇叭裏衝你嚷了一句:「有人找你!」,因而你就放下手裏的活兒去看看是誰找你什麼事情。固然了,你很任性,那是徹底能夠不鳥喇叭裏喊什麼內容,也就是忽略信號。也能夠更任性,當喇叭裏衝你嚷「吃飯」的時候,你去就不去吃飯,你去睡覺,這些均可以由你來。而你在幹活過程當中,歷來不會由於要等某個信號就不幹活了一直等信號,而是信號隨時隨地均可能會來,而你只須要在這個時候做出相應的迴應便可,因此說,信號是一種軟件中斷,也是一種異步的處理事件的方式。

回到上文所說的問題,就是子進程在結束前,父進程就已經先調用了pcntl_waitpid(),致使子進程在結束後依然變成了殭屍進程。實際上在父進程不斷while循環調用pcntl_waitpid()是個解決辦法,大概代碼以下:

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

下圖是運行結果:

解析一下這個結果,我前後三次執行了ps -aux | grep php去查看這兩個php進程。

  • 第一次:子進程正在休眠中,父進程依舊在循環中。
  • 第二次:子進程已經退出了,父進程依舊在循環中,可是代碼尚未執行到pcntl_waitpid(),因此在子進程退出後到父進程執行回收前這段空隙內子進程變成了殭屍進程。
  • 第三次:此時父進程已經執行了pcntl_waitpid(),將已經退出的子進程回收,釋放了pid等資源。

可是這樣的代碼有一個缺陷,實際上就是子進程已經退出的狀況下,主進程還在不斷while pcntl_waitpid()去回收子進程,這是一件很奇怪的事情,並不符合社會主義主流價值觀,不低碳不節能,代碼也不優雅,很差看。因此,應該考慮用更好的方式來實現。那麼,咱們篇頭提了許久的信號終於概要出場了。

如今讓咱們考慮一下,爲什麼信號能夠解決「不低碳不節能,代碼也不優雅,很差看」的問題。子進程在退出的時候,會向父進程發送一個信號,叫作SIGCHLD,那麼父進程一旦收到了這個信號,就能夠做出相應的回收動做,也就是執行pcntl_waitpid(),從而解決掉殭屍進程,並且還顯得咱們代碼優雅好看節能環保。

梳理一下流程,子進程向父進程發送SIGCHLD信號是對人們來講是透明的,也就是說咱們無須關心。可是,咱們須要給父進程安裝一個響應SIGCHLD信號的處理器,除此以外,還須要讓這些信號處理器運行起來,安裝上了不運行是一件尷尬的事情。那麼,在php裏給進程安裝信號處理器使用的函數是pcntl_signal(),讓信號處理器跑起來的函數是pcntl_signal_dispatch()

  • pcntl_signal(),安裝一個信號處理器,具體說明是pcntl_signal ( int $signo , callback $handler [, bool $restart_syscalls = true ] ),參數signo就是信號,callback則是響應該信號的代碼段,返回bool值。
  • pcntl_signal_dispatch(),調用每一個等待信號經過pcntl_signal() 安裝的處理器,參數爲void,返回bool值。

下面結合新引入的兩個函數來解決一下樓上的醜陋代碼:

$pid = pcntl_fork();
if( 0 > $pid ){
    exit('fork error.'.PHP_EOL);
} else if( 0 < $pid ) {
    // 在父進程中
    // 給父進程安裝一個SIGCHLD信號處理器
    pcntl_signal( SIGCHLD, function() use( $pid ) {
        echo "收到子進程退出".PHP_EOL;
        pcntl_waitpid( $pid, $status, WNOHANG );
    } );
    cli_set_process_title('php father process');
    // 父進程不斷while循環,去反覆執行pcntl_waitpid(),從而試圖解決已經退出的子進程
    while( true ){
        sleep( 1 );
        // 註釋掉原來老掉牙的代碼,轉而使用pcntl_signal_dispatch()
        //pcntl_waitpid( $pid, &$status, WNOHANG );
        pcntl_signal_dispatch();
    }
} else if( 0 == $pid ) {
    // 在子進程中
    // 子進程休眠3秒鐘後直接退出
    cli_set_process_title('php child process');
    sleep( 20 );
    exit;
}

運行結果以下:

PHP 預約義了一些信號量,可參看 http://php.net/manual/zh/pcntl.constants.php

原文地址:
https://blog.ti-node.com/blog/6363989547574886401
https://blog.ti-node.com/blog/6375675957193211905
https://blog.ti-node.com/blog/6375380006637404161

相關文章
相關標籤/搜索
本站公眾號
   歡迎關注本站公眾號,獲取更多信息