当前位置: 首页 > 后端技术 > PHP

使用PHP玩进程之二——多进程PHPServer

时间:2023-03-29 18:07:05 PHP

首发于范浩博科学院。在使用PHP玩了第一篇流程——基础复习之后,我们已经掌握了流程的基础知识,现在我们可以尝试使用PHP来做一些简单的流程控制和管理,加深对流程的理解。接下来,我将实现一个简单的多进程模型的PHPServer,基于它你可以做任何事情。完整的PHPServer源代码可以在fan-haobai/php-server获取。总体流程PHPServer的Master进程和Worker进程的主要控制流程如下图所示:其中,主要涉及三个对象,分别是入口脚本、Master进程和Worker进程。它们所扮演的角色如下:入口脚本:主要实现PHPServer的启动、停止、重载功能,即触发Master进程的启动、停止、重载过程;Master进程:负责创建和监控Worker进程。在启动阶段,会注册signalhandler,然后创建Worker;在运行阶段,会持续监控Worker进程的健康状态,接收并响应入口脚本的控制信号;在停止阶段,停止所有Worker进程;进程:负责执行业务逻辑。被Master进程创建后,处于持续运行阶段,会监听Master进程的信号,实现自停;整个过程包括4个进程:进程①:以daemon状态启动PHPServer时的主进程。入口脚本会daemonize,即实现进程的daemon状态。这时候会fork出一个Master进程;Master进程会先保存PID,注册信号处理器,然后创建一个Workerfork多个Worker进程;进程②:是对Master进程的持续监控进程,进程中会捕获入口脚本发送的信号。主要监控Worker进程的健康状态。当Worker进程异常退出时,会尝试创建一个新的Worker进程来维持Worker进程的数量;进程③:对于Worker进程来说是一个持续运行的进程,进程中会捕获Master进程发送的信号。进程①中的Worker进程创建后,会继续执行业务逻辑,阻塞在这里;进程④:停止PHPServer主进程。入口脚本会先向Master进程发送一个SIGINT信号。Master进程捕获到信号后,将SIGINT信号转发给所有Worker进程(通知所有Worker进程终止),等待所有Worker进程终止退出;在流程②中,Worker进程被Master进程fork出来后,会继续运行并阻塞在这里,只有Master进程才会继续后续流程。代码实现启动流程见流程①,主要包括四个部分:守护进程、保存PID、注册信号处理器、创建多进程Worker。守护进程首先在入口脚本中fork出一个子进程,然后该进程退出,并将新的子进程设置为sessionleader。这时候子进程就会脱离当前终端的控制。如下图所示:这里使用了2个fork,所以最后一个fork的子进程就是Master进程。其实一个fork也是可以的。代码如下:protectedstaticfunctiondaemonize(){umask(0);$pid=pcntl_fork();if(-1===$pid){exit("processforkfail\n");}elseif($pid>0){退出(0);}//将当前进程提升为会话领导者if(-1===posix_setsid()){exit("processsetsidfail\n");}//再次fork以避免SVR4系统终端再次获得进程控制$pid=pcntl_fork();if(-1===$pid){exit("processforkfail\n");}elseif(0!==$pid){退出(0);}}通常在启动时加上-d参数,表示进程将以daemon模式运行。成功成为daemon进程后,Master进程已经脱离了终端控制,所以需要关闭标准输出和标准错误输出。如下:protectedstaticfunctionresetStdFd(){global$STDERR,$STDOUT;//重定向标准输出和错误输出@fclose(STDOUT);fclose(STDERR);$STDOUT=fopen(static::$stdoutFile,'a');$STDERR=fopen(static::$stdoutFile,'a');}保存PID为了实现PHPServer的过载或者停止,我们需要在PID文件中保存Master进程的PID,比如php-server.pid文件。代码如下:protectedstaticfunctionsaveMasterPid(){//保存pid用于重新加载和停止static::$_masterPid=posix_getpid();if(false===file_put_contents(static::$pidFile,static::$_masterPid)){exit("无法将pid保存到".static::$pidFile."\n");}echo"PHPServerstart\t\033[32m[OK]\033[0m\n";}注册信号处理因为守护进程一旦脱离了终端控制,就如脱缰的野马,有可能任由它狂奔,它就为所欲为,所以我们要驯服它。这里使用信号来实现进程间通信,控制进程的行为。注册信号处理程序如下:protectedstaticfunctioninstallSignal(){pcntl_signal(SIGINT,array('\PHPServer\Worker','signalHandler'),false);pcntl_signal(SIGTERM,array('\PHPServer\Worker','signalHandler'),false);pcntl_signal(SIGUSR1,array('\PHPServer\Worker','signalHandler'),false);pcntl_signal(SIGQUIT,array('\PHPServer\Worker','signalHandler'),false);//忽略信号pcntl_signal(SIGUSR2,SIG_IGN,false);pcntl_signal(SIGHUP,SIG_IGN,false);}受保护的静态函数signalHandler($signal){switch($signal){caseSIGINT:caseSIGTERM:static::stop();休息;案例SIGQUIT:案例SIGUSR1:static::reload();休息;默认值:中断;其中,SIGINT和SIGTERM信号会触发stop操作,即终止所有进程;SIGQUIT和SIGUSR1信号会触发reload操作,即重新加载所有Worker进程;这里忽略了SIGUSR2和SIGHUP信号,但是没有忽略SIGKILL信号,即可以强行杀死所有进程。创建多进程的WorkerMaster进程通过fork系统调用,可以创建多个Worker进程。实现代码如下:protectedstaticfunctionforkOneWorker(){$pid=pcntl_fork();//父进程if($pid>0){static::$_workers[]=$pid;}elseif($pid===0){//子进程static::setProcessTitle('PHPServer:worker');//子进程会阻塞在这里static::run();//子进程退出exit(0);}else{thrownew\Exception("forkoneworkerfail");}}受保护的静态函数forkWorkers(){while(count(static::$_workers)=0){//worker健康检查static::checkWorkerAlive();}//othersyouwanttomonitor}}第二次pcntl_signal_dispatch()捕获信号,因为waithang时间可能会很长,这段时间可能有信号,所以需要重新捕获。其中,PHPServer的停止和重新加载操作由信号触发,具体操作在信号处理器中完成;每次调度过程都会触发Worker进程的健康检查。Worker进程的健康检查Worker进程由于执行繁重的业务逻辑,可能会异常崩溃。因此,Master进程需要监控Worker进程的健康状态,并尽量维护一定数量的Worker进程。健康检查流程,如下图:代码实现,如下:protectedstaticfunctioncheckWorkerAlive(){$allWorkerPid=static::getAllWorkerPid();foreach($allWorkerPidas$index=>$pid){if(!static::isAlive($pid)){unset(static::$_workers[$index]);}}static::forkWorkers();}停止对Master进程的持续监控,见流程④。详细过程如下图所示:入口脚本向Master进程发送SIGINT信号,Master进程捕获该信号并执行信号处理程序,调用stop()方法。如下:protectedstaticfunctionstop(){//主进程向所有子进程发送退出信号if(static::$_masterPid===posix_getpid()){static::stopAllWorkers();如果(is_file(static::$pidFile)){@unlink(static::$pidFile);}退出(0);}else{//子进程退出//可以在退出之前做一些事情exit(0);}}如果Master进程执行该方法,会先调用stopAllWorkers()方法,向所有Worker进程发送SIGINT信号等待所有Worker进程终止退出,然后清除PID文件退出。特殊情况下,当Worker进程超时退出时,Master进程会再次发送SIGKILL信号强行杀死所有Worker进程;由于Master进程会向Worker进程发送SIGINT信号,Worker进程也会执行该方法,直接退出。受保护的静态函数stopAllWorkers(){$allWorkerPid=static::getAllWorkerPid();foreach($allWorkerPidas$workerPid){posix_kill($workerPid,SIGINT);}//子进程异常退出,强制killusleep(1000);if(static::isAlive($allWorkerPid)){foreach($allWorkerPidas$workerPid){static::forceKill($workerPid);}}//清除worker实例static::$_workers=array();}重载代码发布后,经常需要重新加载。其实reload进程只需要重启所有的Worker进程即可。流程如下图所示:整个流程有两个进程,流程①终止所有Worker进程,流程②是Worker进程的健康检查。在流程①中,入口脚本向Master进程发送SIGUSR1信号,Master进程捕获该信号,执行信号处理程序并调用reload()方法,reload()方法调用stopAllWorkers()方法。如下:protectedstaticfunctionreload(){//停止所有worker,master会自动fork出新的workerstatic::stopAllWorkers();}reload()方法只会在Master进程中执行,因为SIGQUIT和SIGUSR1signals不会发送给Worker进程。你可能会疑惑,为什么我们需要重启所有的Worker进程,而这里我们只是停止所有的Worker进程呢?这是因为在worker进程终止退出后,由于master进程对worker进程的健康检查功能,所有的worker进程都会自动重新创建。运行效果至此,我们就完成了一个多进程的PHPServer。让我们体验一下:$phpserver.phpUsage:Commands[mode]Commands:startStartworker.stopStopworker.reloadReloadcodes.Options:-d以DAEMON模式启动。使用“--help”获取更多信息命令。首先,我们启动它:$phpserver.phpstart-dPHPServerstart[OK]其次,查看进程树,如下:$pstree-pinit(1)-+-init(3)---bash(4)|-php(1286)-+-php(1287)`-php(1288)最后,我们停止它:$phpserver.phpstopPHPServerstopping...PHPServerstopsuccess现在是不是觉得流程控制其实很简单,并没有我们想象的那么复杂。( ̄┰ ̄*)总结我们实现了一个简单的多进程PHPServer,模拟进程管控。需要注意的是Master进程偶尔会异常崩溃。为了避免这种情况:一是不要把繁重的任务分配给Master进程,Master进程更适合调度和管理任务;其次,我们可以使用Supervisor等工具来管理我们的程序。当Master进程异常崩溃时,我们可以尝试再次拉起,避免Master进程异常退出的情况发生。相关文章?PHP的趣味流程之一——基础(2018-08-28)