聊聊 PHP 多程序模式下的孤兒程序和殭屍程序

Yxh_blogs發表於2024-08-22

大家好,我是碼農先森。

在 PHP 的程式設計實踐中多程序通常都是在 cli 指令碼的模式下使用,我依稀還記得在多年以前為了實現從資料庫匯出千萬級別的資料,第一次在 PHP 指令碼中採用了多程序程式設計。在此之前我從未接觸過多程序,只知道 PHP-FPM 程序管理器是多程序模型,但從未在程式設計中進行實踐。多程序雖然能帶來效率上的提升,但依然會帶來不少的問題,如果初學者使用多程序,那註定會遇到各種奇奇怪怪的 Bug 比如併發運算元據庫引起死鎖、共用記憶體變數資源造成串資料、忘記回收程序資源導致產生孤兒程序、殭屍程序等。反正如果我們長期都是 PHP-FPM 模式下程式設計的話,在使用多程序程式設計時需要慎之又慎,避免出現意想不到的問題。不過這次我想分享的內容是多程序模式下的孤兒程序和殭屍程序,透過示例程式碼來看看這兩者程序是如何產生的,又應該如何解決,內容不難但是在實際的程式設計中是可能比較容易忽視的點。

按照慣例我們先看看孤兒程序和殭屍程序的基礎概念。

  • 孤兒程序:是指一個程序的父程序已經終止,但該子程序仍然在執行。當父程序結束時,作業系統會將其所有的子程序重新分配給 init 程序。init 程序會負責這些孤兒程序,並確保它們能夠正確結束。孤兒程序不會造成資源洩漏,因為最終它們會被 init 程序管理並正確清理。
  • 殭屍程序:是指一個已經完成執行的程序,但仍在程序表中保留了一些資訊。這通常發生在父程序未呼叫 wait() 或相關函式來獲取子程序的退出狀態時。殭屍程序處於 Z 狀態,是一種佔用系統資源但不佔用 CPU 的程序。殭屍程序會繼續佔用系統的程序 ID,如果大量產生將導致程序 ID 耗盡,可能會影響系統的正常執行。

這兩者程序的基礎概念應該還比較好理解,孤兒程序的產生就是緣於父程序的不負責,自己先跑路了,導致自己的子程序變成了孤兒,最後孤兒程序被系統給回收了,可以理解為被政府的福利院收養了。殭屍程序的產生就是兒子程序執行完了沒有退出,但是父程序又不知情,無法及時回收兒子程序的資源,導致自己的兒子程序變成了殭屍程序,殭屍程序往往比孤兒程序對系統的危害更大,接下來我們來看看具體的程式碼示例。

首先看看孤兒程序示例,使用 pcntl_fork 函式建立了一個子程序,子程序會每間隔 1 秒鐘獲取一次自己程序的 ID 和父程序的 ID,而父程序在 2 秒鐘之後就退出跑路了,自此子程序就變成了孤兒程序,被系統程序收養了。

<?php

// 孤兒程序示例

$pid = pcntl_fork();
if ($pid < 0) {
   exit('fork error');
} else if($pid > 0) {
   // 父程序執行空間 ...
   // getmypid 函式獲取當前父程序ID
   echo "父程序ID: " . getmypid() . PHP_EOL;

   // 2 秒之後退出當前的父程序
   // 父程序先行跑路了
   sleep(2);
   exit();
}

// 子程序執行空間 ...
// getmypid 函式獲取當前子程序ID
$cid = getmypid();
echo "當前子程序: {$cid}" . PHP_EOL;

// 每隔 1 秒獲取一下程序ID
for($i = 1; $i <= 10; $i++){
    // posix_getppid 函式獲取當前子程序的父程序ID
    sleep(1);
    echo "當前子程序ID: " . $cid. ", 父程序ID: " . posix_getppid() . PHP_EOL;
}

// 由於父程序跑路了,子程序變成了孤兒程序 ...

執行 php index.php 觀察輸出結果,可以看出間隔一段時間之後父程序的 ID 就變成 1 了,即為系統程序。

## 執行程式
[manongsen@root php_test]$ php index.php 
父程序ID: 3484
當前子程序: 3485
當前子程序ID: 3485, 父程序ID: 3484
當前子程序ID: 3485, 父程序ID: 3484
當前子程序ID: 3485, 父程序ID: 1
當前子程序ID: 3485, 父程序ID: 1
當前子程序ID: 3485, 父程序ID: 1
當前子程序ID: 3485, 父程序ID: 1
當前子程序ID: 3485, 父程序ID: 1
當前子程序ID: 3485, 父程序ID: 1
當前子程序ID: 3485, 父程序ID: 1
當前子程序ID: 3485, 父程序ID: 1

然後再看看殭屍程序示例,同樣也使用 pcntl_fork 建立了一個子程序,然後子程序先行執行完了,父程序還未執行完,這時子程序變成為了殭屍程序。當然殭屍程序也不會一直存在,如果父程序退出了其也會結束自身程序,反之就會一直存在佔用著系統資源。

<?php

// 殭屍程序示例

$pid = pcntl_fork();
if ($pid < 0) {
   exit('fork error');
} else if($pid > 0) {
   // 父程序執行空間 ...
   // getmypid 函式獲取當前父程序ID
   echo "父程序ID: " . getmypid() . PHP_EOL;

   // 120 秒之後退出當前的父程序
   sleep(120);
   exit();
}

// 子程序執行空間 ...
// getmypid 函式獲取當前子程序ID
$cid = getmypid();
echo "當前子程序: {$cid}" . PHP_EOL;

// 10 秒之後退出子程序
sleep(10);

執行 php index.php 觀察輸出結果,透過檢視子程序資訊中有一個 Z+ 標識,則表示該程序已經成為了殭屍程序。

## 執行程式
[manongsen@root php_test]$ php index.php 
父程序ID: 85804
當前子程序: 85805

## 檢視程序資訊
[manongsen@root php_test]$ ps aux | grep 85805
root             90776   0.0  0.0 408169072   1408 s060  U+    22:06下午   0:00.00 grep 85805
root             85805   0.0  0.0         0      0 s062  Z+    22:06下午   0:00.00 (php)

最後來看看正常程序的示例,也先使用 pcntl_fork 建立了一個子程序,但與上面兩個例子不同的是在其父程序中會呼叫 pcntl_wait 函式一直等待子程序結束。在子程序 10 秒鐘過後,父程序會接受到子程序執行完畢的通知,然後回收子程序的資源。

<?php

// 正常程序示例

$pid = pcntl_fork();
if ($pid < 0) {
   exit('fork error');
} else if($pid > 0) {
    // 父程序執行空間 ...
    // getmypid 函式獲取當前父程序ID
    echo "父程序ID: " . getmypid() . PHP_EOL;

    // 一直等待到子程序結束後回收資源
    $cid = pcntl_wait($status);
    echo "父程序ID: " . getmypid() . ", 接收到子程序ID: {$cid} 退出" . PHP_EOL;
    exit();
}

// 子程序執行空間 ...
// getmypid 函式獲取當前子程序ID
$cid = getmypid();
echo "當前子程序: {$cid}" . PHP_EOL;

// 睡眠 10 秒
sleep(10);

執行 php index.php 觀察輸出結果,可以看出子程序執行完畢之後,父程序接收到了子程序的通知。

## 執行程式
[manongsen@root php_test]$ php index.php 
父程序ID: 49954
當前子程序: 49955
父程序ID: 49954, 接收到子程序ID: 49955 退出

## 檢視程序 49955
[manongsen@root php_test]$ ps aux | grep 49955
root             19516   0.0  0.0 407972944   1216 s062  R+    22:23下午   0:00.00 grep 49955
root             49955   0.0  0.0 437931336    372 s060  S+    22:23下午   0:00.00 php index.php

## 再次檢視程序 49955
[manongsen@root php_test]$ ps aux | grep 49955
root             26599   0.0  0.0 407963440    480 s062  R+    22:24下午   0:00.00 grep 49955

透過這上面的例子可以看出,多程序中正確的使用方式是要在父程序中使用 pcntl_wait 函式等待子程序的結束,而不是隻管 pcntl_fork 生產完子程序,然後就對子程序不聞不問了。從生活化的例子來說就是,你不能只管生娃,生完之後就不管養育了,這種操作肯定是不行的,道德和法律層面這一關你都過不去。利用 pcntl_wait 這個函式可以很優雅的解決了孤兒程序和殭屍程序,但在實際的程式設計中很容易忽視這一點,因此這一點值得注意。本次分享的內容就到這裡了,希望對大家能有所幫助。

感謝閱讀,個人觀點僅供參考,歡迎在評論區發表不同觀點。


歡迎關注、分享、點贊、收藏、在看,我是微信公眾號「碼農先森」作者。

相關文章