深入理解 PHP 高效能框架 Workerman 守護程序原理

Yxh_blogs發表於2024-08-12

大家好,我是碼農先森。

守護程序顧名思義就是能夠在後臺一直執行的程序,不會霸佔使用者的會話終端,脫離了終端的控制。相信朋友們對這東西都不陌生了吧?如果連這個概念都還不能理解的話,建議回爐重造多看看 Linux 程序管理相關的基礎知識。在我們日常的程式設計中常見有類似 php think ...php artisan ...php yii ... 等命令啟動需要一直執行的任務,都會透過 nohup 掛載到後臺保持長期執行的狀態。同樣在 Workerman 中也是使用類似 php index.php start 的命令來啟動程序,但不同的是它不需要利用 nohup 便可以掛載到後臺執行。那有些朋友就會好奇它是怎麼實現的呢?為了解決朋友們的疑惑,我們今天就重點深入分析一下 Workerman 守護程序的實現原理。

我們先了解一些程序相關的知識:

  • 父程序:父程序是生成其他程序的程序。當一個程序建立了另一個程序時,建立者被稱為父程序,而被建立的程序則成為子程序。父程序可以透過程序識別符號(PID)來識別它所建立的子程序。
  • 子程序:子程序是由父程序建立的新程序。子程序繼承了父程序的一些屬性,例如環境變數、檔案描述符等。子程序獨立於父程序執行,它可以執行自己的程式碼,並且具有自己的資源和記憶體空間。
  • 程序組:程序組是一組相關聯的程序的集合。每個程序組都有一個唯一的程序組ID(PGID),用於標識該程序組。程序組通常由一個父程序建立,並且包含了與父程序具有相同會話ID(SID)的所有子程序。
  • 會話:會話是一組關聯程序的集合,通常由使用者登入到系統開始,直至使用者登出或關閉終端會話結束,一個會話中的程序共享相同的控制終端。每個會話都有一個唯一的會話ID(SID),用於標識該會話。會話通常包含一個或多個程序組,其中第一個程序組成為會話的主程序組。

這些概念俗稱八股文,向來都不怎麼好理解,那我們來看個例子。執行了命令 php index.php 便產生了程序 61052「該程序的父程序是 Bash 程序 8243,這裡不用管它」,然後透過 Fork 建立了子程序 61053 且其父程序就是 61052,這兩個程序擁有共同的程序組 61052 和會話 8243。呼叫 posix_setsid 函式,將會為子程序 61053 開啟新的程序組 61053 和新的會話 61053,這裡的會話可以理解為一個新的命令視窗終端。最後子程序 61053 透過 Fork 建立了子程序 61054,程序 61053 升級成了父程序,這裡再次 Fork 的原因是要避免被終端控制程序所關聯,這個程序 61052 是在終端的模式下建立的,自此程序 61054 就形成了守護程序。

[manongsen@root phpwork]$ php index.php
[parent] 程序ID: 61052, 父程序ID: 8243, 程序組ID: 61052, 會話ID: 8243 
[parent1] 程序ID: 61052, 父程序ID: 8243, 程序組ID: 61052, 會話ID: 8243 退出了該程序
[child1] 程序ID: 61053, 父程序ID: 61052, 程序組ID: 61052, 會話ID: 8243 
[child1] 程序ID: 61053, 父程序ID: 61052, 程序組ID: 61053, 會話ID: 61053 
[parent2] 程序ID: 61053, 父程序ID: 61052, 程序組ID: 61053, 會話ID: 61053 退出了該程序
[child2] 程序ID: 61054, 父程序ID: 61053, 程序組ID: 61053, 會話ID: 61053 保留了該程序

[manongsen@root phpwork]$ ps aux | grep index.php
root             66064   0.0  0.0 408105040   1472 s080  S+   10:00下午   0:00.00 grep index.php
root             61054   0.0  0.0 438073488    280   ??  S    10:00下午   0:00.00 php index.php

上面舉例的程序資訊,正是這段程式碼執行所產生的。如果看了這段程式碼且細心的朋友,會發現為什麼 posix_setsid 這個函式不放在第一次 Fork 前呼叫,而在第二次 Fork 前呼叫呢,這樣的話就不用 Fork 兩次了?原因是組長程序是不能建立會話的,程序組ID 61052 和程序ID 61052 相同「即當前程序則為組長程序」,所以需要子程序來建立新的會話,這一點需要特別注意一下。

<?php

function echoMsg($prefix, $suffix="") {
    // 程序ID
    $pid = getmypid(); 
    // 程序組ID
    $pgid = posix_getpgid($pid);
    // 會話ID
    $sid = posix_getsid($pid); 
    // 父程序ID
    $ppid = posix_getppid();

    echo "[{$prefix}] 程序ID: {$pid}, 父程序ID: {$ppid}, 程序組ID: {$pgid}, 會話ID: {$sid} {$suffix}" . PHP_EOL;
}

// [parent] 程序ID: 61052, 父程序ID: 8243, 程序組ID: 61052, 會話ID: 8243
echoMsg("parent");

// 第一次 Fork 程序  
$pid = pcntl_fork();
if ( $pid < 0 ) {
    exit('fork error');
} else if( $pid > 0 ) {
    // [parent1] 程序ID: 61052, 父程序ID: 8243, 程序組ID: 61052, 會話ID: 8243 退出了該程序
    echoMsg("parent1", "退出了該程序");
    exit;
}

// 建立的 子程序ID 為 61053 但 程序組、會話 還是和父程序是同一個
// [child1] 程序ID: 61053, 父程序ID: 61052, 程序組ID: 61052, 會話ID: 8243 
echoMsg("child1");

// 呼叫 posix_setsid 函式,會建立一個新的會話和程序組,並設定 程序組ID 和 會話ID 為該 程序ID
if (-1 === \posix_setsid()) {
    throw new Exception("Setsid fail");
}

// 現在會發現 程序組ID 和 會話ID 都變成了 61053 在這裡相當於啟動了一個類似 Linux 終端下的會話視窗
// [child1] 程序ID: 61053, 父程序ID: 61052, 程序組ID: 61053, 會話ID: 61053 
echoMsg("child1");

// 第二次 Fork 程序
// 這裡需要二次 Fork 程序的原因是避免被終端控制程序所關聯,這個程序 61052 是在終端的模式下建立的
// 需要脫離這個程序 61052 以確保守護程序的穩定
$pid = pcntl_fork();
if ( $pid  < 0 ){
    exit('fork error');
} else if( $pid > 0 ) {
    // [parent2] 程序ID: 61053, 父程序ID: 61052, 程序組ID: 61053, 會話ID: 61053 退出了該程序
    echoMsg("parent2", "退出了該程序");
    exit;
}

// 到這裡該程序已經脫離了終端程序的控制,形成了守護程序
// [child2] 程序ID: 61054, 父程序ID: 61053, 程序組ID: 61053, 會話ID: 61053 保留了該程序
echoMsg("child2", "保留了該程序");

sleep(100);

有時間的朋友最好自行執行程式碼並分析一遍,會有不一樣的收穫。這裡假裝你已經實踐過了,這下我們來看 Workerman 的 Worker.php 檔案中 554 行的 runAll 方法中的 static::daemonize() 這個函式,實現的流程邏輯和上面的例子幾乎一樣。不過這裡還使用了 umask 這個函式,其主要的作用是為該程序所建立的檔案或目錄賦予相應的許可權,保證有許可權操作檔案或目錄。

// workerman/Worker.php:554
/**
 * Run all worker instances.
 * 執行程序
 * @return void
 */
public static function runAll()
{
    static::checkSapiEnv();
    static::init();
    static::parseCommand();
    static::lock();
    // 建立程序並形成守護程序
    static::daemonize();
    static::initWorkers();
    static::installSignal();
    static::saveMasterPid();
    static::lock(\LOCK_UN);
    static::displayUI();
    static::forkWorkers();
    static::resetStd();
    static::monitorWorkers();
}

// workerman/Worker.php:1262
/**
 * Run as daemon mode.
 * 使用守護程序模式執行
 * @throws Exception
 */
protected static function daemonize()
{
	// 判斷是否已經是守護狀態、以及當前系統是否是 Linux 環境
    if (!static::$daemonize || static::$_OS !== \OS_TYPE_LINUX) {
        return;
    }
    
    // 設定 umask 為 0 則當前程序建立的檔案許可權都為 777 擁有最高許可權
    \umask(0);
    
    // 第一次建立程序
    $pid = \pcntl_fork();
    if (-1 === $pid) {
    	// 建立程序失敗
        throw new Exception('Fork fail');
    } elseif ($pid > 0) {
    	// 主程序退出
        exit(0);
    }

	// 子程序繼續執行...
    // 呼叫 posix_setsid 函式,可以讓程序脫離父程序,轉變為守護程序
    if (-1 === \posix_setsid()) {
        throw new Exception("Setsid fail");
    }

	// 第二次建立程序,在基於 System V 的系統中,透過再次 Fork 父程序退出
	// 保證形成的守護程序,不會成為會話首程序,不會擁有控制終端
    $pid = \pcntl_fork();
    if (-1 === $pid) {
    	// 建立程序失敗
        throw new Exception("Fork fail");
    } elseif (0 !== $pid) {
    	// 主程序退出
        exit(0);
    }

    // 子程序繼續執行...
}

守護程序也是 Workerman 中重要的一部分,它保障了 Workerman 程序的穩定性。不像我們透過 nohup 啟動的命令,掛起到後臺之後,有時還神不知鬼不覺的就掛了,朋友們或許都有這樣的經歷吧。當然在市面上也有一些開源的守護程序管理軟體,比如 supervisor 等,其次還有人利用會話終端 screen、tmux 等工具來實現。其實守護程序的實現方式有多種多樣,我們這裡只是為了分析 Workerman 中守護程序的實現原理,而引出了在 PHP 中實現守護程序模式的例子,希望本次的內容能對你有所幫助。

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


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

相關文章