1. 程式人生 > >PHP多程序程式設計理論+實戰

PHP多程序程式設計理論+實戰

這篇文章主要介紹了PHP多程序程式設計例項,本文講解的是在Linux下實現PHP多程序程式設計,需要的朋友可以參考下:

羨慕火影忍者裡鳴人的影分身麼?沒錯,PHP程式是可以開動影分身的!想完成任務,又覺得一個程序太慢,那麼,試試用多程序來搞吧。這篇文章將會介紹一下PHP多程序的基本需求,如何建立多程序以及基本的訊號控制,暫時不會告訴你如何進行程序間通訊和資訊共享。

1. 準備

在動手之前,請確定你用的不是M$ Windows平臺(因為我沒有Windows)。Linux / BSD / Unix應該都是沒問題的。確認好了工作環境以後一起來看看我們需要的PHP模組是否都有。開啟終端輸入下面的命令:

$ php -m

這個命令檢查並列印當前PHP所有開啟的擴充套件,看一下pcntl和posix是否在輸出的列表中。

1.1. pcntl

如果找不到pcntl,八成是編譯的時候沒把這個擴充套件編譯進去。如果你和我一樣是編譯安裝的PHP,那麼需要重新編譯安裝PHP。在配置的時候記得加上--enable-pcntl引數即可。

$ cd /path/to/php_source_code_dir
$ ./configure [some other options] --enable-pcntl
$ make && make install

1.2. posix

這貨一般預設就會裝上,只要你編譯的時候沒有加上--disable-posix。

2. 預備知識

在繼續之前,你還需要對Linux多程序有一點了解。多程序是咋回事呢?這裡可跟火影忍者裡的影分身稍微有點不同。首先,鳴人從小長到大,比如16歲,咳。有一天他發動了影分身,分出了5個他。顯然,這些分身也是16歲的鳴人而不是剛出生啥也不懂就會哭的嬰兒(那叫克隆)。然後,不一樣的地方來了:分身們變成了獨立的人各自去做各自的事,互相之間不再知道其他分身和原身都做了什麼(當然不會像動畫片裡一樣積累經驗給原身啦)。除非,他們互相之間有交流,不然,只有16歲之前的事情才是他們共同的記憶。

有同學說了,老大你這不坑爹呢麼?我又沒看過火影忍者!那你去看一遍好了……

最後,預備知識完了,就是大致瞭解一下主程序開出來的子程序是怎麼回事。子程序的程式碼和主程序是完全一樣的,還有一部分一樣的東西就是直到發動影分身之前執行的所有內容。

3. 影分身之術

所以呢,沒有點基礎知識怎麼能理解卷軸裡的內容呢?打開卷軸首先看到了一個單詞:fork。

3.1. fork

叉子?叉子是分岔的,一個變多個嘛!差不多就是這個意思。建立子程序就用這個命令。這裡需要用到pcntl_fork()函式。(可以先簡單看一下PHP手冊關於這個函式的介紹。)建立一個PHP指令碼:
$pid = pcntl_fork(); // 一旦呼叫成功,事情就變得有些不同了
if ($pid == -1) {
    die('fork failed');
} else if ($pid == 0) {
} else {
}

pcntl_fork()函式建立一個子程序,子程序和父程序唯一的區別就是PID(程序ID)和PPID(父程序ID)不同。在終端下檢視程序用ps命令(問問man看ps怎麼用:man ps)。當函式返回值為-1的時候,說明fork失敗了。試試在if前面加一句:echo $pid . PHP_EOL;。執行你的指令碼,輸出可能像下面這樣(結果說明子程序和父程序的程式碼是相同的):
67789 # 這個是父程序列印的
0     # 這個是子程序列印的

pcntl_fork()函式呼叫成功後,在父程序中會返回子程序的PID,而在子程序中返回的是0。所以,下面直接用if分支來控制父程序和子程序做不同的事。

3.2. 分配任務

然後我們來說說鳴人16歲那次影分身的事兒,給原身和分身分配兩個簡單的輸出任務:
$parentPid = getmypid(); // 這就是傳說中16歲之前的記憶
$pid = pcntl_fork(); // 一旦呼叫成功,事情就變得有些不同了
if ($pid == -1) {
    die('fork failed');
} else if ($pid == 0) {
    $mypid = getmypid(); // 用getmypid()函式獲取當前程序的PID
    echo 'I am child process. My PID is ' . $mypid . ' and my father's PID is ' . $parentPid . PHP_EOL;
} else {
    echo 'Oh my god! I am a father now! My child's PID is ' . $pid . ' and mine is ' . $parentPid . PHP_EOL;
}

輸出的結果可能是這樣:
Oh my god! I am a father now! My child's PID is 68066 and mine is 68065
I am child process. My PID is 68066 and my father's PID is 68065

再強調一下,pcntl_fork()呼叫成功以後,一個程式變成了兩個程式:一個程式得到的$pid變數值是0,它是子程序;另一個程式得到的$pid的值大於0,這個值是子程序的PID,它是父程序。在下面的分支語句中,由於$pid值的不同,運行了不同的程式碼。再次強調一下:子程序的程式碼和父程序的是一樣的。所以就要通過分支語句給他們分配不同的任務。

3.3. 子程序回收

剛剛有man ps麼?一般我習慣用ps aux加上grep命令來查詢執行著的後臺程序。其中有一列STAT,標識了每個程序的執行狀態。這裡,我們關注狀態Z:殭屍(Zombie)。當子程序比父程序先退出,而父程序沒對其做任何處理的時候,子程序將會變成殭屍程序。Oops,又跟火影裡的影分身不一樣了。鳴人的影分身被幹死了以後就自動消失了,但是這裡的子程序分身死了話還留著一個空殼在,直到父程序回收它。殭屍程序雖然不佔什麼記憶體,但是很礙眼,院子裡一堆躺著的殭屍怎麼都覺得怪怪的。(別忘了它們還佔用著PID)

一般來說,在父程序結束之前回收掛掉的子程序就可以了。在pcntl擴充套件裡面有一個pcntl_wait()函式,它會將父程序掛起,直到有一個子程序退出為止。如果有一個子程序變成了殭屍的話,它會立即返回。所有的子程序都要回收,所以多等等也沒關係啦!

3.4. 父程序先掛了

如果父程序先掛了怎麼辦?會發生什麼?什麼也不會發生,子程序依舊還在執行。但是這個時候,子程序會被交給1號程序,1號程序成為了這些子程序的繼父。1號程序會很好地處理這些程序的資源,當它們結束時1號程序會自動回收資源。所以,另一種處理殭屍程序的臨時辦法是關閉它們的父程序。

4. 訊號

一般多程序的事兒講到上面就完了,可是訊號在系統中確實是一個非常重要的東西。訊號就是訊號燈,點亮一個訊號燈,程式就會做出反應。這個你一定用過,比如說在終端下執行某個程式,等了半天也沒什麼反應,可能你會按 Ctrl+C 來關閉這個程式。實際上,這裡就是通過鍵盤向程式傳送了一箇中斷的訊號:SIGINT。有時候程序失去響應了還會執行kill [PID]命令,未加任何其他引數的話,程式會接收到一個SIGTERM訊號。程式收到上面兩個訊號的時候,預設都會結束執行,那麼是否有可能改變這種預設行為呢?必須能啊!

4.1. 註冊訊號

人是活的程式也是活的,只不過程式需要遵循人制定的規則來執行。現在開始給訊號重新設定規則,這裡用到的函式是pcntl_signal()(繼續之前為啥不先查查PHP手冊呢?)。下面這段程式將給SIGINT重新定義行為,注意看好:
// 定義一個處理器,接收到SIGINT訊號後只輸出一行資訊
function signalHandler($signal) {
    if ($signal == SIGINT) {
        echo 'signal received' . PHP_EOL;
    }
}
// 訊號註冊:當接收到SIGINT訊號時,呼叫signalHandler()函式
pcntl_signal(SIGINT, 'signalHandler');
while (true) {
    sleep(1);
    // do something
    pcntl_signal_dispatch(); // 接收到訊號時,呼叫註冊的signalHandler()
}

執行一下,隨時按下 Ctrl+C 看看會發生什麼事。

4.2. 訊號分發

說明一下:pcntl_signal()函式僅僅是註冊訊號和它的處理方法,真正接收到訊號並呼叫其處理方法的是pcntl_signal_dispatch()函式。試試把// do something替換成下面這段程式碼:
for ($i = 0; $i < 1000000; $i++) {
    echo $i . PHP_EOL;
    usleep(100000);
}

在終端下執行這個指令碼,當它不停輸出數字的時候嘗試按下 Ctrl+C 。看看程式有什麼響應?嗯……什麼都沒有,除了螢幕可能多了個^C以外,程式一直在不停地輸出數字。因為程式一直沒有執行到pcntl_signal_dispatch(),所以就並沒有呼叫signalHandler(),所以就沒有輸出signal received。

4.3. 版本問題

如果認真看了PHP文件,會發現pcntl_signal_dispatch()這個函式是PHP 5.3以上才支援的,如果你的PHP版本大於5.3,建議使用這個方法呼叫訊號處理器。5.3以下的版本需要在註冊訊號之前加一句:declare(ticks = 1);表示每執行一條低階指令,就檢查一次訊號,如果檢測到註冊的訊號,就呼叫其訊號處理器。想想就挺不爽的,幹嘛一直都檢查?還是在我們指定的地方檢查一下就好。

4.4. 感受殭屍程序

現在我們回到子程序回收的問題上(差點忘了= =")。當你的一個子程序掛了(或者說是結束了),但是父程序還在執行中並且可能很長一段時間不會退出。一個殭屍程序從此站起來了!這時,保護傘公司(核心)發現它的地盤裡出現了一個殭屍,這個殭屍是誰兒子呢?看一下PPID就知道了。然後,核心給PPID這個程序(也就是殭屍程序的父程序)傳送一個訊號:SIGCHLD。然後,你知道怎麼在父程序中回收這個子程序了麼?提示一下,用pcntl_wait()函式。

4.5. 傳送訊號

希望剛剛有認真man過kill命令。它其實就是向程序傳送訊號,在PHP中也可以呼叫posix_kill()函式來達到相同的效果。有了它就可以在父程序中控制其他子程序的運行了。比如在父程序結束之前關閉所有子程序,那麼fork的時候在父程序記錄所有子程序的PID,父程序結束之前依次給子程序傳送結束訊號即可。

5. 實踐

PHP的多程序跟C還是挺像的,搞明白了以後用其他語言寫的話也大同小異差不多都是這麼個情況。如果有空的話,嘗試寫一個小程式,切身體會一下箇中滋味:

1.16歲的鳴人傳送影分身,分出5個分身
2.每個分身隨機生存10到30秒,每秒都輸出點什麼
3.保證原身能感受到分身的結束,然後開動另一個分身,保證最多有5個分身
4.不使用nohup,讓原身在終端關閉後依舊能夠執行
5.把分身數量(5)寫進一個配置檔案裡,當給原身傳送訊號(可以考慮用SIGUSR1或SIGUSR2)時,原身讀取配置檔案並更新允許的分身最大數量
6.如果分身多了,關閉幾個;如果少了,再分出來幾個

提示:

1.用while迴圈保證程序執行,注意sleep以免100%的CPU佔用
2.執行程序的終端被關閉時,程式會收到一個SIGHUP訊號
3.可以用parse_ini_file()函式解析INI配置檔案

6.實戰演示:

    PHP有一組程序控制函式(編譯時需要–enable-pcntl與posix擴充套件),使得php能在nginx系統中實現跟c一樣的建立子程序、使用exec函式執行程式、處理訊號等功能。

CentOS 6 下yum安裝php的,預設是不安裝pcntl的,因此需要單獨編譯安裝。

6.1.演示一:

<?php  
    // 總程序的數量  
    $intSum = 5;  
    // 執行的指令碼數量  
    $cmdArr = array();  
	// 需要執行的指令碼路徑
    $strPath = '/home/root/running.php';
    // 執行的指令碼數量的陣列  
    for ($i = 0; $i < $totals; $i++) {  
        $cmdArr[] = array( "path" => $strPath,  'pid' => $i ,'sum' => $intSum );  
    }  
    /* 
    展開:$cmdArr 
    Array 
    ( 
        [0] => Array 
            ( 
                [path] => /home/root/running.php 
                [pid] => 0 
                [total] => 3 
            ) 
     ...
     ...
    ) 
    */  
    
    pcntl_signal(SIGCHLD, SIG_IGN); // 如果父程序不關心子程序什麼時候結束,子程序結束後,核心會回收。  
    
    foreach ($cmdArr as $cmd) {  
        $pid = pcntl_fork();    // 建立子程序  
        // 父程序和子程序都會執行下面程式碼  
        if ( $pid == -1 ) {  
            // 錯誤處理:建立子程序失敗時返回-1.  
            die('could not fork');  
        } else if ( $pid == 0 ) {
			// 子程序得到的$pid為0, 所以這裡是子程序執行的邏輯。  
            $path = $cmd["path"];  
            $pid = $cmd['pid'] ;  
            $total = $cmd['total'] ;  
            echo exec("/usr/bin/php {$path} {$pid} {$total}")."\n";  
            exit(0) ;  
        } else {  
         	// 父程序會得到子程序號,所以這裡是父程序執行的邏輯  
            // 如果不需要阻塞程序,而又想得到子程序的退出狀態,則可以註釋掉pcntl_wait($status)語句,或寫成:  
            pcntl_wait( $status, WNOHANG ); // 等待子程序中斷,防止子程序成為殭屍程序。  
        }  
    }  
?>  

6.2.演示二:

Running.php檔案:

<?php
$cmds=array(
        array('/home/lhc/Debug_Test/Doing.php','mobile',1),
        array('/home/lhc/Debug_Test/Doing.php','mobile',2),
);

foreach($cmds as $cmd){
        $pid=pcntl_fork();
        if($pid==-1){ //程序建立失敗
                die('fork child process failure!');
        }
        else if($pid){ //父程序處理邏輯
                pcntl_wait($status,WNOHANG);
        }
        else{ //子程序處理邏輯
                pcntl_exec('/usr/local/php/bin/php',$cmd);
        }
}

?>
Doing.php檔案:
<?php
	file_put_contents( "1.txt","##aa##\r\n",FILE_APPEND);
?>
執行結果:
1.txt檔案:
##aa##
##aa##

參考文章來源:http://www.jb51.net/article/56301.htm