PHP多进程系列笔记(一)
-
本系列文章将向大家讲解 pcntl_*系列函数,从而更深入的理解进程相关知识。
-
PCNTL在PHP中进程控制支持默认是关闭的。您需要使用 --enable-pcntl 配置选项重新编译PHP的 CGI或CLI版本以打开进程控制支持。
-
Note: 此扩展在 Windows 平台上不可用。
pcntl_fork
-
int pcntl_fork ( void )
-
用于创建子进程。成功时,在父进程执行线程内返回产生的子进程的PID,在子进程执行线程内返回0。失败时,在父进程上下文返回-1,不会创建子进程,并且会引发一个PHP错误。
-
fork.php
-
-
$pid = pcntl_fork();
-
if($pid == -1){
-
//错误处理:创建子进程失败时返回-1.
-
die( 'could not fork' );
-
}elseif($pid){
-
//父进程会得到子进程号,所以这里是父进程执行的逻辑
-
$id = getmypid();
-
echo "Parent process,pid {$id}, child pid {$pid} ";
-
}else{
-
//子进程得到的$pid为0, 所以这里是子进程执行的逻辑
-
$id = getmypid();
-
echo "Child process,pid {$id} ";
-
sleep(10);
-
}
命令行运行:
-
$ php fork.php
-
Parent process,pid 98, child pid 99
-
Child process,pid 99
该例里父进程还没有来得及等子进程运行完毕就自动退出了,子进程由 init进程接管。通过 ps-ef|grep php 看到子进程还在运行:
-
[root@9355490fe5da /]# ps -ef | grep php
-
root 105 1 0 16:46 pts/0 00:00:00 php fork.php
-
root 107 27 0 16:46 pts/1 00:00:00 grep php
子进程成为孤立进程,ppid(父进程id)变成1了。如果在父进程里也加个 sleep(5),你会看到子进程ppid本来是大于1的,后来就变成1了。
注:如果是docker环境,孤立进程的ppid可能是0。
pcntl_wait
pcntl_wait()函数用来让父进程等待子进程退出,默认情况下会阻塞主进程。
阻塞模式
紧接着上面的例子,如果想等子进程运行结束后父进程再退出,该怎么办?那就用到 pcntl_wait了。
int pcntl_wait ( int &$status [, int $options = 0 ] )
该函数阻塞当前进程,只到当前进程的一个子进程退出或者收到一个结束当前进程的信号。 我们修改代码:
-
<?php
-
$pid = pcntl_fork();
-
if($pid == -1){
-
exit("fork fail");
-
}elseif($pid){
-
$id = getmypid();
-
echo "Parent process,pid {$id}, child pid {$pid} ";
-
pcntl_wait($status);
-
//pcntl_waitpid($pid, $status);
-
}else{
-
$id = getmypid();
-
echo "Child process,pid {$id} ";
-
sleep(10);
-
}
此时再次运行程序,父进程就会一直等待子进程运行结束然后退出。 pcntl_waitpid()和 pcntl_wait()功能相同。前者第一个参数支持指定pid参数,当指定-1作为 pid的值等同于后者。
int pcntl_waitpid ( int $pid , int &$status [, int $options = 0 ] )
当已知子进程pid的时候,可以使用 pcntl_waitpid()。 这两个函数返回退出的子进程进程号(>1),发生错误时返回-1,如果提供了 WNOHANG 作为option(wait3可用的系统)并且没有可用子进程时返回0。 返回值为退出的子进程进程号时,想了解如何退出,可以通过 $status状态码反应。
非阻塞模式
pcntl_wait()默认情况下会阻塞主进程,直到子进程执行完毕才继续往下运行。如果设置最后一个参数为常量 WNOHANG,那么就不会阻塞主进程,而是继续执行后续代码, 此时 pcntl_waitpid 就会返回0。 示例:
-
<?php
-
$pid = pcntl_fork();
-
if($pid == -1){
-
exit("fork fail");
-
}elseif($pid){
-
$id = getmypid();
-
echo "Parent process,pid {$id}, child pid {$pid} ";
-
while(1){
-
$res = pcntl_wait($status, WNOHANG);
-
//$res = pcntl_waitpid($pid, $status, WNOHANG);
-
if ($res == -1 || $res > 0){
-
sleep(10);//此处为了方便看效果,实际不需要
-
break;
-
}
-
}
-
}else{
-
$id = getmypid();
-
echo "Child process,pid {$id} ";
-
sleep(2);
-
}
该示例里只有一个子进程,看不出来非阻塞的好处,我们修改一下:
-
<?php
-
$child_pids = [];
-
for($i=0;$i<3; $i++){
-
$pid = pcntl_fork();
-
if($pid == -1){
-
exit("fork fail");
-
}elseif($pid){
-
$child_pids[] = $pid;
-
$id = getmypid();
-
echo time()." Parent process,pid {$id}, child pid {$pid} ";
-
}else{
-
$id = getmypid();
-
$rand = rand(1,3);
-
echo time()." Child process,pid {$id},sleep $rand ";
-
sleep($rand); //#1 故意设置时间不一样
-
exit();//#2 子进程需要exit,防止子进程也进入for循环
-
}
-
}
-
while(count($child_pids)){
-
foreach ($child_pids as $key => $pid) {
-
// $res = pcntl_wait($status, WNOHANG);
-
$res = pcntl_waitpid($pid, $status, WNOHANG);//#3
-
if ($res == -1 || $res > 0){
-
echo time()." Child process exit,pid {$pid} ";
-
unset($child_pids[$key]);
-
}else{
-
// echo time()." Wait End,pid {$pid} "; //#4
-
}
-
}
-
}
-
-
#3处首先先去掉 WNOHANG参数,运行:
-
$ php fork.1.php
-
1528637334 Parent process,pid 6600, child pid 6601
-
1528637334 Child process,pid 6601,sleep 2
-
1528637334 Parent process,pid 6600, child pid 6602
-
1528637334 Child process,pid 6602,sleep 2
-
1528637334 Parent process,pid 6600, child pid 6603
-
1528637334 Child process,pid 6603,sleep 1
-
1528637336 Child process exit,pid 6601
-
1528637336 Child process exit,pid 6602
-
1528637336 Child process exit,pid 6603
我们看到,6603号进程运行时间最短,但是是最后回收。我们再加上 WNOHANG参数,运行:
-
$ php fork.1.php
-
1528637511 Parent process,pid 6695, child pid 6696
-
1528637511 Child process,pid 6696,sleep 2
-
1528637511 Parent process,pid 6695, child pid 6697
-
1528637511 Child process,pid 6697,sleep 1
-
1528637511 Parent process,pid 6695, child pid 6698
-
1528637511 Child process,pid 6698,sleep 3
-
1528637512 Child process exit,pid 6697
-
1528637513 Child process exit,pid 6696
-
1528637514 Child process exit,pid 6698
6697进程最先回收!说明确实是异步非阻塞的。感兴趣的朋友还可以开启 #4处代码,未使用 WNOHANG参数的时候,里面的代码是不会运行的。
注意: #2处需要注意子进程需要exit,防止子进程也进入for循环。如果没有 exit(),最终创建的子进程不只3个。
- 检测status函数
在 pcntl_wait和 pcntl_waitpid两个函数中的 $status中存了子进程的状态信息,这个参数可以用于 pcntl_wifexited、 pcntl_wifstopped、 pcntl_wifsignaled、 pcntl_wexitstatus、 pcntl_wtermsig、 pcntl_wstopsig、 pcntl_waitpid这些函数。
代码片段:
-
while(1){
-
$res = pcntl_wait($status);
-
if ($res == -1 || $res > 0){
-
if(!pcntl_wifexited($status)){
-
//进程非正常退出
-
echo "service exit unusally; pid is $pid ";
-
}else{
-
//获取进程终端的退出状态码;
-
$code = pcntl_wexitstatus($status);
-
echo "service exit code: $code;pid is $pid ";
-
}
-
if(pcntl_wifsignaled($status)){
-
//不是通过接受信号中断
-
echo "service term not by signal;pid is $pid ";
-
}else{
-
$signal = pcntl_wtermsig($status);
-
echo "service term by signal $signal;pid is $pid ";
-
}
-
if(pcntl_wifstopped($status)){
-
echo "service stop not unusally;pid is $pid ";
-
}else{
-
$signal = pcntl_wstopsig($status);
-
echo "service stop by signal $signal;pid is $pid ";
-
}
-
break;
-
}
- 参考
1、php多进程 防止出现僵尸进程 https://www.cnblogs.com/jkko123/p/6351615.html?utmsource=itdadao&utmmedium=referral
2、PCNTL函数族--PHP多进程编程 (转) https://www.cnblogs.com/zox2011/archive/2013/02/19/2917448.html
PHP多进程系列笔记(二)
-
作者:飞鸿影~
-
-
出处:http://52fhy.cnblogs.com/
-
僵尸(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} ";
-
-
while(1){sleep(3);} //#1
-
}else{
-
$id = getmypid();
-
echo "Child process,pid {$id} ";
-
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信号处理函数来解决僵尸进程问题。示例:
-
-
-
//表示每执行一条低级指令,就检查一次信号,如果检测到注册的信号,就调用其信号处理器
-
declare(ticks = 1);
-
-
//安装SIGCHLD信号
-
pcntl_signal(SIGCHLD, function(){
-
echo "SIGCHLD ";
-
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} ";
-
-
//先sleep一下,否则代码一直循环,无法处理信号接收
-
while(1){sleep(3);} //#1
-
}else{
-
$id = getmypid();
-
echo "Child process,pid {$id} ";
-
sleep(2);
-
exit();
-
}
第一次注释掉#1和#2处的代码,父进程提前结束,子进程被init进程接手,所以没有产生僵尸进程。 第二次我们注释掉#2处的代码,开启#1处的代码,即父进程是个死循环,又没有回收子进程,就产生僵尸进程了。 第三次我们开启#1处和#2处的代码,父进程由于安装了信号处理,并调用wait函数等待子进程结束,所以也没有产生僵尸进程。
-
对子进程的结束不感兴趣
-
如果父进程不关心子进程什么时候结束,那么可以用pcntl_signal(SIGCHLD, SIG_IGN)通知内核,自己对子进程的结束不感兴趣,那么子进程结束后,内核会回收,并不再给父进程发送信号。这样我们就不写子进程退出的处理函数了。
说明:
如果去掉declare( ticks = 1 );无法响应信号。因php的信号处理函数是基于ticks来实现的,而不是注册到真正系统底层的信号处理函数中。
- 安装其他信号
我们可以在主进程安装更多信号,例如:
-
-
declare( ticks = 1 );
-
-
//信号处理函数
-
function sig_handler ( $signo )
-
{
-
-
switch ( $signo ) {
-
case SIGTERM :
-
// 处理SIGTERM信号
-
exit;
-
break;
-
case SIGHUP :
-
//处理SIGHUP信号
-
break;
-
case SIGUSR1 :
-
echo "Caught SIGUSR1... " ;
-
break;
-
default:
-
// 处理所有其他信号
-
}
-
-
}
-
-
echo "Installing signal handler... " ;
-
-
//安装信号处理器
-
pcntl_signal ( SIGTERM , "sig_handler" );
-
pcntl_signal ( SIGHUP , "sig_handler" );
-
pcntl_signal ( SIGUSR1 , "sig_handler" );
-
-
echo "Generating signal SIGTERM to self... " ;
-
-
//向当前进程发送SIGUSR1信号
-
posix_kill ( posix_getpid (), SIGUSR1 );
-
-
echo "Done "
注:通过 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
-
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:
-
-
-
//declare( ticks = 1 );
-
-
//信号处理函数
-
function sig_handler ( $signo )
-
{
-
-
switch ( $signo ) {
-
case SIGUSR1 :
-
echo "Caught SIGUSR1... " ;
-
break;
-
default:
-
// 处理所有其他信号
-
}
-
-
}
-
-
echo "Installing signal handler... " ;
-
-
//安装信号处理器
-
pcntl_signal ( SIGUSR1 , "sig_handler" );
-
-
echo "Generating signal SIGTERM to self... " ;
-
-
//向当前进程发送SIGUSR1信号
-
posix_kill ( posix_getpid (), SIGUSR1 );
-
pcntl_signal_dispatch();
-
-
echo "Done ";
运行结果:
-
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 的例子:
-
-
declare(ticks = 1);
-
-
//安装SIGALRM信号
-
pcntl_signal(SIGALRM, function(){
-
echo "SIGALRM ";
-
pcntl_alarm(5); //再次调用,会重新发送一个SIGALRM信号
-
});
-
pcntl_alarm(5);//发送一个SIGALRM信号
-
-
echo "run... ";
-
-
//死循环,否则进程会退出
-
while(1){sleep(1);}
-
注:如果不想使用ticks,那么需要在主循环里主动增加pcntl_signal_dispatch()调用。
PHP多进程系列笔记(三)
多进程实例
本节讲解几个多进程的实例。
- Master-Worker结构
下面例子实现了简单的多进程管理:
- 支持设置最大子进程数
- Master-Worker结构:Worker挂掉,Master进程会重新创建一个
-
-
-
$pids = []; //存储子进程pid
-
$MAX_PROCESS = 3;//最大进程数
-
-
$pid = pcntl_fork();
-
if($pid <0){
-
exit("fork fail ");
-
}elseif($pid > 0){
-
exit;//父进程退出
-
}else{
-
// 从当前终端分离
-
if (posix_setsid() == -1) {
-
die("could not detach from terminal");
-
}
-
-
$id = getmypid();
-
echo time()." Master process, pid {$id} ";
-
-
for($i=0; $i<$MAX_PROCESS;$i++){
-
start_worker_process();
-
}
-
-
//Master进程等待子进程退出,必须是死循环
-
while(1){
-
foreach($pids as $pid){
-
if($pid){
-
$res = pcntl_waitpid($pid, $status, WNOHANG);
-
if ( $res == -1 || $res > 0 ){
-
echo time()." Worker process $pid exit, will start new... ";
-
start_worker_process();
-
unset($pids[$pid]);
-
}
-
}
-
}
-
}
-
}
-
-
/**
-
* 创建worker进程
-
*/
-
function start_worker_process(){
-
global $pids;
-
$pid = pcntl_fork();
-
if($pid <0){
-
exit("fork fail ");
-
}elseif($pid > 0){
-
$pids[$pid] = $pid;
-
// exit; //此处不可退出,否则Master进程就退出了
-
}else{
-
//实际代码
-
$id = getmypid();
-
$rand = rand(1,3);
-
echo time()." Worker process, pid {$id}. run $rand s ";
-
while(1){
-
sleep($rand);
-
}
-
}
-
}
多进程Server
下面我们使用多进程实现一个tcp服务器,支持:
- 多进程处理客户端连接
- 子进程退出,Master进程会重新创建一个
- 支持事件回调
-
-
-
class TcpServer{
-
const MAX_PROCESS = 3;//最大进程数
-
private $pids = []; //存储子进程pid
-
private $socket;
-
-
public function __construct(){
-
$pid = pcntl_fork();
-
if($pid <0){
-
exit("fork fail ");
-
}elseif($pid > 0){
-
exit;//父进程退出
-
} else{
-
// 从当前终端分离
-
if (posix_setsid() == -1) {
-
die("could not detach from terminal");
-
}
-
-
umask(0);
-
-
$id = getmypid();
-
echo time()." Master process, pid {$id} ";
-
-
//创建tcp server
-
$this->socket = stream_socket_server("tcp://0.0.0.0:9201", $errno, $errstr);
-
if(!$this->socket) exit("start server err: $errstr --- $errno");
-
}
-
}
-
-
public function run(){
-
for($i=0; $i<self::MAX_PROCESS;$i++){
-
$this->start_worker_process();
-
}
-
-
echo "waiting client... ";
-
-
//Master进程等待子进程退出,必须是死循环
-
while(1){
-
foreach($this->pids as $k=>$pid){
-
if($pid){
-
$res = pcntl_waitpid($pid, $status, WNOHANG);
-
if ( $res == -1 || $res > 0 ){
-
echo time()." Worker process $pid exit, will start new... ";
-
$this->start_worker_process();
-
unset($this->pids[$k]);
-
}
-
}
-
}
-
sleep(1);//让出1s时间给CPU
-
}
-
}
-
-
/**
-
* 创建worker进程,接受客户端连接
-
*/
-
private function start_worker_process(){
-
$pid = pcntl_fork();
-
if($pid <0){
-
exit("fork fail ");
-
}elseif($pid > 0){
-
$this->pids[] = $pid;
-
// exit; //此处不可退出,否则Master进程就退出了
-
}else{
-
$this->acceptClient();
-
}
-
}
-
-
private function acceptClient()
-
{
-
//子进程一直等待客户端连接,不能退出
-
while(1){
-
$conn = stream_socket_accept($this->socket, -1);
-
if($this->onConnect) call_user_func($this->onConnect, $conn); //回调连接事件
-
-
//开始循环读取消息
-
$recv = ''; //实际收到消息
-
$buffer = ''; //缓冲消息
-
while(1){
-
$buffer = fread($conn, 20);
-
-
//没有收到正常消息
-
if($buffer === false || $buffer === ''){
-
if($this->onClose) call_user_func($this->onClose, $conn); //回调断开连接事件
-
break;//结束读取消息,等待下一个客户端连接
-
}
-
-
$pos = strpos($buffer, " "); //消息结束符
-
if($pos === false){
-
$recv .= $buffer;
-
}else{
-
$recv .= trim(substr($buffer, 0, $pos+1));
-
-
if($this->onMessage) call_user_func($this->onMessage, $conn, $recv); //回调收到消息事件
-
-
//客户端强制关闭连接
-
if($recv == "quit"){
-
echo "client close conn ";
-
fclose($conn);
-
break;
-
}
-
-
$recv = ''; //清空消息,准备下一次接收
-
}
-
}
-
}
-
}
-
-
function __destruct() {
-
@fclose($this->socket);
-
}
-
}
-
-
$server = new TcpServer();
-
-
$server->onConnect = function($conn){
-
echo "onConnect -- accepted " . stream_socket_get_name($conn,true) . " ";
-
fwrite($conn,"conn success ");
-
};
-
-
$server->onMessage = function($conn,$msg){
-
echo "onMessage --" . $msg . " ";
-
fwrite($conn,"received ".$msg." ");
-
};
-
-
$server->onClose = function($conn){
-
echo "onClose --" . stream_socket_get_name($conn,true) . " ";
-
fwrite($conn,"onClose "." ");
-
};
-
-
$server->run();
运行:
-
$ php process_multi.server.php
-
1528734803 Master process, pid 9110
-
waiting client...
此时服务端已经变成守护进程了。新开终端,我们使用ps命令查看进程:
-
$ ps -ef | grep php
-
yjc 9110 1 0 00:33 ? 00:00:00 php process_multi.server.php
-
yjc 9111 9110 0 00:33 ? 00:00:00 php process_multi.server.php
-
yjc 9112 9110 0 00:33 ? 00:00:00 php process_multi.server.php
-
yjc 9113 9110 0 00:33 ? 00:00:00 php process_multi.server.php
-
yjc 9134 8589 0 00:35 pts/1 00:00:00 grep php
可以看到4个进程:1个主进程,3个子进程。使用kill命令结束子进程,主进程会重新拉起一个新的子进程。
然后我们使用telnet测试连接:
-
$ telnet 127.0.0.1 9201
-
Trying 127.0.0.1...
-
Connected to 127.0.0.1.
-
Escape character is '^]'.
-
conn success
-
hello server!
-
received hello server!
-
quit
-
received quit
-
Connection closed by foreign host.
PHP多进程系列笔记(四)
Posix常用函数
本节主要讲解Posix常用函数和进程池的概念,也会涉及到守护进程的知识。本节难度较低。
- posix_kill
向指定pid进程发送信号。成功时返回 TRUE , 或者在失败时返回 FALSE 。
bool posix_kill ( int $pid , int $sig )
$sig=0,可以检测进程是否存在,不会发送信号。
示例:
-
//向当前进程发送SIGUSR1信号
-
posix_kill ( posix_getpid (), SIGUSR1 );
注:通过 kill -l 可以看到Linux下所有的信号常量。
posix_getpid 返回当前进程id。
posix_getppid 返回父进程id。
posix_setsid 设置新会话组长,脱离终端。成功时返回session id,失败返回 -1。写守护进程(Daemon) 用到该函数。下面引用Workerman源代码里的一段示例:
-
function daemonize(){
-
umask(0);
-
$pid = pcntl_fork();
-
if (-1 === $pid) {
-
die('fork fail');
-
} elseif ($pid > 0) {
-
exit(0);
-
}
-
-
if (-1 === posix_setsid()) {
-
die("setsid fail");
-
}
-
-
// Fork again avoid SVR4 system regain the control of terminal.
-
$pid = pcntl_fork();
-
if (-1 === $pid) {
-
die("fork fail");
-
} elseif (0 !== $pid) {
-
exit(0);
-
}
-
}
如果程序需要以守护进程的方式执行,在业务代码之前调用该函数即可。
进程池
什么是进程池? 其实是很简单的概念,就是预先创建一组子进程,当有新任务来时,系统通过调配该组进程中的某个子进程完成此任务。
前面几节的示例里我们都是使用这种方式,预先创建好进程,而不是动态创建。
引入《Linux高性能服务器编程》的一段话,描述动态创建进程的缺点:
动态创建进程(或线程)比较耗费时间,这将导致较慢的客户响应。 动态创建的子进程通常只用来为一个客户服务,这样导致了系统上产生大量的细微进程(或线程)。进程和线程间的切换将消耗大量CPU时间。 动态创建的子进程是当前进程的完整映像,当前进程必须谨慎的管理其分配的文件描述符和堆内存等系统资源,否则子进程可能复制这些资源,从而使系统的可用资源急剧下降,进而影响服务器的性能。 所以任何时候,建议预先创建好进程,也就是使用进程池的方式实现。
像我们熟知的php-fpm还支持最大创建多少个进程、初始创建多少个进程这种方式,大家感兴趣可以研究研究。
PHP多进程系列笔记(五)
swoole多进程
前面几节都是讲解pcntl扩展实现的多进程程序。本节给大家介绍swoole扩展的swoole_process模块。
swoole_process 是swoole提供的进程管理模块,用来替代PHP的pcntl扩展。
首先,确保安装的swoole版本大于1.7.2:
-
$ php --ri swoole
-
-
swoole
-
-
swoole support => enabled
-
Version => 1.10.1
注意:swoole_process在最新的1.8.0版本已经禁止在Web环境中使用了,所以也只能支持命令行。
swoole提供的多进程扩展基本功能和pcntl提供的一样,但swoole更易简单上手,并且提供了:
- 默认基于unixsock的进程间通信;
- 支持消息队列作为进程间通信;
- 基于signalfd和eventloop处理信号,几乎没有任何额外消耗;
- 高精度微秒定时器;
- 配合swoole_event模块,创建的PHP子进程可以异步的事件驱动模式
- 基础方法
-
swoole_process::__construct
-
swoole_process->start
-
swoole_process->name
-
swoole_process->exec
-
swoole_process->close
-
swoole_process->exit
-
swoole_process::kill
-
swoole_process::wait
-
swoole_process::daemon
-
swoole_process::setAffinity
- 管道通信
-
swoole_process->write
-
swoole_process->read
-
swoole_process->setTimeout
-
swoole_process->setBlocking
- 消息队列通信
-
swoole_process->useQueue
-
swoole_process->statQueue
-
swoole_process->freeQueue
-
swoole_process->push
-
swoole_process->pop
- 信号与定时器
-
swoole_process::signal
-
swoole_process::alarm
基础应用
- 本例实现的是tcp server,特性:
- 多进程处理客户端连接
- 子进程退出,Master进程会重新创建一个
- 支持事件回调
- 主进程退出,子进程在干完手头活后退出
-
-
-
class TcpServer{
-
const MAX_PROCESS = 3;//最大进程数
-
private $pids = []; //存储子进程pid
-
private $socket;
-
private $mpid;
-
-
public function run(){
-
$process = new swoole_process(function(){
-
$this->mpid = $id = getmypid();
-
echo time()." Master process, pid {$id} ";
-
-
//创建tcp server
-
$this->socket = stream_socket_server("tcp://0.0.0.0:9201", $errno, $errstr);
-
if(!$this->socket) exit("start server err: $errstr --- $errno");
-
-
for($i=0; $i<self::MAX_PROCESS;$i++){
-
$this->start_worker_process();
-
}
-
-
echo "waiting client... ";
-
-
//Master进程等待子进程退出,必须是死循环
-
while(1){
-
foreach($this->pids as $k=>$pid){
-
if($pid){
-
$res = swoole_process::wait(false);
-
if ( $res ){
-
echo time()." Worker process $pid exit, will start new... ";
-
$this->start_worker_process();
-
unset($this->pids[$k]);
-
}
-
}
-
}
-
sleep(1);//让出1s时间给CPU
-
}
-
}, false, false); //不启用管道通信
-
swoole_process::daemon(); //守护进程
-
$process->start();//注意:start之后的变量子进程里面是获取不到的
-
}
-
-
/**
-
* 创建worker进程,接受客户端连接
-
*/
-
private function start_worker_process(){
-
$process = new swoole_process(function(swoole_process $worker){
-
$this->acceptClient($worker);
-
}, false, false);
-
$pid = $process->start();
-
$this->pids[] = $pid;
-
}
-
-
private function acceptClient(&$worker)
-
{
-
//子进程一直等待客户端连接,不能退出
-
while(1){
-
-
$conn = stream_socket_accept($this->socket, -1);
-
if($this->onConnect) call_user_func($this->onConnect, $conn); //回调连接事件
-
-
//开始循环读取消息
-
$recv = ''; //实际收到消息
-
$buffer = ''; //缓冲消息
-
while(1){
-
$this->checkMpid($worker);
-
-
$buffer = fread($conn, 20);
-
-
//没有收到正常消息
-
if($buffer === false || $buffer === ''){
-
if($this->onClose) call_user_func($this->onClose, $conn); //回调断开连接事件
-
break;//结束读取消息,等待下一个客户端连接
-
}
-
-
$pos = strpos($buffer, " "); //消息结束符
-
if($pos === false){
-
$recv .= $buffer;
-
}else{
-
$recv .= trim(substr($buffer, 0, $pos+1));
-
-
if($this->onMessage) call_user_func($this->onMessage, $conn, $recv); //回调收到消息事件
-
-
//客户端强制关闭连接
-
if($recv == "quit"){
-
echo "client close conn ";
-
fclose($conn);
-
break;
-
}
-
-
$recv = ''; //清空消息,准备下一次接收
-
}
-
}
-
}
-
}
-
-
//检查主进程是否存在,若不存在子进程在干完手头活后退出
-
public function checkMpid(&$worker){
-
if(!swoole_process::kill($this->mpid,0)){
-
$worker->exit();
-
// 这句提示,实际是看不到的.需要写到日志中
-
echo "Master process exited, I [{$worker['pid']}] also quit ";
-
}
-
}
-
-
function __destruct() {
-
@fclose($this->socket);
-
}
-
}
-
-
$server = new TcpServer();
-
-
$server->onConnect = function($conn){
-
echo "onConnect -- accepted " . stream_socket_get_name($conn,true) . " ";
-
fwrite($conn,"conn success ");
-
};
-
-
$server->onMessage = function($conn,$msg){
-
echo "onMessage --" . $msg . " ";
-
fwrite($conn,"received ".$msg." ");
-
};
-
-
$server->onClose = function($conn){
-
echo "onClose --" . stream_socket_get_name($conn,true) . " ";
-
fwrite($conn,"onClose "." ");
-
};
-
-
$server->run();
-
运行后可以使用telnet连接:
telnet 127.0.0.1 9201
由于设置了最大三个子进程,最多只能接受3个客户端连接。
进程间通信
前面讲解的例子里,主进程和子进程直接是没有直接的数据交互的。如果主进程需要得到的来自子进程的反馈,或者子进程接受来自主进程的数据,那么就需要进程间通信了。
swoole内置了管道通信和消息队列通信。
- 管道通信
管道通信主要是数据传输:一个进程需要将数据发送给另外一个进程。
这个swoole封装后,使用非常简单:
-
-
-
$workers = [];
-
-
for ($i=0; $i<3; $i++) {
-
$process = new swoole_process(function(swoole_process $worker){
-
//子进程逻辑
-
$cmd = $worker->read();
-
-
ob_start();
-
passthru($cmd);//执行外部程序并且显示未经处理的、原始输出,会直接打印输出。
-
$return = ob_get_clean() ? : ' ';
-
$return = trim($return).". worker pid:".$worker->pid." ";
-
-
// $worker->write($return);//写入数据到管道
-
echo $return;//写入数据到管道。注意:子进程里echo也是写入到管道
-
}, true); //第二个参数为true,启用管道通信
-
$pid = $process->start();
-
$workers[$pid] = $process;
-
}
-
-
foreach($workers as $pid=>$worker){
-
$worker->write('whoami'); //通过管道发数据到子进程。管道是单向的:发出的数据必须由另一端读取。不能读取自己发出去的
-
$recv = $worker->read();//同步阻塞读取管道数据
-
echo "recv result: $recv";
-
}
-
-
//回收子进程
-
while(count($workers)){
-
// echo time(). " ";
-
foreach($workers as $pid=>$worker){
-
$ret = swoole_process::wait(false);
-
if($ret){
-
echo "worker exit: $pid ";
-
unset($workers[$pid]);
-
}
-
}
-
}
-
-
运行:
-
$ php swoole_process_pipe.php
-
recv result: Linux
-
recv result: 2018年 06月 24日 星期日 16:18:01 CST
-
recv result: yjc
-
worker exit: 14519
-
worker exit: 14522
-
worker exit: 14525
注意点:
- 管道数据读取是同步阻塞的;上面的例子里如果子进程里再加一句$worker->read(),会一直阻塞。可以使用swoole_event_add将管道加入到事件循环中,变为异步模式。
- 子进程里的输出(例如echo)与write效果相同。
- 通过管道发数据到子进程。管道是单向的:发出的数据必须由另一端读取。不能读取自己发出去的。
这里额外讲解一下swoole_process::wait():
- swoole_process::wait()默认是阻塞的, swoole_process::wait(false)则是非阻塞的;
- swoole_process::wait()阻塞模式调用一次仅能回收一个子进程,非阻塞模式调用一次不一定能当前就能回收子进程;
- 如果不加swoole_process::wait(),主进程又是死循环,主进程退出后会变成僵尸进程。
-
ps -A -o stat,ppid,pid,cmd | grep -e '^[Zz]'可以查询僵尸进程。
-
- 消息队列通信
消息队列与管道有些不一样:消息队列是全局的,所有进程都可以发送、读取。你可以把它看做redis list结构。
消息队列更常见的用途是主进程分配任务,子进程消费执行。
-
-
-
$workers = [];
-
-
for ($i=0; $i<3; $i++) {
-
$process = new swoole_process(function(swoole_process $worker){
-
//子进程逻辑
-
sleep(1); //防止父进程还未往消息队列中加入内容直接退出
-
while($cmd = $worker->pop()){
-
// echo "recv from master: $cmd ";
-
-
ob_start();
-
passthru($cmd);//执行外部程序并且显示未经处理的、原始输出,会直接打印输出。
-
$return = ob_get_clean() ? : ' ';
-
$return = "res: ".trim($return).". worker pid: ".$worker->pid." ";
-
-
echo $return;
-
// sleep(1);
-
}
-
-
$worker->exit(0);
-
}, false, false); //不创建管道
-
-
$process->useQueue(1, 2 | swoole_process::IPC_NOWAIT); //使用消息队列
-
$pid = $process->start();
-
$workers[$pid] = $process;
-
}
-
-
//由于所有进程是共享使用一个消息队列,所以只需向一个子进程发送消息即可
-
$worker = current($workers);
-
for ($i=0; $i<3; $i++) {
-
$worker->push('whoami'); //发送消息
-
}
-
-
-
//回收子进程
-
while(count($workers)){
-
foreach($workers as $pid=>$worker){
-
$ret = swoole_process::wait();
-
if($ret){
-
echo "worker exit: $pid ";
-
unset($workers[$pid]);
-
}
-
}
-
}
-
运行结果:
-
$ php swoole_process_quene.php
-
res: yjc. worker pid: 15885
-
res: yjc. worker pid: 15886
-
res: yjc. worker pid: 15887
-
worker exit: 15885
-
worker exit: 15886
-
worker exit: 15887
-
注意点:
- 所有进程共享使用一个消息队列;
- 消息队列的读取操作是阻塞的,可以在useQueue的时候第2个参数mode改为2 | swoole_process::IPC_NOWAIT,则是异步的。mode仅仅设置为2是阻塞的,示例里去掉swoole_process::IPC_NOWAIT后读取消息的while会死循环。
- 子进程前面加了个sleep(1);,这是为了防止父进程还未往消息队列中加入内容直接退出。
- 子进程末尾也加了sleep,这是为了防止一个进程把所有消息都消费完了,实际应用需要去掉。
- 信号与定时器 swoole_process::alarm支持微秒定时器:
-
-
-
function ev_timer(){
-
static $i = 0;
-
echo "#{$i} alarm ";
-
$i++;
-
if ($i > 5) {
-
//清除定时器
-
swoole_process::alarm(-1);
-
-
//退出进程
-
swoole_process::kill(getmypid());
-
-
}
-
}
-
-
//安装信号
-
swoole_process::signal(SIGALRM, 'ev_timer');
-
-
//触发定时器信号:单位为微秒。如果为负数表示清除定时器
-
swoole_process::alarm(100 * 1000);//100ms
-
-
echo getmypid()." "; //该句会顺序执行,后续无需使用while循环防止进程直接退出
-
运行:
-
$ php swoole_process_alarm.php
-
13660
-
#0 alarm
-
#1 alarm
-
#2 alarm
-
#3 alarm
-
#4 alarm
-
#5 alarm
注:alarm不能和SwooleTimer同时使用。