模仿php-fpm用php实现一套多进程管理框架

之前我分析过fpm中的进程管理,是采用纯c语言实现的,其实php本身的pcntl拓展也提供了进程创建,信号管理的功能。于是我打算模仿fpm在php里实现一套进程管理工具。

TODO列表

  • 使用pcntl_signal 处理,SIGCHLD 和 SIGINT
  • 创建多个进程,父进程记录pids,在子进程退出的时候更新pids
  • 创建多个进程,父进程记录pids,在子进程退出的时候重启子进程,让子进程的数量固定
  • 通过向父进程发送 SIGQUIT 实现退出所有进程
  • 通过向父进程发送 SIGUSR2 实现重启所有进程(平滑)
  • 父进程退出(或异常)所有子进程也退出
  • 修改进程名 改成 master 和 worker
  • 封装,子进程自定义函数执行
  • 封装,父进程根据自定义函数调整进程数量

php提供的基本功能

php多进程编程本质上是将 c语言中 <unistd.h>,<signal.h>,<fcntl.h> <sys/socket.h> 等unix函数(应用层和内核打交道的接口)用php拓展的形式封装了一遍,并且自己用c语言封装了一些通用的功能。可以对比一下原生的c语言函数。

使用的pcntl中的函数

  • pcntl_fork
pcntl_fork ( void ) : int

核心函数。fork是创建了一个子进程,父进程和子进程 都从fork的位置开始向下继续执行,不同的是父进程执行过程中,得到的fork返回值为子进程 号,而子进程得到的是0。调用一次返回两次。对应c语言中的 pid_t fork(void);


  • pcntl_waitpid
pcntl_waitpid ( int $pid , int &$status [, int $options = 0 ] ) : int

等待或返回fork的子进程状态, 用来在SIGCHLD发生时回收子进程,正常返回子进程pid。 相当于c语言中的 pid_t waitpid(pid_t pid, int *stat_loc, int options);


  • pcntl_signal
pcntl_signal ( int $signo , callback $handler [, bool $restart_syscalls = true ] ) : bool

安装一个信号处理器。相当于c语言中的 sig_t signal(int sig, sig_t func);


  • pcntl_exec
pcntl_exec ( string $path [, array $args [, array $envs ]] ) : void

在一个进程里启动另一个进程,启动进程的所有内容(代码和数据)被清空,替换成新的进程。bash工作的时候就是fork一个新的进程然后在新的进程里exec的,进程id和父子关系都不会变。相当于c语言中的
int execve(const char *path, char *const argv[], char *const envp[]);


  • pcntl_signal_dispatch
pcntl_signal_dispatch ( void ) : bool

函数pcntl_signal_dispatch()调用每个等待信号通过pcntl_signal() 安装的处理器。非阻塞的,有就调用,没有就返回。c语言中不需要,因为php算是一个大型的C项目,里面指令的粒度不一样,只能把信号先放到一个队列里,用户主动调用pcntl_signal_dispatch时依次调用安装的信号处理器,c语言操作系统就帮忙自动调用了,直接中断当前指令就好了


  • pcntl_get_last_error
pcntl_get_last_error ( void ): int

php.net 中是Retrieve the error number set by the last pcntl function which failed。相当于c语言中的 errno,没错这只是个全局变量,需要#include <errno.h>使用


  • pcntl_strerror
pcntl_strerror ( int $errno ) : string

将posix错误码转换为可读的字符串。相当于c语言中的 char* strerror(int errnum);


使用的stream中的函数

  • stream_socket_pair
stream_socket_pair ( int $domain , int $type , int $protocol ) : array

创建unix套接字处理php中的信号,信号函数比较难控制。相当于c语言中的 int socketpair(int domain, int type, int protocol, int socket_vector[2]);


  • stream_set_blocking
stream_set_blocking ( resource $stream , int $mode ) : bool

为资源流设置阻塞或者阻塞模式,防止event loop中读取socket pair 阻塞。相当于c语言中的 int fcntl(int fildes, int cmd, …);

使用的其他php函数

  • cli_set_process_title
cli_set_process_title ( string $title ) : bool

设置进程名,这样在ps aux 里看到的就可以区分开了

实现

gitlist

<?php
class ErrorLevel {
	const WARNNING = 'warnning';
	const FATAL = 'fatal';
	const NOTICE = 'notice';
	const DEBUG = 'debug';
}

class Process {
	const STAGE_NORMAL = 'normal';
	const STAGE_QUIT = 'quit';
	const STAGE_RESTARTING = 'restart';

	/**
	 * 标识子进程是否处于退出的状态
	 *
	 * @var boolean
	 */
	private $inShutDown = false;

	/**
	 * 主进程的状态
	 *
	 * @var string
	 */
	private $masterStage = self::STAGE_NORMAL;

	/**
	 * 记录所有当前存在的进程,只在父进程中维护,子进程的没有意义
	 *
	 * @var array
	 */
	private $pids = [];
	/**
	 *  用全局变量标识父进程还是子进程
	 *
	 * @var boolean
	 */
	private $isParent = true; //
	
	/**
	 * 处理信号量的管道,$sockets[0] 用来写,$sockets[1] 用来读
	 *
	 * @var array
	 */
	private $sockets = [];

	/**
	 * 错误日志写入资源句柄
	 *
	 * @var resource
	 */
	private $logFd;

	/**
	 * 开启的进程数量
	 *
	 * @var int
	 */
	private $forkCount;

	/**
	 * event loop 每次检查信号的间隔,间隔越小,越能及时处理进程的死亡和拉起
	 *
	 * @var int
	 */
	private $eventLoopInterval = 1;
	
	/**
	 * 主进程的pid
	 *
	 * @var integer
	 */
	private $masterPid = 0;

	public function __construct($forkCount, $logFd = STDOUT) {
		$this->forkCount = $forkCount;
		$this->logFd = $logFd;
	}

	/**
	 * 运行程序入口
	 *
	 * @return void
	 */
	public function run()
	{
		$this->masterPid = posix_getpid();
		// 创建socket(相当于匿名管道)
		$this->sockets = stream_socket_pair(STREAM_PF_UNIX, STREAM_SOCK_STREAM, STREAM_IPPROTO_IP);
		// 注册信号处理函数
		pcntl_signal(SIGCHLD, array($this, "sigAction"));
		pcntl_signal(SIGQUIT, array($this, "sigAction"));
		pcntl_signal(SIGUSR2, array($this, "sigAction"));
		// 初始化子进程
		$ret = $this->makeChildren($this->forkCount);
		if($ret == 2) { // fork失败,程序退出,TODO 这里没有处理子进程变成孤儿进程的情况
			$this->log("fork failed " . pcntl_strerror(pcntl_get_last_error()), "fatal");
			exit;
		}
		
		// 父进程
		if($this->isParent) {
			// 设置具柄为非阻塞的
			stream_set_blocking( $this->sockets[1], False );
			cli_set_process_title("php master");
			// 相当于 fpm_event_loop
			while (true) {
				pcntl_signal_dispatch(); 
				$this->checkSignal(); // 检查是否有信号发生
				if(!$this->isParent) { // 可能在checkSiganl里fork了子进程,这里要退出
					break;
				}
				if(count($this->pids) == 0) { // 父进程回收完所有子进程退出
					if($this->masterStage == self::STAGE_QUIT) {
						$this->log("master quiting");
						break;
					} else if($this->masterStage == self::STAGE_RESTARTING) {
						// // 父进程回收完所有子进程重启
						global $argv;
						$this->log("master restarting");
						pcntl_exec(exec("which php"), $argv);
					}
				}
				$this->log("stage " . $this->masterStage . " child count " . count($this->pids). " fall sleep");
				sleep($this->eventLoopInterval);
			}    
			if($this->isParent) { // 关闭打开的管道
				fclose($this->sockets[0]);
				fclose($this->sockets[1]);    
			}
		}
		
		// 子进程
		if(!$this->isParent) {
			pcntl_signal(SIGCHLD, SIG_IGN); // 忽略SIGCHLD,尽管子进程应该没有child才合理
			pcntl_signal(SIGQUIT, [$this, "sigActionChild"]);
			cli_set_process_title("php worker");
			fclose($this->sockets[0]); // 子进程用不上
			fclose($this->sockets[1]); 
			// sleep(mt_rand(1, 5); 验证拉起子进程
			// exit;
			while(true) { // 验证重启和退出信号的代码
				sleep(mt_rand(1, 5));
				$this->log("I'm sleeping");
				$this->heartbeat();
			}
			exit; // 要及时退出
		}
	}

	/**
	 * 子进程中当一个单位的任务完成后调用,防止在一个任务执行中被中断
	 *
	 * @param \Callable $clearUpFunc
	 * @return void
	 */
	public function heartbeat($clearUpFunc = null){
		// 检查是否被父进程告知要死去
		pcntl_signal_dispatch();
		if($this->inShutDown) {
			$this->log("child quit for master notify");
		} else if(!file_exists("/proc/{$this->masterPid}")) {
			// 检查父进程是否已经死去
			$this->log("child quit for master die");
			$this->inShutDown = true;
		}
		if($this->inShutDown) {
			if(is_callable($clearUpFunc)) {
				call_user_func($clearUpFunc);
			}
			exit;
		}
	}

	/**
	 * 真正的,信号处理函数 对应fpm中的 fpm_got_signal,从管道中读取信号
	 *
	 * @return void
	 */
	private function checkSignal()
	{
		do {
			$c = fread($this->sockets[1], 1);
			if(empty($c)) {
				break;
			}
			switch ($c) {
				case 'C':
					// 表示有子进程挂掉了,pcntl_waitpid循环检查(因为SIGCHLD多个信号同时到来,没有及时处理掉,只会触发一次,为了防止僵尸进程的出现要循环检查,WNOHANG是不阻塞)
					while(($pid = pcntl_waitpid(-1,$status,WNOHANG)) > 0) {
						unset($this->pids[$pid]);  // 从进程列表中移除
						$this->log("child reaped ". $pid);
						if($this->masterStage != self::STAGE_NORMAL) {
							continue;
						}
						$ret = $this->makeChildren(1); // 拉起一个新的进程
						if($ret == 2) {
							$this->log("fork failed " . pcntl_strerror(pcntl_get_last_error()), 'fatal');
							exit;
						}
						if(!$this->isParent) { // 子进程不处理信号,只有父进程处理
							return;
						}
					}
				break;
				case "Q": // 退出
					if($this->masterStage != self::STAGE_NORMAL) {
						break;
					}
					$this->masterStage = self::STAGE_QUIT;
					$this->killAll(SIGQUIT);
				break;
				case '2': // 重启
					if($this->masterStage != self::STAGE_NORMAL) {
						break;
					}
					$this->masterStage = self::STAGE_RESTARTING;
					$this->killAll(SIGQUIT);
				break;
			}
		} while(1);
	}

	/**
	* 信号处理函数,只负责从管道写入信号
	*
	* @param int $signo
	* @return void
	*/
   private function sigAction($signo) {
	   switch ($signo) {
		   case SIGCHLD:
			   fwrite($this->sockets[0], "C");
			   break;
		   case SIGQUIT:
			   fwrite($this->sockets[0], "Q");
			   break;
		   case SIGUSR2:
			   fwrite($this->sockets[0], "2");
			   break;
	   }
   }

	/**
	* 子进程的信号函数
	*
	* @param int $signo
	* @return void
	*/
   private function sigActionChild($signo) {
	   switch ($signo) {
		   case SIGQUIT:
			   // 标记退出状态
			   $this->inShutDown = true;
			   break;
	   }
   }

	/**
	 * 
	 *
	 * @param int $n fork的进程数量
	 * @return void
	 */
	private function makeChildren($n)
	{
		for($i = 0; $i < $n; $i++) {
			$pid = pcntl_fork();
			switch ($pid) {
				case 0:    
					$this->isParent = false;            
					return 0;
				case -1:
					return 2;
				default:
					$this->pids[$pid] = 1; // 记录当前存活的进程
					break;
			}
		}
		return 1;
	}

	/**
	 * 给所有子进程发送信号,重启或退出时用
	 *
	 * @param [type] $signo
	 * @return void
	 */
	private function killAll($signo){
		$aliveCount = 0;
		foreach($this->pids as $pid => $value) {
			$ret = posix_kill($pid, $signo);
			if(!$ret) {
				$aliveCount++;
			}
		}
		if($aliveCount > 0) {
			$this->log("kill all alive count is : " . $aliveCount, ErrorLevel::WARNNING);
		}
	}

	/**
	 * 写入日志
	 *
	 * @param string $str
	 * @param string $level
	 * @return void
	 */
	private function log($str, $level = ErrorLevel::DEBUG){
		$str = sprintf("pid %s time %s level %s: %s" . PHP_EOL, posix_getpid(), microtime(true), $level, $str);
		fwrite ($this->logFd , $str);
	}
}

使用方式

$p = new Process(10);
$p->run();

验证拉起子进程

要打开141,142行的注释

> php Process.php

尽管时刻都有进程死去,但总的进程数量都是10

在这里插入图片描述

验证重启或退出

退出和重启的逻辑在114 和 124行之间,在收到信号时向所有子进程发送SIGQUIT,然后主进程感觉 $masterStage 变量决定重启还是退出。注意子进程如果是常驻进程一定要定期调用hearbeat方法,处理父进程发送的信号和检查父进程的存活。

kill -s SIGQUIT ${masterPid} #退出进程

在这里插入图片描述

kill -s SIGUSR2 ${masterPid} #重启所有进程

在这里插入图片描述

参考

[PHP] cli环境下php设置进程名字

PHP多进程与信号 pcntl,Signal
unix 环境高级编程 第8章

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
SimpleFork simple-fork-php 是基于 PCNTL 扩展的进程管理包,接口类似与 Java 的 Thread 和 Runnable 为什么要写 SimpleFork 多进程程序的编写相比较多线程编写更加复杂,需要考虑进程回收、同步、互斥、通信等问题。对于初学者来说,处理上述问题会比较困难。 尤其是信号处理和进程通信这块,很难做到不出问题。 SimpleFork提供一套类似于JAVA多线程的进程控制接口,提供回收、同步、互斥、通信等方案,开发者可以关注业务问题,不需要过多考虑进程控制。 引入 composer require jenner/simple_fork require path/to/SimpleFork/autoload.php 依赖 必须 ext-pcntl 进程控制 可选 ext-sysvmsg 消息队列 ext-sysvsem 同步互斥锁 ext-sysvshm 共享内存 特性 提供进程池 自动处理僵尸进程回收,支持无阻塞调用 提供共享内存、System V 消息队列、Semaphore锁,方便IPC通信(进程通信) 提供Process和Runnable两种方式实现进程 可以实时获取到进程状态 shutdown所有进程或单独stop一个进程时,可以注册覆盖beforeExit()方法,返回true则退出,false继续运行(在某些场景,进程不能立即退出) 支持子进程运行时reload 注意事项 System V 消息队列由于在程序退出时可能存在尚未处理完的数据,所以不会销毁。如果需要销毁,请调用$queue->remove()方法删除队列 共享内存会在所有进程退出后删除 Semaphore对象会在对象回收时进行销毁 进程池start()后,需要调用wait()进行僵尸进程回收,可以无阻塞调用 获取进程状态(调用isAlive()方法)前,最好调用一个无阻塞的wait(false)进行一次回收,由于进程运行状态的判断不是原子操作,所以isAlive()方法不保证与实际状态完全一致 如果你不清楚在什么情况下需要在程序的最开始加入declare(ticks=1);,那么最好默认第一行都加入这段声明。 如何使用declare(ticks=1); declare(ticks=1); 这段声明用于进程信号处理。如果注册了信号处理器,程序会没执行一行代码后自动检查是否有尚未处理的信号。http://php.net/manual/zh/control-structures.declare.php TODO 提供更多功能的进程池,模仿java 提供第三方进程通信机制(Redis等) 更多的测试及示例程序 示例程序 更多示例程序见exmples目录 simple.php class TestRunnable extends \Jenner\SimpleFork\Runnable{ /**      * 进程执行入口      * @return mixed      */ public function run() { echo "I am a sub process" . PHP_EOL;     } } $process = new \Jenner\SimpleFork\Process(new TestRunnable()); $process->start(); shared_memory.php class Producer extends \Jenner\SimpleFork\Process{ public function run(){ for($i = 0; $icache->set($i, $i); echo "set {$i} : {$i}" . PHH_EOL;         }     } } class Worker extends \Jenner\SimpleFork\Process{ public 

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值