Old drivers who have written CLI resident processes must have encountered such a problem: how can I safely close the old process when the program needs to be updated? You might expect a graceful restart like NGINX
, php-fpm
to send a USR2
signal to the process, which will then process the current request and exit.
However, it is estimated that not many people can explain how a process receives and processes signals.
principle
It is not difficult to achieve smooth shutdown/restart, here are two knowledge points:
blocking signal
When our program is processing a task, you definitely don't want it to be terminated in the middle. For example, if you are executing a database transaction, you definitely don't want the transaction to be terminated before the process is committed.
<?php
echo "开始执行事务" . PHP_EOL;
// 模拟一些耗时的操作
$finish_time = time() + 5;
while (time() < $finish_time) {
}
echo "事务执行完毕" . PHP_EOL;
In the above code, if you use the kill
command to kill the process before the second echo
7edb496d6df27cfffba4cae7a40c6dca---, then the second echo
will not be executed . Is it possible to temporarily ignore the kill
signal during the transaction?
can. We can use pcntl_sigprocmask()
to block the signal and let the transaction complete and then respond to the kill
signal.
<?php
// 阻塞信号
$sig_set = array(SIGINT, SIGTERM); // 要阻塞的信号集合
pcntl_sigprocmask(SIG_BLOCK, $sig_set); // SIG_BLOCK: 把信号加入到当前阻塞信号中
echo date("[Y-m-d H:i:s]") . " 开始执行事务" . PHP_EOL;
$finish_time = time() + 5;
while (time() < $finish_time) {
}
echo date("[Y-m-d H:i:s]") . "事务执行完毕" . PHP_EOL;
pcntl_sigprocmask(SIG_UNBLOCK, $sig_set); // SIG_UNBLOCK: 从当前阻塞信号中移出信号
同样的,在第二个echo
Ctrl + C
kill
进程,你会发现第二个echo
normally, and the time interval between the two outputs is 5 seconds.
Our resident process usually executes repetitive tasks in a while(true)
loop, if written like this:
<?php
while (true) {
pcntl_sigprocmask(SIG_BLOCK, $sig_set);
// ...
pcntl_sigprocmask(SIG_UNBLOCK, $sig_set);
}
We can guarantee that a transaction will not be interrupted, but our program does not know whether the signal has been received, and the process exits immediately after the blocking signal is removed, and there is no way to do some finishing work (such as closing document).
process signal
In order to solve the problem mentioned above, we need to do the finishing work when the signal occurs, and then exit the process.
pcntl
extension provides some signal related functions, we can use pcntl_signal()
and pcntl_signal_dispatch()
to register signal handlers and distribute signals.
<?php
$sig_handler = function ($signo) {
echo "收到信号 {$signo}" . PHP_EOL;
};
pcntl_signal(SIGINT, $sig_handler); // 给 SIGINT 信号注册一个处理器
// 模拟耗时操作
echo "开始执行事务" . PHP_EOL;
$finish_time = time() + 5;
while(true) {
if (time() > $finish_time) {
echo "事务执行完毕" . PHP_EOL;
break;
}
}
pcntl_signal_dispatch(); // 分发信号
Execute the above code and press Ctrl + C
within 5 seconds, you will see that sig_handler
is executed; and if you do not press Ctrl + C
sig_handler
then- sig_handler
will not be executed.
You should have understood the usage of pcntl_signal()
and pcntl_signal_dispatch()
by now, put it into the code just now and try
<?php
$sig_handler = function ($signo) {
echo "收到信号 {$signo}" . PHP_EOL;
};
$sig_set = array(SIGINT, SIGTERM);
foreach ($sig_set as $sig) {
pcntl_signal($sig, $sig_handler); // 注册多个信号
}
// [1]
while (true) {
// [2-1]
pcntl_sigprocmask(SIG_BLOCK, $sig_set);
// [2-2]
// ...
// [2-3]
pcntl_sigprocmask(SIG_UNBLOCK, $sig_set);
// [2-4]
}
// [3]
pcntl_signal_dispatch()
Where should I put it? Is it [1]
[2]
or [3]
? Try it out first
Then you will find that only putting it in [2]
can make the signal handler execute. At the same time, this experiment also tells us pcntl_signal_dispatch()
to make the processor execute after the signal occurs: when you put it on [1]
, unless your hand speed is fast enough, or when you press the Ctrl + C
or kill
has been executed before; and in [3]
it will never have a chance to execute.
As for where to put it in [2]
, I suggest putting it in [2-4]
, because the task has been processed by this time.
set up
Now that you have understood the principle of smooth shutdown/restart, let's organize the above semi-finished code (because it may enter the next layer of loop after receiving the signal):
<?php
$running = true;
$sig_handler = function ($signo) use (&$running) {
echo "收到信号 {$signo}" . PHP_EOL;
// 做收尾工作
$running = false;
};
$sig_set = array(SIGINT, SIGTERM, SIGUSR2 /* 熟悉的 USR2 信号不能漏 */);
foreach ($sig_set as $sig) {
pcntl_signal($sig, $sig_handler); // 注册多个信号
}
while ($running) {
pcntl_sigprocmask(SIG_BLOCK, $sig_set);
// ... 业务逻辑
pcntl_sigprocmask(SIG_UNBLOCK, $sig_set);
pcntl_signal_dispatch();
}
We get a resident process framework that can smooth the program, you can also encapsulate it into a class.
think
If you are careful, you may find that if there is an infinite loop in the business logic of the above code, and there is still no way to exit, can we set a timeout to force the finishing work to start and then exit the process?
Think about it first, then I'll write an article explaining it :-)
This article was first published on my blog: https://yian.me/blog/what-is/php-graceful-shutdown.html
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。