大家好,我是碼農先森。
守護程序顧名思義就是能夠在後臺一直執行的程序,不會霸佔使用者的會話終端,脫離了終端的控制。相信朋友們對這東西都不陌生了吧?如果連這個概念都還不能理解的話,建議回爐重造多看看 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 中實現守護程序模式的例子,希望本次的內容能對你有所幫助。
感謝大家閱讀,個人觀點僅供參考,歡迎在評論區發表不同觀點。
歡迎關注、分享、點贊、收藏、在看,我是微信公眾號「碼農先森」作者。