PHP實現系統編程(二) --- 多進程編程介紹及孤兒進程、殭屍進程

多進程編程也是系統編程的一個重要方面,但PHP程序員通常不需要關心多進程的問題,因爲web服務器或者PHP-FPM已經幫我們管理好進程方面的問題了,但是如果我們想要用PHP來開發CLI程序,多進程編程是不可或缺的基本技術。

PHP中關於進程控制的方法主要使用到PCNTL(Process Control)擴展, 所以,在進行多進程編程之前,首先要確保你的PHP已經安裝了最新的PCNTL擴展,可以輸入php -m命令來查看當前已經安裝的擴展:



該擴展給我們提供了一組用於進程操作的方法:

PCNTL 函數
pcntl_alarm — 爲進程設置一個alarm鬧鐘信號
pcntl_errno — 別名 pcntl_get_last_error
pcntl_exec — 在當前進程空間執行指定程序
pcntl_fork — 在當前進程當前位置產生分支(子進程)。
pcntl_get_last_error — Retrieve the error number set by the last pcntl function which failed
pcntl_getpriority — 獲取任意進程的優先級
pcntl_setpriority — 修改任意進程的優先級
pcntl_signal_dispatch — 調用等待信號的處理器
pcntl_signal_get_handler — Get the current handler for specified signal
pcntl_signal — 安裝一個信號處理器
pcntl_sigprocmask — 設置或檢索阻塞信號
pcntl_sigtimedwait — 帶超時機制的信號等待
pcntl_sigwaitinfo — 等待信號
pcntl_strerror — Retrieve the system error message associated with the given errno
pcntl_wait — 等待或返回fork的子進程狀態
pcntl_waitpid — 等待或返回fork的子進程狀態
pcntl_wexitstatus — 返回一箇中斷的子進程的返回代碼
pcntl_wifexited — 檢查狀態代碼是否代表一個正常的退出。
pcntl_wifsignaled — 檢查子進程狀態碼是否代表由於某個信號而中斷
pcntl_wifstopped — 檢查子進程當前是否已經停止
pcntl_wstopsig — 返回導致子進程停止的信號
pcntl_wtermsig — 返回導致子進程中斷的信號

pcntl_fork — 在當前進程當前位置產生分支(子進程)。譯註:fork是創建了一個子進程,父進程和子進程 都從fork的位置開始向下繼續執行,不同的是父進程執行過程中,得到的fork返回值爲子進程號,而子進程得到的是0。

fork出的子進程幾近於完全的複製了父進程,父子進程共享代碼段,雖然父子進程的數據段、堆、棧是相互獨立的,但在一開始,子進程完全複製了父進程的這些數據,但之後的修改互不影響。

int pcntl_fork ( void )

創建5個子進程代碼演示:

<?php

for($i = 0; $i < 5; $i++)
{
        $pid = pcntl_fork();  //創建子進程,子進程也是從這裏開始執行。

        if ($pid == 0)
        {
                break; //由於子進程也會執行循環的代碼,所以讓子進程退出循環,否則子進程又會創建自己的子進程。
        }
}

sleep($i);  //第一個創建的子進程將睡眠0秒,第二個將睡眠1s,依次類推...主進程會睡眠5秒

if ($i < 5)
{
        exit("第 " . ($i+1) . " 個子進程退出..." . time() . PHP_EOL);
}
else
{
        exit("父進程退出..." . time() . PHP_EOL);
}

運行結果:

[root@localhost process]# php process.php 
第 1 個子進程退出...1503322773
第 2 個子進程退出...1503322774
第 3 個子進程退出...1503322775
第 4 個子進程退出...1503322776
第 5 個子進程退出...1503322777
父進程退出...1503322778

對於pcntl_fork函數要重點理解:fork是創建了一個子進程,父進程和子進程 都從fork的位置開始向下繼續執行,不同的是父進程執行過程中,得到的fork返回值爲子進程號,而子進程得到的是0”

把上面的代碼稍作修改,不讓進程退出,然後利用ps命令查看系統狀態:

<?php

for($i = 0; $i < 5; $i++)
{
        $pid = pcntl_fork();

        if ($pid == 0)
        {
                break; //由於子進程也會執行循環的代碼,所以讓子進程退出循環
        }
}

sleep($i);  //第一個創建的子進程將睡眠0秒,第二個將睡眠1s,依次類推...主進程會睡眠5秒

/*
if ($i < 5)
{
        exit("第 " . ($i+1) . " 個子進程退出..." . time() . PHP_EOL);
}
else
{
        exit("父進程退出..." . time() . PHP_EOL);
}
*/

while(1)
{
        sleep(1);       //執行死循環不退出
}

運行後輸入 ps -ef | grep php 查看系統進程

[root@localhost ~]# ps -ef | grep php
root      3670  3609  0 21:54 pts/0    00:00:00 php process.php
root      3671  3670  0 21:54 pts/0    00:00:00 php process.php
root      3672  3670  0 21:54 pts/0    00:00:00 php process.php
root      3673  3670  0 21:54 pts/0    00:00:00 php process.php
root      3674  3670  0 21:54 pts/0    00:00:00 php process.php
root      3675  3670  0 21:54 pts/0    00:00:00 php process.php
root      3677  3646  0 21:54 pts/1    00:00:00 grep php

可以看到6個 php process.php 進程,其中第二列是進程號,第三列是進程的父進程號,可以看到後面五個進程的父進程號都是第一個進程的進程號。

上面的代碼子進程和父進程都是執行相同的代碼,有沒有辦法讓子進程和父進程做不同的事呢,最簡單的辦法就是if判斷,子進程執行子進程的代碼,父進程執行父進程的代碼:

<?php
$ppid = posix_getpid(); //記錄父進程的進程號

for($i = 0; $i < 5; $i++)
{
        $pid = pcntl_fork();

        if ($pid == 0)
        {       
                break; //由於子進程也會執行循環的代碼,所以讓子進程退出循環
        }
}

if ($ppid == posix_getpid())
{
        //父進程
        while(1)
        {
                sleep(1);
        }
}
else
{
        //子進程
        for($i = 0; $i < 100; $i ++)
        {
                echo "子進程" . posix_getpid() . " 循環 $i ...\n";
                sleep(1);
        }
}

[root@localhost process]# php process.php 
子進程6677 循環 0 ...
子進程6676 循環 0 ...
子進程6678 循環 0 ...
子進程6680 循環 0 ...
子進程6679 循環 0 ...
子進程6677 循環 1 ...
子進程6676 循環 1 ...
子進程6678 循環 1 ...
子進程6680 循環 1 ...
子進程6679 循環 1 ...
子進程6677 循環 2 ...
子進程6676 循環 2 ...
子進程6678 循環 2 ...
子進程6680 循環 2 ...

其實上面的程序父子進程還是執行了相同的代碼,只是進入的if分支不一樣,而pcntl_exec則可以讓子進程完全脫離父進程的影響,去執行新的程序。

pcntl_exec — 在當前進程空間執行指定程序

void pcntl_exec ( string $path [, array $args [, array $envs ]] )

path
path必須時可執行二進制文件路徑或一個在文件第一行指定了 一個可執行文件路徑標頭的腳本(比如文件第一行是#!/usr/local/bin/perl的perl腳本)。 更多的信息請查看您系統的execve(2)手冊。

args
args是一個要傳遞給程序的參數的字符串數組。

envs
envs是一個要傳遞給程序作爲環境變量的字符串數組。這個數組是 key => value格式的,key代表要傳遞的環境變量的名稱,value代表該環境變量值。

注意該方法的返回值比較特殊:當發生錯誤時返回 FALSE ,沒有錯誤時沒有返回,因爲pcntl_exec調用成功,子進程就去運行新的程序 從父進程繼承的代碼段、數據段、堆、棧等信息全部被替換成新的,此時的pcntl_exec函數調用棧已經不存在了,所以也就沒有返回了。代碼示例:

<?php
for($i = 0; $i < 3; $i++)
{
        $pid = pcntl_fork();

        if($pid == 0)
        {
                echo "子進程pid = " . posix_getpid() . PHP_EOL;
                $ret = pcntl_exec('/bin/ls');  //執行 ls 命令, 此處調用成功子進程將不會再回來執行下面的任何代碼
                var_dump($ret); // 此處的代碼不會再執行
        }
}

sleep(5);  //睡眠5秒以確保子進程執行完畢,原因後面會說

exit( "主進程退出...\n");

運行結果:

[root@localhost process]# php pcntl_exec.php 
子進程pid = 6728
子進程pid = 6729
子進程pid = 6727
pcntl_exec.php	process.php
pcntl_exec.php	process.php
pcntl_exec.php	process.php
主進程退出...
[root@localhost process]# ls
pcntl_exec.php  process.php

以上就是對PHP多進程開發的簡單介紹,對於子進程不同的存續狀態,引出孤兒進程和殭屍進程的概念,在linux系統中,init進程(1號進程)是所有進程的祖先,其他進程要麼是該進程的子進程,要麼是子進程的子進程,子進程的子進程的子進程...,linux系統中可以用 pstree 命令查看進程樹結構:


在多進程程序中,如果父進程先於子進程退出,那麼子進程將會被init進程收養,成爲init進程的子進程,這種進程被稱爲孤兒進程,我們可以把上面的代碼稍作修改來演示這種情況:

<?php
$ppid = posix_getpid(); //記錄父進程的進程號

for($i = 0; $i < 5; $i++)
{
        $pid = pcntl_fork();

        if ($pid == 0)
        {       
                break; //由於子進程也會執行循環的代碼,所以讓子進程退出循環
        }
}

if ($ppid == posix_getpid())
{
        //父進程直接退出,它的子進程都會成爲孤兒進程
        exit(0);
}
else
{
        //子進程
        for($i = 0; $i < 100; $i ++)
        {
                echo "子進程" . posix_getpid() . " 循環 $i ...\n";
                sleep(1);
        }
}
運行該程序,然後查看進程狀態:

[root@localhost ~]# ps -ef | grep php
root      2903     1  0 12:09 pts/0    00:00:00 php pcntl.fork.php
root      2904     1  0 12:09 pts/0    00:00:00 php pcntl.fork.php
root      2905     1  0 12:09 pts/0    00:00:00 php pcntl.fork.php
root      2906     1  0 12:09 pts/0    00:00:00 php pcntl.fork.php
root      2907     1  0 12:09 pts/0    00:00:00 php pcntl.fork.php
root      2935  2912  0 12:10 pts/1    00:00:00 grep php

可以看到五個子進程的父進程號都是1了,並且這時控制檯不再被程序佔用,子進程轉到了後臺運行,這種孤兒進程被init進程收養的機制是實現後面將要介紹的守護進程的必要條件之一。

子進程還有一種狀態叫殭屍進程,子進程結束時並不是完全退出,內核進程表中仍舊保有該進程的記錄,這樣做的目的是能夠讓父進程可以得知子進程的退出狀態,以及子進程是自殺(調用exit或代碼執行完畢)還是他殺(被信號終止),父進程可以調用pcntl_wait 或 pcntl_waitpid 方法來回收子進程(收屍),釋放子進程佔用的所有資源,並獲得子進程的退出狀態,如果父進程不做回收,則殭屍進程一直存在,如果這時父進程也退出了,則這些殭屍進程會被init進程接管並自動回收。

對於linux系統來說,一個長時間運行的多進程程序一定要回收子進程,因爲系統的進程資源是有限的,殭屍進程會讓系統的可用資源減少。

代碼演示殭屍進程的產生:

<?php
$ppid = posix_getpid(); //記錄父進程的進程號

for($i = 0; $i < 5; $i++)
{
        $pid = pcntl_fork();

        if ($pid == 0)
        {
                break; //由於子進程也會執行循環的代碼,所以讓子進程退出循環
        }
}

if ($ppid == posix_getpid())
{
        //父進程不退出,也不回收子進程
        while(1)
        {
                sleep(1);
        }
}
else
{
        //子進程退出,會成爲殭屍進程
                exit("子進程退出 $ppid ...\n");
}

運行之後查看進程狀態:

[root@localhost ~]# ps -ef | grep php
root      2971  2864  0 14:13 pts/0    00:00:00 php pcntl.fork.php
root      2972  2971  0 14:13 pts/0    00:00:00 [php] <defunct>
root      2973  2971  0 14:13 pts/0    00:00:00 [php] <defunct>
root      2974  2971  0 14:13 pts/0    00:00:00 [php] <defunct>
root      2975  2971  0 14:13 pts/0    00:00:00 [php] <defunct>
root      2976  2971  0 14:13 pts/0    00:00:00 [php] <defunct>
root      2978  2912  0 14:13 pts/1    00:00:00 grep php

殭屍進程會用 <defunct>(死者,死人) 來標識,除非我們結束父進程,否則這些殭屍進程會一直存在,也無法用kill命令來殺死。

PHP的pcntl擴展提供了兩個回收子進程的方法供我們調用:

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

pcntl_wait函數掛起當前進程的執行直到一個子進程退出或接收到一個信號要求中斷當前進程或調用一個信號處理函數。 如果一個子進程在調用此函數時已經退出(俗稱殭屍進程),此函數立刻返回。子進程使用的所有系統資源將被釋放。
關於wait在您系統上工作的詳細規範請查看您系統的wait(2)手冊。這個函數等同於以-1作爲參數pid 的值並且沒有options參數來調用pcntl_waitpid() 函數。

代碼示例:

<?php
$ppid = posix_getpid(); //記錄父進程的進程號

for($i = 0; $i < 5; $i++)
{
        $pid = pcntl_fork();

        if ($pid == 0)
        {
                break; //由於子進程也會執行循環的代碼,所以讓子進程退出循環
        }
}

if ($ppid == posix_getpid())
{
        //父進程循環回收收子進程
        while(($id = pcntl_wait($status)) > 0) //如果沒有子進程退出, pcntl_wait 會一直阻塞
        {
                echo "回收子進程:$id, 子進程退出狀態值: $status...\n";
        }

        exit("父進程退出 $id....\n"); //當子進程全部結束 pcntl_wait 返回-1
}
else
{
        //子進程退出,會成爲殭屍進程
        sleep($i);
        exit($i); 
}


運行結果:

[root@localhost php]# php pcntl.fork.php 
回收子進程:3043, 子進程退出狀態值: 0...
回收子進程:3044, 子進程退出狀態值: 256...
回收子進程:3045, 子進程退出狀態值: 512...
回收子進程:3046, 子進程退出狀態值: 768...
回收子進程:3047, 子進程退出狀態值: 1024...
父進程退出 -1....


這裏只是對PHP多進程編程做了基本的介紹,後面會結合 信號、進程間通信以及守護進程 做更進一步的介紹,歡迎大家關注後續文章。

PHP是世界上最好的語言 That's all :)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章