多進程編程 - 孤兒進程/殭屍進程/信號量通訊

多進程編程中常常會涉及到孤兒進程/殭屍進程的概念,下面將用代碼實際的演示一遍。php

孤兒進程

孤兒進程指的是父進程結束運行時,還未運行結束的子進程。這些子進程將會成爲系統的孤兒進程,系統將會調用 pid = 1 的 init 進程來負責接管這些孤兒進程,監聽其是否運行結束,以便回收資源。c++

因此,孤兒進程對系統沒有太大的影響。在某些場景下咱們可能還會能夠的利用孤兒進程,好比編寫守護進程,或一些不須要等待執行結果的多任務時。編程

殭屍進程

殭屍進程(zombie),能夠方便的使用 top 命令來查看系統是否存在。殭屍進程產生的緣由是父進程仍在運行,但沒有對子進程運行結束時發送的 SIGCHLD 信號進行處理(經常使用的是使用 wait/waitpid 來配合處理) 或 手動捕獲 SIGCHLD 信號卻沒作任何處理(做死)。由於父進程還在,因此也沒法被系統的 init 進程接管,便成爲了會影響系統性能的殭屍進程。安全

查看系統中是否存在殭屍進程

top函數

clipboard.png

ps -A -o stat,ppid,pid,cmd | grep -e '^[Zz]'性能

clipboard.png

3261 即爲父進程的 pid,kill -9 {pid} 便可清除這些殭屍進程spa

如何避免殭屍進程

在瞭解了殭屍進程產生的緣由和影響後,咱們要儘量的避免殭屍進程的產生。3d

其實通常你隨便寫個多進程大都不會產生長期存在的殭屍進程,若是不須要同步父子進程的執行狀態,或父進程在短期內會當即退出,子進程可能會出現短暫的殭屍進程的狀態,但最終都會由於父進程退出轉爲孤兒進程被 init 回收。code

但若是父進程在提供常駐服務時建立子進程,且不使用 wait/waitpid 或 對 SIGCHLD信號 作 SIG_IGN 處理,子進程就變成殭屍進程長期佔用系統資源了。blog

SIG_DFL / SIG_IGN

// c/c++
signal(SIGCHLD, SIG_IGN)
// php
pcntl_signal(SIGCHLD, SIG_IGN)

顯式的聲明將 SIGCHLD 信號作 SIG_IGN 處理(簡單的理解成強制孤兒化子進程就好),結束的子進程則會交由系統 init 回收,父進程無需關注子進程的退出,故能夠提升服務性能。

這裏還須要理解 SIG_IGN 是在運行時層面忽略信號,而並不是捕獲了不做處理,部分信號量 SIG_IGN 和 捕獲但不處理 的效果相同,但對一些特別的信號量(SIGCHLD)兩者是有很大區別的。

<?php
// 均可以屏蔽終端暫停信號 ctrl+z
pcntl_signal(SIGTSTP, SIG_IGN);// 忽略
pcntl_signal(SIGTSTP, function () {
    // 捕獲但不作處理
});
// 均可以屏蔽終端結束信號 ctrl+c
pcntl_signal(SIGINT, SIG_IGN);// 忽略
pcntl_signal(SIGINT, function () {
    // 捕獲但不作處理
});

// 兩者結果徹底不一樣
pcntl_signal(SIGCHLD, SIG_IGN);// 忽略 交由 init 處理 安全
pcntl_signal(SIGCHLD, function () {
    // 捕獲但不作處理 就會致使子進程成爲殭屍進程 非安全
});

父進程已退出,子進程成爲孤兒進程最終被 init 回收。
父進程未退出,但不處理子進程退出時發送過來的的 SIGCHLD 信號,子進程成爲殭屍進程。

咱們只須要保證:在父進程退出前,父進程調用了 wait/waitpid 函數處理了可能來自子進程的 SIGCHLD 信號便可。

wait/waitpid 會阻塞父進程,等待/返回某子進程的退出狀態及 pid。

咱們能夠用來避免殭屍進程的產生/同步父子進程的執行狀態,在一些場景下咱們可能正須要父進程等待全部的子進程執行完畢後去彙總某些數據。

<?php
/**
 * 安全的多進程處理
 */
if (!function_exists('pcntl_fork')) {
    trigger_error("need pcntl extension!", E_USER_ERROR);
}

$workers_num = 4;
$workers_pid = [];

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

    if ($pid == -1) {
        trigger_error("child process create failed!", E_USER_ERROR);
    }

    if ($pid == 0) {
        // 子進程執行模塊
        echo "I am child pid: " . getmypid() . PHP_EOL;
        sleep(rand(1, 3));
        exit(0);
    } else {
        // 父進程管理子進程的pid
        $workers_pid[] = $pid;
    }
}

// 父進程使用 wait/waitpid 等待/處理子進程的 SIGCHLD 信號
while (true) {
    // 如有未退出的子進程
    if (! empty($workers_pid)) {
        // pcntl_wait 會阻塞/等待子進程發送的信號量
        $worker_pid = pcntl_wait($status);
        if ($status == 0) { // 正常退出
            echo 'child ' . $worker_pid . ' safe exited!' . PHP_EOL;
        } else { // exit 狀態碼
            echo 'child ' . $worker_pid . ' wrong end with status: ' . $status . PHP_EOL;
        }

        // 刪除子進程的 pid
        $key = array_search($worker_pid, $workers_pid);
        unset($workers_pid[$key]);
    } else {
        // 全部子進程都已執行完畢
        break;
    }
}

// 此時全部的子進程已執行完畢 不會有孤兒/殭屍進程產生
echo "main end" . PHP_EOL;

咱們在建立多進程任務時應該極力避免殭屍進程的產生,最經常使用的即父進程使用 wait/waitpid 函數監聽子進程的退出並將其回收,或者能夠用上面的 SIG_IGN 處理 SIGCHLD 信號,根據自身業務需求選擇正確處理方式便可。

以上的代碼父進程因 wait/waitpid 會處於阻塞狀態,等待某一個子進程執行完畢發送 SIGCHLD 信號後獲取其 pid 以及 exit_code 後才能繼續運行。有沒有什麼更好的方式呢?好比子進程能夠發送一個通知給父進程,父進程定時檢測是否有此通知,有的話就回收子進程後再去作別的工做,這就是下面要講的進程通訊 -- 信號量。

進程通訊--信號量

進程通訊的方式有:管道,信號量,消息隊列,共享內存。

這裏咱們簡單的使用信號量來進行子父進程間的通訊,通訊目的也很簡單:子執行完畢時通知父將其快點回收,別丟那裏無論不問成了殭屍進程。

若是不使用信號量通訊

<?php

for ($i = 0; $i < 4; $i++) {
    $pid = pcntl_fork();
    
    if ($pid == -1) {
        trigger_error("child process create failed!" . PHP_EOL, E_USER_ERROR);
    }

    if ($pid == 0) {
        // -- child process code --
        echo "child: " . getmypid() . " running!" . PHP_EOL;
        sleep(rand(1, 3));
        // child process exit code 能夠被父進程接受到以判別子進程的退出狀態
        exit(0);
        // -- child process code --
    } else {
        // father process code
        $children_pid_set[] = $pid;
        echo "father: child " . $pid . " created!" . PHP_EOL;
    }
}

while (true) {
    // 這裏會阻塞直到接收到子進程發送的 SIGCHLD 信號
    $child_pid = pcntl_wait($status);
    
    // 刪除子進程的 pid
    $key = array_search($child_pid, $children_pid_set);
    unset($children_pid_set[$key]);
    
    if (empty($children_pid_set)) {
        echo "all children process run finished!" . PHP_EOL;
        break;
    }
}

echo "father process run finished!" . PHP_EOL;

即父進程會被 pcntl_await 阻塞而不能作其餘的事情,若是咱們用信號量通訊就能夠更爲靈活。

<?php
/**
 * @author big_cat
 * @version 0.0.1
 * 非阻塞版的父進程建立管理子進程
 * 採用 SIGCHLD 通訊方式 父進程使用 pcntl_signal_dispatch 定時檢測有無子進程的信號量
 * 若有則調用相應的註冊好的方法回收子進程
 * 如沒有則繼續父進程的業務
 */
if (!extension_loaded('pcntl')) {
    trigger_error("need pcntl extension!", E_USER_ERROR);
}

// 子進程數量
$children_num = 4;
// 存放子進程 pid
$children_pid_set = [];
// 是否退出執行
$sign_exit = false;

// 捕獲子進程的退出信號 -- SIGCHLD 進行子進程回收
pcntl_signal(SIGCHLD, function ($signo) use (&$children_pid_set) {
    // 回收子進程 防止成爲殭屍進程
    $child_pid = pcntl_wait($status);

    // 刪除子進程的 pid
    $key = array_search($child_pid, $children_pid_set);
    unset($children_pid_set[$key]);

    if (0 == $status) {
        echo "child: " . $child_pid . " run finished!" . PHP_EOL;
    } else {
        echo "child: " . $child_pid . " run error and exit!" . PHP_EOL;
    }
});

// 作個軟退出 -- SIGINT
pcntl_signal(SIGINT, function ($signo) use (&$sign_exit) {
    // 捕獲 SIGINT ctrl+c 的退出執行命令後
    // 咱們能可控的作一些退出清理工做
    $sign_exit = true;
});

/**
 * 建立必定數量的子進程
 * @param  [type] $process_num  建立的數量
 * @param  [type] &$children_pid_set 全局的子進程pid
 * @return [type]                [description]
 */
function process_pool($process_num, &$children_pid_set)
{
    // 建立子進程
    for ($i = 0; $i < $process_num; $i++) {
        $pid = pcntl_fork();

        if ($pid == -1) {
            trigger_error("child process create failed!" . PHP_EOL, E_USER_ERROR);
        }

        if ($pid == 0) {
            // -- child process code --
            echo "child: " . getmypid() . " running!" . PHP_EOL;
            sleep(rand(1, 3));
            // child process exit code 能夠被父進程接受到以判別子進程的退出狀態
            exit(0);
            // -- child process code --
        } else {
            // father process code
            $children_pid_set[] = $pid;
            echo "father: child " . $pid . " created!" . PHP_EOL;
        }
    }
}

// 預先建立若干個子進程
process_pool($children_num, $children_pid_set);

// 父進程使用 wait 函數等待全部子進程執行完畢
while (true) {
    // echo "father process running..." . PHP_EOL;

    // 始終維持 $children_num 個子進程
    if (($need_create = $children_num - count($children_pid_set)) > 0) {
        process_pool($need_create, $children_pid_set);
    }

    // PHP 信號捕獲回調須要特定的使用此函數進行分發處理
    // declare(ticks=1) 存在浪費性能的可能
    // 故在主循環體中加入信號時間分發器
    pcntl_signal_dispatch();

    // 經過捕獲 SIGINT 信號來實現軟退出
    if ($sign_exit) {
        break;
    }

    // 模擬父進程耗時處理其餘業務
    sleep(2000);
}

if (!empty($children_pid_set)) {
    // 可能會有一些還未結束的子進程 但無需擔憂 父進程退出後他們會成爲孤兒進程被 init 接管
    // 你也能夠自行對這些子進程作處理
    echo implode(" ", $children_pid_set) . ' are still running! will be ctrled by init process' . PHP_EOL;
}

echo "father process run finished!" . PHP_EOL;

源碼解讀:一、父進程註冊信號量 SIGCHLD 的 handler 方法,咱們應在此信號量的 handler方法中作 pcntl_wait() 用來處理回收發送此信息號的子進程。二、父進程建立子進程,並進入非阻塞循環,使用 pcntl_signal_dispatch() 來檢測是否有信號待處理(使用 declare(ticks=1) 存在一些性能浪費的可能),如有待處理的信號,則父進程調用 pcntl_signal() 註冊的信號及handler,若沒有,則繼續執行其餘業務。三、SIGCHLD 信號的 handler 中應使用 pcntl_wait() 方法來回收子進程,防止殭屍進程。

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