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