用PHP玩轉進程之二 — 多進程PHPServer

首發於 樊浩柏科學院

通過 用 PHP 玩轉進程之一 — 基礎 的回顧複習,咱們已經掌握了進程的基礎知識,如今能夠嘗試用 PHP 作一些簡單的進程控制和管理,來加深咱們對進程的理解。接下來,我將用多進程模型實現一個簡單的 PHPServer,基於它你能夠作任何事。php

預覽圖

PHPServer 完整的源代碼,可前往 fan-haobai/php-server 獲取。html

總流程

該 PHPServer 的 Master 和 Worker 進程主要控制流程,以下圖所示:git

控制流程

其中,主要涉及 3 個對象,分別爲 入口腳本Master 進程Worker 進程。它們扮演的角色以下:github

  • 入口腳本:主要實現 PHPServer 的啓動、中止、重載功能,即觸發 Master 進程startstopreload流程;
  • Master 進程:負責建立並監控 Worker 進程。在啓動階段,會註冊信號處理器,而後建立 Worker;在運行階段,會持續監控 Worker 進程健康狀態,並接受來自入口腳本的控制信號並做出響應;在中止階段,會中止掉全部 Worker 進程;
  • Worker 進程:負責執行業務邏輯。在被 Master 進程建立後,就處於持續運行階段,會監聽到來自 Master 進程的信號,以實現自個人中止;

整個過程,又包括 4 個流程bash

  • 流程 ① :以守護態啓動 PHPServer 時的主要流程。入口腳本會進行 daemonize,也就是實現進程的守護態,此時會fork出一個 Master 進程;Master 進程先通過 保存 PID註冊信號處理器 操做,而後 建立 Workerfork出多個 Worker 進程;
  • 流程 ② :爲 Master 進程持續監控的流程,過程當中會捕獲入口腳本發送來的信號。主要監控 Worker 進程健康狀態,當 Worker 進程異常退出時,會嘗試建立新的 Worker 進程以維持 Worker 進程數量;
  • 流程 ③ :爲 Worker 進程持續運行的流程,過程當中會捕獲 Master 進程發送來的信號。流程 ① 中 Worker 進程被建立後,就會持續執行業務邏輯,並阻塞於此;
  • 流程 ④ :中止 PHPServer 的主要流程。入口腳本首先會向 Master 進程發送 SIGINT 信號,Master 進程捕獲到該信號後,會向全部的 Worker 進程轉發 SIGINT 信號(通知全部的 Worker 進程終止),等待全部 Worker 進程終止退出;
在流程 ② 中,Worker 進程被 Master 進程 fork出來後,就會 持續運行 並阻塞於此,只有 Master 進程纔會繼續後續的流程。

代碼實現

啓動

啓動流程見 流程 ①,主要包括 守護進程保存 PID註冊信號處理器建立多進程 Worker 這 4 部分。工具

守護進程

首先,在入口腳本中fork一個子進程,而後該進程退出,並設置新的子進程爲會話組長,此時的這個子進程就會脫離當前終端的控制。以下圖所示:spa

守護進程流程

這裏使用了 2 次fork,因此最後fork的一個子進程纔是 Master 進程,其實一次fork也是能夠的。代碼以下:code

protected static function daemonize()
{
    umask(0);
    $pid = pcntl_fork();
    if (-1 === $pid) {
        exit("process fork fail\n");
    } elseif ($pid > 0) {
        exit(0);
    }

    // 將當前進程提高爲會話leader
    if (-1 === posix_setsid()) {
        exit("process setsid fail\n");
    }

    // 再次fork以免SVR4這種系統終端再一次獲取到進程控制
    $pid = pcntl_fork();
    if (-1 === $pid) {
        exit("process fork fail\n");
    } elseif (0 !== $pid) {
        exit(0);
    }
}
一般在啓動時增長 -d參數,表示進程將運行於守護態模式。

當順利成爲一個守護進程後,Master 進程已經脫離了終端控制,因此有必要關閉標準輸出和標準錯誤輸出。以下:orm

protected static function resetStdFd()
{
    global $STDERR, $STDOUT;
    //重定向標準輸出和錯誤輸出
    @fclose(STDOUT);
    fclose(STDERR);
    $STDOUT = fopen(static::$stdoutFile, 'a');
    $STDERR = fopen(static::$stdoutFile, 'a');
}

保存PID

爲了實現 PHPServer 的重載或中止,咱們須要將 Master 進程的 PID 保存於 PID 文件中,如php-server.pid文件。代碼以下:server

protected static function saveMasterPid()
{
    // 保存pid以實現重載和中止
    static::$_masterPid = posix_getpid();
    if (false === file_put_contents(static::$pidFile, static::$_masterPid)) {
        exit("can not save pid to" . static::$pidFile . "\n");
    }

    echo "PHPServer start\t \033[32m [OK] \033[0m\n";
}

註冊信號處理器

由於守護進程一旦脫離了終端控制,就猶如一匹脫繮的野馬,任由其奔騰可能會隨心所欲,因此咱們須要去馴服它。

這裏使用信號來實現進程間通訊並控制進程的行爲,註冊信號處理器以下:

protected static function installSignal()
{
    pcntl_signal(SIGINT, array('\PHPServer\Worker', 'signalHandler'), false);
    pcntl_signal(SIGTERM, array('\PHPServer\Worker', 'signalHandler'), false);

    pcntl_signal(SIGUSR1, array('\PHPServer\Worker', 'signalHandler'), false);
    pcntl_signal(SIGQUIT, array('\PHPServer\Worker', 'signalHandler'), false);

    // 忽略信號
    pcntl_signal(SIGUSR2, SIG_IGN, false);
    pcntl_signal(SIGHUP,  SIG_IGN, false);
}

protected static function signalHandler($signal)
{
    switch($signal) {
        case SIGINT:
        case SIGTERM:
            static::stop();
            break;
        case SIGQUIT:
        case SIGUSR1:
            static::reload();
            break;
        default: break;
    }
}

其中,SIGINT 和 SIGTERM 信號會觸發stop操做,即終止全部進程;SIGQUIT 和 SIGUSR1 信號會觸發reload操做,即從新加載全部 Worker 進程;此處忽略了 SIGUSR2 和 SIGHUP 信號,可是並未忽略 SIGKILL 信號,即全部進程均可以被強制kill掉。

建立多進程Worker

Master 進程經過fork系統調用,就能建立多個 Worker 進程。實現代碼,以下:

protected static function forkOneWorker()
{
    $pid = pcntl_fork();

    // 父進程
    if ($pid > 0) {
        static::$_workers[] = $pid;
    } else if ($pid === 0) { // 子進程
        static::setProcessTitle('PHPServer: worker');

        // 子進程會阻塞在這裏
        static::run();

        // 子進程退出
        exit(0);
    } else {
        throw new \Exception("fork one worker fail");
    }
}

protected static function forkWorkers()
{
    while(count(static::$_workers) < static::$workerCount) {
        static::forkOneWorker();
    }
}

Worker進程的持續運行

Worker 進程的持續運行,見 流程 ③ 。其內部調度流程,以下圖:

Worker進程的持續運行

對於 Worker 進程,run()方法主要執行具體業務邏輯,固然 Worker 進程會被阻塞於此。對於 任務 ① 這裏簡單地使用while來模擬調度,實際中應該使用事件(Select 等)驅動。

public static function run()
{
    // 模擬調度,實際用event實現
    while (1) {
        // 捕獲信號
        pcntl_signal_dispatch();

        call_user_func(function() {
            // do something
            usleep(200);
        });
    }
}

其中,pcntl_signal_dispatch()會在每次調度過程當中,捕獲信號並執行註冊的信號處理器。

Master進程的持續監控

調度流程

Master 進程的持續監控,見 流程 ② 。其內部調度流程,以下圖:

Master持續監控流程

對於 Master 進程的調度,這裏也使用了while,可是引入了wait的系統調用,它會掛起當前進程,直到一個子進程退出或接收到一個信號。

protected static function monitor()
{
    while (1) {
        // 這兩處捕獲觸發信號,很重要
        pcntl_signal_dispatch();
        // 掛起當前進程的執行直到一個子進程退出或接收到一個信號
        $status = 0;
        $pid = pcntl_wait($status, WUNTRACED);
        pcntl_signal_dispatch();

        if ($pid >= 0) {
            // worker健康檢查
            static::checkWorkerAlive();
        }
        // 其餘你想監控的
    }
}
第兩次的 pcntl_signal_dispatch()捕獲信號,是因爲 wait掛起時間可能會很長,而這段時間可能偏偏會有信號,因此須要再次進行捕獲。

其中,PHPServer 的 中止重載 操做是由信號觸發,在信號處理器中完成具體操做;Worker 進程的健康檢查 會在每一次的調度過程當中觸發。

Worker進程的健康檢查

因爲 Worker 進程執行繁重的業務邏輯,因此可能會異常崩潰。所以 Master 進程須要監控 Worker 進程健康狀態,並嘗試維持必定數量的 Worker 進程。健康檢查流程,以下圖:

健康檢查流程

代碼實現,以下:

protected static function checkWorkerAlive()
{
    $allWorkerPid = static::getAllWorkerPid();
    foreach ($allWorkerPid as $index => $pid) {
        if (!static::isAlive($pid)) {
            unset(static::$_workers[$index]);
        }
    }

    static::forkWorkers();
}

中止

Master 進程的持續監控,見 流程 ④ 。其詳細流程,以下圖:

中止流程

入口腳本給 Master 進程發送 SIGINT 信號,Master 進程捕獲到該信號並執行 信號處理器,調用stop()方法。以下:

protected static function stop()
{
    // 主進程給全部子進程發送退出信號
    if (static::$_masterPid === posix_getpid()) {
        static::stopAllWorkers();

        if (is_file(static::$pidFile)) {
            @unlink(static::$pidFile);
        }
        exit(0);
    } else { // 子進程退出

        // 退出前能夠作一些事
        exit(0);
    }
}

如果 Master 進程執行該方法,會先調用stopAllWorkers()方法,向全部的 Worker 進程發送 SIGINT 信號並等待全部 Worker 進程終止退出,再清除 PID 文件並退出。有一種特殊狀況,Worker 進程退出超時時,Master 進程則會再次發送 SIGKILL 信號強制殺死全部 Worker 進程;

因爲 Master 進程會發送 SIGINT 信號給 Worker 進程,因此 Worker 進程也會執行該方法,並會直接退出。

protected static function stopAllWorkers()
{
    $allWorkerPid = static::getAllWorkerPid();
    foreach ($allWorkerPid as $workerPid) {
        posix_kill($workerPid, SIGINT);
    }

    // 子進程退出異常,強制kill
    usleep(1000);
    if (static::isAlive($allWorkerPid)) {
        foreach ($allWorkerPid as $workerPid) {
            static::forceKill($workerPid);
        }
    }

    // 清空worker實例
    static::$_workers = array();
}

重載

代碼發佈後,每每都須要進行從新加載。其實,重載過程只須要重啓全部 Worker 進程便可。流程以下圖:

重載流程

整個過程共有 2 個流程,流程 ① 終止全部的 Worker 進程,流程 ② 爲 Worker 進程的健康檢查 。其中流程 ① ,入口腳本給 Master 進程發送 SIGUSR1 信號,Master 進程捕獲到該信號,執行信號處理器調用reload()方法,reload()方法調用stopAllWorkers()方法。以下:

protected static function reload()
{
    // 中止全部worker便可,master會自動fork新worker
    static::stopAllWorkers();
}
reload()方法只會在 Master 進程中執行,由於 SIGQUIT 和 SIGUSR1 信號不會發送給 Worker 進程。

你可能會納悶,爲何咱們須要重啓全部的 Worker 進程,而這裏只是中止了全部的 Worker 進程?這是由於,在 Worker 進程終止退出後,因爲 Master 進程對 Worker 進程的健康檢查 做用,會自動從新建立全部 Worker 進程。

運行效果

到這裏,咱們已經完成了一個多進程 PHPServer。咱們來體驗一下:

$ php server.php 
Usage: Commands [mode] 

Commands:
start        Start worker.
stop        Stop worker.
reload        Reload codes.

Options:
-d        to start in DAEMON mode.

Use "--help" for more information about a command.

首先,咱們啓動它:

$ php server.php start -d
PHPServer start      [OK]

其次,查看進程樹,以下:

$ pstree -p
init(1)-+-init(3)---bash(4)
        |-php(1286)-+-php(1287)
                    `-php(1288)

最後,咱們把它中止:

$ php server.php stop
PHPServer stopping ...
PHPServer stop success

如今,你是否是感受進程控制其實很簡單,並無咱們想象的那麼複雜。( ̄┰ ̄*)

總結

咱們已經實現了一個簡易的多進程 PHPServer,模擬了進程的管理與控制。須要說明的是,Master 進程可能偶爾也會異常地崩潰,爲了不這種狀況的發生:

首先,咱們不該該給 Master 進程分配繁重的任務,它更適合作一些相似於調度和管理性質的工做;
其次,可使用 Supervisor 等工具來管理咱們的程序,當 Master 進程異常崩潰時,能夠再次嘗試被拉起,避免 Master 進程異常退出的狀況發生。

相關文章 »

相關文章
相關標籤/搜索