上一篇文章讲解了 pcntl_fork 和 pcntl_wait 两个函数的使用, 本篇继续讲解 PHP 多进程相关新知识.
僵尸 (zombie) 进程
这里说下僵尸进程:
僵尸进程是指的父进程已经退出, 而该进程 dead 之后没有进程接受, 就成为僵尸进程 (zombie) 进程. 任何进程在退出前(使用 exit 退出) 都会变成僵尸进程(用于保存进程的状态等信息), 然后由 init 进程接管. 如果不及时回收僵尸进程, 那么它在系统中就会占用一个进程表项, 如果这种僵尸进程过多, 最后系统就没有可以用的进程表项, 于是也无法再运行其它的程序.
通过如下命令查看是否有僵尸进程, 如果有, 类似下面这样:
- $ ps -A -o stat,ppid,pid,cmd | grep -e '^[Zz]'
- Z+ 282 283 [php] <defunct>
如果子进程还没有结束时, 父进程就结束了, 那么 init 进程会自动接手这个子进程, 进行回收.
如果父进程是循环, 又没有安装 SIGCHLD 信号处理函数调用 wait 或 waitpid()等待子进程结束. 那么子进程结束后, 没有回收, 就产生僵尸进程了.
示例:
- fork_zombie.php
- <?php
- $pid = pcntl_fork();
- if($pid == -1){
- exit("fork fail");
- }elseif($pid){
- $id = getmypid();
- echo "Parent process,pid {$id}, child pid {$pid}\n";
- while(1){sleep(3);} //#1
- }else{
- $id = getmypid();
- echo "Child process,pid {$id}\n";
- sleep(2);
- exit();
- }
命令行里运行程序, 然后新终端查看:
- $ ps -A -o stat,ppid,pid,cmd | grep -e '^[Zz]'
- Z+ 7252 7253 [php] <defunct>
出现了一个僵尸进程. 这时候就算手动结束脚本程序也无法关闭这个僵尸子进程了. 需要使用 kill -9 关闭.
- pcntl_signal
- bool pcntl_signal ( int $signo , callback $handler [, bool $restart_syscalls = true ] )
该函数为 signo 指定的信号安装一个新的信号处理器.
安装 SIGCHLD 信号
上一节里, 我们讲到僵尸进程产生的原因:
如果父进程是循环, 又没有安装 SIGCHLD 信号处理函数调用 wait 或 waitpid()等待子进程结束. 那么子进程结束后, 没有回收, 就产生僵尸进程了.
本小节我们通过安装 SIGCHLD 信号处理函数来解决僵尸进程问题. 示例:
- <?php
- // 表示每执行一条低级指令, 就检查一次信号, 如果检测到注册的信号, 就调用其信号处理器
- declare(ticks = 1);
- // 安装 SIGCHLD 信号
- pcntl_signal(SIGCHLD, function(){
- echo "SIGCHLD \r\n";
- pcntl_wait($status);
- }); //#2
- $pid = pcntl_fork();
- if($pid == -1){
- exit("fork fail");
- }elseif($pid){
- $id = getmypid();
- echo "Parent process,pid {$id}, child pid {$pid}\n";
- // 先 sleep 一下, 否则代码一直循环, 无法处理信号接收
- while(1){sleep(3);} //#1
- }else{
- $id = getmypid();
- echo "Child process,pid {$id}\n";
- sleep(2);
- exit();
- }
第一次注释掉 #1 和 #2 处的代码, 父进程提前结束, 子进程被 init 进程接手, 所以没有产生僵尸进程.
第二次我们注释掉 #2 处的代码, 开启 #1 处的代码, 即父进程是个死循环, 又没有回收子进程, 就产生僵尸进程了.
第三次我们开启 #1 处和 #2 处的代码, 父进程由于安装了信号处理, 并调用 wait 函数等待子进程结束, 所以也没有产生僵尸进程.
对子进程的结束不感兴趣
如果父进程不关心子进程什么时候结束, 那么可以用
pcntl_signal(SIGCHLD, SIG_IGN)
通知内核, 自己对子进程的结束不感兴趣, 那么子进程结束后, 内核会回收, 并不再给父进程发送信号. 这样我们就不写子进程退出的处理函数了.
说明:
如果去掉
declare( ticks = 1 );
无法响应信号. 因 php 的信号处理函数是基于 ticks 来实现的, 而不是注册到真正系统底层的信号处理函数中.
安装其他信号
我们可以在主进程安装更多信号, 例如:
- <?php
- declare( ticks = 1 );
- // 信号处理函数
- function sig_handler ( $signo )
- {
- switch ( $signo ) {
- case SIGTERM :
- // 处理 SIGTERM 信号
- exit;
- break;
- case SIGHUP :
- // 处理 SIGHUP 信号
- break;
- case SIGUSR1 :
- echo "Caught SIGUSR1...\n" ;
- break;
- default:
- // 处理所有其他信号
- }
- }
- echo "Installing signal handler...\n" ;
- // 安装信号处理器
- pcntl_signal ( SIGTERM , "sig_handler" );
- pcntl_signal ( SIGHUP , "sig_handler" );
- pcntl_signal ( SIGUSR1 , "sig_handler" );
- echo "Generating signal SIGTERM to self...\n" ;
- // 向当前进程发送 SIGUSR1 信号
- posix_kill ( posix_getpid (), SIGUSR1 );
- echo "Done\n"
注: 通过 kill -l 可以看到 Linux 下所有的信号常量.
$ 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
防盗版声明: 本文系原创文章, 发布于公众号
飞鸿影的博客(fhyblog)
及博客园, 转载需作者同意.
ticks 相关
PHP 的 ticks=1 表示每执行 1 行 PHP 代码就回调此函数(指的
pcntl_signal_dispatch
). 实际上大部分时间都没有信号产生, 但 ticks 的函数一直会执行. 如果一个服务器程序 1 秒中接收 1000 次请求, 平均每个请求要执行 1000 行 PHP 代码. 那么 PHP 的 pcntl_signal, 就带来了额外的 1000 * 1000, 也就是 100 万次空的函数调用. 这样会浪费大量的 CPU 资源.
(摘自: 韩天峰 (Rango) 的博客 » PHP 官方的 pcntl_signal 性能极差
http://rango.swoole.com/archives/364 )
pcntl_signal_dispatch 的作用就是查看是否收到了信号需要处理, 如果有信号的话, 就调用相应的信号处理函数.
所以上述问题比较好的做法是去掉 ticks, 转而手动调用
pcntl_signal_dispatch
, 在代码循环中自行处理信号.
我们把上一小节的例子改改, 不使用 ticks:
- <?php
- //declare( ticks = 1 );
- // 信号处理函数
- function sig_handler ( $signo )
- {
- switch ( $signo ) {
- case SIGUSR1 :
- echo "Caught SIGUSR1...\n" ;
- break;
- default:
- // 处理所有其他信号
- }
- }
- echo "Installing signal handler...\n" ;
- // 安装信号处理器
- pcntl_signal ( SIGUSR1 , "sig_handler" );
- echo "Generating signal SIGTERM to self...\n" ;
- // 向当前进程发送 SIGUSR1 信号
- posix_kill ( posix_getpid (), SIGUSR1 );
- pcntl_signal_dispatch();
- echo "Done\n";
运行结果:
Installing signal handler...
Generating signal SIGTERM to self...
Caught SIGUSR1...
Done
相比每执行一条 php 语句都会调用
pcntl_signal_dispatch
一次, 效率好多了.
- pcntl_alarm
- int pcntl_alarm ( int $seconds )
该函数创建一个计时器, 在指定的秒数后向进程发送一个 SIGALRM 信号. 每次对 pcntl_alarm() 的调用都会取消之前设置的 alarm 信号. 注意不是定时器, 只会运行一次.
下面是一个隔 5 秒发送一个 SIGALRM 信号, 并由 signal_handler 函数获取, 然后打印一个 SIGALRM 的例子:
- <?php
- declare(ticks = 1);
- // 安装 SIGALRM 信号
- pcntl_signal(SIGALRM, function(){
- echo "SIGALRM\n";
- pcntl_alarm(5); // 再次调用, 会重新发送一个 SIGALRM 信号
- });
- pcntl_alarm(5);// 发送一个 SIGALRM 信号
- echo "run...\n";
- // 死循环, 否则进程会退出
- while(1){sleep(1);}
注: 如果不想使用 ticks, 那么需要在主循环里主动增加
pcntl_signal_dispatch()
调用.
来源: https://www.cnblogs.com/52fhy/p/9196593.html