在上一篇文章《使用 swoole 實現進程的守護(二)》中,實現了一個能通過讀取配置同時守護多個腳本的 Daemon 類。
本文嘗試繼續擴展這個 Daemon 類,讓它能夠在不重啓進程的情況下實現配置的重載。
最常見的一種熱重載的方式,就是向進程發送系統信號,當進程監聽到相應信號時,即執行重新加載配置到進程空間的內存即可。
像 Nginx 和 Caddy 這種高性能的常駐進程服務器,爲了避免重啓進程導致的服務器不可用,也是通過這種方式來實現熱重載的。
在 Linux 的 bash 可以通過 kill -l 命令來查看所有支持的進程信號
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP
6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR
31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3
38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8
43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7
58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2
63) SIGRTMAX-1 64) SIGRTMAX
我們可以通過選擇監聽用戶自定義信號 SIGUSR1 來實現。
PHP 官方提供了兩個函數來處理進程號,分別是:
- pcntl_signal(SIGINT, 'signalHandler'); 用於註冊收到信號後的處理函數。
- pcntl_signal_dispatch() 用於調用每個等待信號通過 pcntl_signal() 註冊的處理器。
那麼,註冊信號處理器的示例代碼可以類似如下:
pcntl_signal(SIGHUP, function () {
printf("收到重載配置信號\n");
$this->loadWorkers();
printf("重載配置完成\n");
});
而調度信號處理器可以在每次檢查進程回收的時候執行:
while (1) {
pcntl_signal_dispatch();
if ($ret = Process::wait(false)) {
// todo something
}
}
於是,Daemon 類可以擴展如下:
namespace App;
use Swoole\Process;
class Daemon
{
/**
* @var string
*/
private $configPath;
/**
* @var Command[]
*/
private $commands;
/**
* @var Worker[]
*/
private $workers = [];
public function __construct(string $configPath)
{
$this->configPath = $configPath;
}
public function run()
{
$this->loadWorkers();
pcntl_signal(SIGHUP, function () {
printf("收到重載配置信號\n");
$this->loadWorkers();
printf("重載配置完成\n");
});
$this->waitAndRestart();
}
/**
* 收回進程並重啓
*/
private function waitAndRestart()
{
while (1) {
pcntl_signal_dispatch();
if ($ret = Process::wait(false)) {
$retPid = intval($ret["pid"] ?? 0);
$index = $this->getIndexOfWorkerByPid($retPid);
if (false !== $index) {
if ($this->workers[$index]->isStopping()) {
printf("[%s] 移除守護 %s\n", date("Y-m-d H:i:s"), $this->workers[$index]->getCommand()->getId());
unset($this->workers[$index]);
} else {
$command = $this->workers[$index]->getCommand()->getCommand();
$newPid = $this->createWorker($command);
$this->workers[$index]->setPid($newPid);
printf("[%s] 重新拉起 %s\n", date("Y-m-d H:i:s"), $this->workers[$index]->getCommand()->getId());
}
}
}
}
}
/**
* 加載 workers
*/
private function loadWorkers()
{
$this->parseConfig();
foreach ($this->commands as $command) {
if ($command->isEnabled()) {
printf("[%s] 啓用 %s\n", date("Y-m-d H:i:s"), $command->getId());
$this->startWorker($command);
} else {
printf("[%s] 停用 %s\n", date("Y-m-d H:i:s"), $command->getId());
$this->stopWorker($command);
}
}
}
/**
* 啓動 worker
* @param Command $command
*/
private function startWorker(Command $command)
{
$index = $this->getIndexOfWorker($command->getId());
if (false === $index) {
$pid = $this->createWorker($command->getCommand());
$worker = new Worker();
$worker->setPid($pid);
$worker->setCommand($command);
$this->workers[] = $worker;
}
}
/**
* 停止 worker
* @param Command $command
*/
private function stopWorker(Command $command)
{
$index = $this->getIndexOfWorker($command->getId());
if (false !== $index) {
$this->workers[$index]->setStopping(true);
}
}
/**
*
* @param $commandId
* @return bool|int|string
*/
private function getIndexOfWorker(string $commandId)
{
foreach ($this->workers as $index => $worker) {
if ($commandId == $worker->getCommand()->getId()) {
return $index;
}
}
return false;
}
/**
* @param $pid
* @return bool|int|string
*/
private function getIndexOfWorkerByPid($pid)
{
foreach ($this->workers as $index => $worker) {
if ($pid == $worker->getPid()) {
return $index;
}
}
return false;
}
/**
* 解析配置文件
*/
private function parseConfig()
{
if (is_readable($this->configPath)) {
$iniConfig = parse_ini_file($this->configPath, true);
$this->commands = [];
foreach ($iniConfig as $id => $item) {
$commandLine = strval($item["command"] ?? "");
$enabled = boolval($item["enabled"] ?? false);
$command = new Command();
$command->setId($id);
$command->setCommand($commandLine);
$command->setEnabled($enabled);
$this->commands[] = $command;
}
}
}
/**
* 創建子進程,並返回子進程 id
* @param $command
* @return int
*/
private function createWorker(string $command): int
{
$process = new Process(function (Process $worker) use ($command) {
$worker->exec('/bin/sh', ['-c', $command]);
});
return $process->start();
}
}
注意:爲了代碼簡潔,以上代碼新增了一個 Worker 類如下:
class Worker
{
/**
* @var Command
*/
private $command;
/**
* @var int
*/
private $pid;
/**
* @var bool
*/
private $stopping;
// ... 以下省略了 Get Set 方法
}
最後,這個 Daemon 類的使用方法,仍然是:
$pid = posix_getpid();
printf("主進程號: {$pid}\n");
$configPath = dirname(__DIR__) . "/config/daemon.ini";
$daemonMany = new Daemon($configPath);
$daemonMany->run();
那麼,假如我們知道 Daemon 程序正在運行的進程號爲 522,則可通過以下命令來實現配置的熱重載:
kill -USR1 522
到目前爲止,這個 Daemon 類可以說是功能完備了,但是仍有可以改進的地方,
比如,有沒有辦法不需要用戶手動去給進程發送信號來重載配置,由程序自己去自動應用最新的配置呢?
下一篇文章 使用 swoole 實現進程的守護(四)將 swoole 的協程嘗試繼續擴展這個 Daemon 類。