https://blog.csdn.net/weixin_42075590/article/details/80740968
前言
刚刚确认这个 Chat 主题的时候,周围就有同事和同学质疑,有的说 多进程没有前途,有的说多进程就是神经病。虽然这些说法过于武断,但是不可否认的,是 PHP 在多进程方面确实不擅长。
既然如此,我还为什么要用 PHP 去实现多进程呢?原因有二。
第一,在开发过程中,我们需要学习许多关于 Linux 进程的一些知识,比如什么是进程,什么是写时复制,什么是信号,如何守护进程化,什么叫僵尸进程等。这些知识是 Linux 进程的知识,与 PHP 语言本身没有什么关系。我们如果能够使用 PHP 进行多进程编程,那么换成 C++ 语言也一样能够搞的定,当然前提是你会 C++。而且 PHP 和 C++ 的多进程相关的 api 也十分类似,例如 PHP 创建一个子进程用的是pcntl_fork() ,而 C++ 用的是 fork() 。
既然如此,为什么我不直接用 C++ 呢?效率比 PHP 更高。理由很简单,因为我是 PHPer,对 C++ 不熟。哈哈。
第二,作为一个 PHP 开发人员,在工作中难免遇到一些需要使用多进程的时候,虽然有一些著名的框架(swoole,workerman)可以用,但是我们必须知其然,且知其所以然。知其所以然的最好方式莫过于自己动手实践一番。
本人也使用多进程开发了两个项目,一是公司使用的推送系统,二是定时任务管理系统(个人开源项目)
环境准备
项目环境:
- 代码运行环境是 Linux,如果你使用 Windows 编程,建议你使用 vagrant,或者使用虚拟机或云服务器(用 ftp 同步代码)。
- 安装好 PHP
- 安装好 pcntl 扩展和 posix 扩展。如果你使用 lnmp 一键安装,那么这两个扩展应该是默认安装好的。否则请自行安装扩展,这里不对此进行详述。
建议学习方式(仅供参考):
- 建议使用 PC 看本文,因为有不少代码需要你去实践
- 先跳到文章最后下载完整代码,然后一边看代码一边看文章内容。最后参照我的源码自己写一遍。
hello world
按照惯例,上来就是一个通俗易懂的 hello world 一定能取得大部分开发人员的好感。
pcntl 扩展让 PHP 拥有进程创建,信号处理等能力。我们使用 pcntl_fork()
创建子进程:
-
<?php
-
//其他代码
-
$pid = pcntl_fork(); //fork进程
-
echo '父子进程都会执行的代码'.PHP_EOL;
-
if($pid > 0){
-
echo "我是父进程,我创建的子进程id为 {$pid}".PHP_EOL;
-
}else if($pid == 0){
-
echo '我是子进程'.PHP_EOL;
-
}else{
-
echo 'fork进程失败'.PHP_EOL;
-
}
运行上面的代码,可以得到如下结果:
-
[root@vagrant-centos65 default]# php test.php
-
父子进程都会执行的代码
-
我是父进程,我创建的子进程id为 8560
-
父子进程都会执行的代码
-
我是子进程
这段代码执行过程中,执行到 pcntl_fork()
时,将产生一个一模一样的脚本,即子进程,然后父进程和子进程分别继续执行之后的代码,互不干扰。
实际开发中我们需要父进程和子进程执行不一样的代码怎么办?
很简单,pcntl_fork()
为我们提供了一个返回值 pid,这个返回值有些特殊。在父进程脚本中,这个值是子进程的进程 id,而在子进程脚本中,这个值是 0,我们就可以用这个值区分父子进程。
运行过程示意图如下:
看到这里应该对多进程编程有了个大概的概念。
信号
多进程编程不是上面几行代码就搞定了的,我们还有许多周边问题需要解决,例如如何在进程间进行通讯,如何让进程常驻,并且不会随随便便的挂掉。
玩过 Linux 的肯定都用过这个命令 kill -9 进程号
来杀死一个进程,许多新手就误以为 kill 命令是杀死进程的命令,其实并不是,kill 命令是向进程发送一个信号,我们可以简单的将信号理解为是一个指令,9 才是真正的杀死进程的罪魁祸首。
除了 9 以为,还有 1、2、3 等几十种信号,可以使用 kill -l
查看信号列表:
-
[root@vagrant-centos65 default]# 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
有了信号,我们就能对进程发出指令,让进程执行退出,显示状态等动作。
每个信号都有一个默认响应动作,我们可以使用 pcntl_signal
来修改响应动作。我们来做个实验,先执行以下代码 php test.php
:
-
<?php
-
//注册SIGINT和SIGUSR2信号的响应
-
pcntl_signal(SIGINT, 'signalHandler', false); //SIGINT : 2
-
pcntl_signal(SIGUSR2, 'signalHandler', false); //SIGINT : 12
-
function signalHandler($signal){
-
echo "收到了信号:".$signal;
-
if($signal == SIGUSR2){
-
echo "进行业务操作1" . PHP_EOL;
-
}elseif($signal == SIGINT){
-
echo "进行业务操作2" . PHP_EOL;
-
}
-
// else if
-
}
-
while(true){
-
sleep(1);
-
//调用该方法,信号处理函数才会被执行
-
pcntl_signal_dispatch();
-
}
while 循环将使进程一直运行,不退出,然后我们另外开一个终端,先用 ps aux 命令找到它的进程 id,然后发信号:
-
[root@vagrant-centos65 default]# kill -2 8600
-
[root@vagrant-centos65 default]# kill -12 8600
回到第一个终端,就可以看到当前进程收到了信号,并执行了信号处理函数,结果如下:
-
[root@vagrant-centos65 default]# php test.php
-
收到了信号:2进行业务操作2
-
收到了信号:12进行业务操作1
PS:上面脚本无法使用ctrl+c退出,因为 ctrl+c 就是信号 2,响应动作被我们修改掉了。请使用 kill -9 进程号
退出。
理论上来说,我们可以修改任何一个信号的响应动作,但是不建议随便修改,例如 9(SIGKILL),这个信号一般都是用来强制杀死进程,如果被改掉了,将对运维人员造成一定的困扰。
SIGUSR1 和 SIGUSR2 是留给用户使用的。
在 PHP 中,使用 posix_kill
来向一个进程发出信号,后面代码会使用到。
守护进程化
守护进程是不受终端控制,在后台运行的进程。我们必须使用守护进程,因为你无法保证你的终端永远不被关闭。
守护进程化的步骤如下:
- 创建子进程,父进程自杀
- 子进程创建新会话,以此摆脱终端控制
代码如下:
-
<?php
-
//设置文件掩码
-
umask(0);
-
$pid = pcntl_fork();
-
if($pid > 0){
-
//父进程自杀
-
exit(0);
-
}elseif($pid == 0){
-
//子进程创建新会话,摆脱终端控制
-
if( -1 === posix_setsid() ){
-
throw new Exception("setsid fail");
-
}
-
//此处可以结合上面的信号代码
-
while(1){
-
sleep(1);
-
//pcntl_signal_dispatch();
-
}
-
}else{
-
throw new Exception("fork fail");
-
}
执行上面脚本后我们可以使用 ps aux
命令查看到该脚本在后台默默运行
master-worker 进程模型
master-worker 进程模型是一个比较经典且常用的进程模式,nginx 也是用这个模型:
-
[root@vagrant-centos65 default]# ps -ef | grep nginx
-
root 1096 1 0 May04 ? 00:00:00 nginx: master process /usr/local/nginx/sbin/nginx -c /usr/local/nginx/conf/nginx.conf
-
www 1097 1096 0 May04 ? 00:02:20 nginx: worker process
-
www 1098 1096 0 May04 ? 00:00:00 nginx: worker process
模型示意图:
worker 进程是 master 进程的子进程,worker进程负责具体的工作,而 master 进程仅负责监控 worker 进程的状态,一旦发现 worker 进程挂掉了,就立刻 fork 一个新的 worker 进程,以此来保证程序的高可用。
到这里你可能会有疑问,如果 master 也挂掉了怎么办?其实不用担心,worker 进程会挂掉基本上都是因为业务上抛异常导致的,而 master 进程仅负责监控,不做任何业务处理,只要你不主动杀死它,它挂掉的概率几乎为零。
worker 进程的数量建议等于 CPU 核心数,或整数倍,这样可以最有效合理地利用 CPU 资源。
master 和 worker 之间则可以使用信号进行通讯。假设我们现在要 stop 这个程序,我们只需要向 master 发出一个信号,然后 master 分别向各个 worker 发出信号让 worker 停止,最后 master 自己也停止。
有了上面的基础,我们就可以正式开始开发。
开发步骤一:环境检查
首先我们分别创建 Worker.php
和 test.php
:
worker.php
-
<?php
-
class Worker{
-
public static function runAll(){
-
static::checkEnv();
-
}
-
public static function checkEnv(){
-
if (php_sapi_name() != 'cli') {
-
exit('请使用命令行模式运行!');
-
}
-
if(!function_exists('posix_kill')){
-
exit('请先安装posix扩展'."\n");
-
}
-
if(!function_exists('pcntl_fork')){
-
exit('请先安装pcntl扩展'."\n");
-
}
-
}
-
}
test.php
-
<?php
-
require 'Worker.php';
-
Worker::runAll();
php test.php
运行
下面所有代码都写在Worker类中,重复代码不再贴出
开发步骤二:初始化
在项目中,我们还需要一些必备的初始化操作,例如配置日志文件路径,目录权限检查等:
-
public static $log_file = '';
-
//将master进程id保存到这个文件中
-
public static $pid_file = '';
-
//保存worker进程的状态
-
public static $status_file = '';
-
public static function runAll(){
-
static::checkEnv();
-
static::init();
-
}
-
public static function init(){
-
//$temp_dir = sys_get_temp_dir() . '/myworker/';
-
$temp_dir = __DIR__.'/tmp/';
-
if (!is_dir($temp_dir) && !mkdir($temp_dir)) {
-
exit('mkdir runtime fail');
-
}
-
$test_file = $temp_dir . 'test';
-
//尝试创建文件
-
if(touch($test_file)){
-
@unlink($test_file);
-
}else{
-
exit('permission denied: dir('.$temp_dir.')');
-
}
-
if (empty(static::$status_file)) {
-
static::$status_file = $temp_dir . 'status_file.status';
-
}
-
if (empty(self::$pid_file)) {
-
static::$pid_file = $temp_dir . 'master.pid';
-
}
-
if (empty(self::$log_file)) {
-
static::$log_file = $temp_dir . 'worker.log';
-
}
-
static::log('初始化完成');
-
}
-
public static function log($message)
-
{
-
$message = '['.date('Y-m-d H:i:s') .']['. $message . "]\n";
-
file_put_contents((string)self::$log_file, $message, FILE_APPEND | LOCK_EX);
-
}
开发步骤三:解析命令
启动和停止项目都是使用命令行操作,在 PHP 中,使用 global $argv
可以获取我们输入的命令,如:
-
<?php
-
global $argv;
-
var_dump($argv);
测试:
-
[root@vagrant-centos65 myworker]# php test_arvg.php a b c
-
array(4) {
-
[0]=>
-
string(13) "test_arvg.php"
-
[1]=>
-
string(1) "a"
-
[2]=>
-
string(1) "b"
-
[3]=>
-
string(1) "c"
-
}
我们启动多进程项目的命令一般格式是:php test.php start -d
,-d 是可选选项,表示使用守护进程模式启动,适合项目正式运行时使用。没有 -d 则是调试模式,所有信息都会输出到终端,适合在开发阶段使用。
代码如下:
-
//是否使用守护进程模式启动
-
public static $deamonize = false;
-
public static function runAll(){
-
static::checkEnv();
-
static::init();
-
static::parseCommand();
-
}
-
public static function parseCommand(){
-
global $argv;
-
if(!isset($argv[1]) || !in_array($argv[1],['start','stop','status'])){
-
exit('usage: php your.php start | stop | status !' . PHP_EOL);
-
}
-
$command1 = $argv[1]; //start , stop , status
-
$command2 = $argv[2]; // -d
-
//检测master是否正在运行
-
$master_id = @file_get_contents(static::$pid_file);
-
//向master进程发送0信号,0信号比较特殊,进程不会响应,但是可以用来检测进程是否存活
-
$master_alive = $master_id && posix_kill($master_id,0);
-
if($master_alive){
-
//不能重复启动
-
if($command1 == 'start' && posix_getpid() != $master_id){
-
exit('worker is already running !'.PHP_EOL);
-
}
-
}else{
-
//项目未启动的情况下,只有start命令有效
-
if ($command1 != 'start') {
-
exit('worker not run!' . PHP_EOL);
-
}
-
}
-
switch($command1){
-
case 'start':
-
if($command2 == '-d'){
-
static::$deamonize = true;
-
}
-
break;
-
case 'stop':
-
//停止进程
-
//必须exit退出
-
exit(0);
-
break;
-
case 'status':
-
//查看状态
-
//必须exit退出
-
exit(0);
-
break;
-
default:
-
exit('usage: php your.php start | stop | status !' . PHP_EOL);
-
break;
-
}
-
}
stop和status 功能后面再说,我们先实现 start 功能,在 parseComman() 方法中,start 不需要进行任何实质性的操作。
开发步骤四:守护进程化
解析完命令,如果是 start 操作,立即进行守护进程化操作:
-
public static function runAll(){
-
static::checkEnv();
-
static::init();
-
static::parseCommand();
-
static::deamonize();
-
}
-
public static function deamonize(){
-
if(static::$deamonize == false){
-
return;
-
}
-
umask(0);
-
$pid = pcntl_fork();
-
if($pid > 0){
-
exit(0);
-
}elseif($pid == 0){
-
if(-1 === posix_setsid()){
-
throw new Exception("setsid fail");
-
}
-
static::setProcessTitle('myworker: master');
-
}else{
-
throw new Exception("fork fail");
-
}
-
}
-
public static function setProcessTitle($title){
-
//设置进程名
-
if (function_exists('cli_set_process_title')) {
-
@cli_set_process_title($title);
-
}
-
}
守护进程化在前面就说过了,不再重复。
cli_set_process_title
是设置进程的名称,方便以后查看进程,就像用 ps 命令查看 nginx,下图红框中就是进程名:
开发步骤五:保存 master 进程号
代码运行到这里,当前进程就是 master 进程了,我们要将 master 进程 id 保存到文件中:
-
public static $master_pid = 0;
-
public static function runAll(){
-
static::checkEnv();
-
static::init();
-
static::parseCommand();
-
static::deamonize();
-
static::saveMasterPid();
-
}
-
public static function saveMasterPid(){
-
static::$master_pid = posix_getpid();
-
if(false === @file_put_contents(static::$pid_file, static::$master_pid)){
-
throw new Exception('fail to save master pid ');
-
}
-
}
开发步骤六:注册信号处理
上面已经解释过信号,现在我们要注册几个信号处理函数,分别对应 stop,status 操作
-
public static function runAll(){
-
static::checkEnv();
-
static::init();
-
static::parseCommand();
-
static::deamonize();
-
static::saveMasterPid();
-
static::installSignal();
-
}
-
public static function installSignal()
-
{
-
pcntl_signal(SIGINT, array(__CLASS__, 'signalHandler'), false);
-
pcntl_signal(SIGUSR2, array(__CLASS__, 'signalHandler'), false);
-
//SIG_IGN表示忽略该信号,不做任何处理。SIGPIPE默认会使进程退出
-
pcntl_signal(SIGPIPE, SIG_IGN, false);
-
}
-
public static function signalHandler($signal){
-
switch ($signal) {
-
case SIGINT: // Stop.
-
//static::stopAll();
-
break;
-
case SIGUSR2: // Show status.
-
//static::writeStatus();
-
break;
-
}
-
}
stop 和 status 的具体实现后面再说
开发步骤七:重定向输入输出
如果是使用守护进程的模式启动项目,我们要求系统代码中的 echo、var_dump 等方法输出的内容不能显示到终端,因此需要将标准输出给重定向到 /dev/null 中。
如果是调试模式,则不需要该步骤中提供了 global $STDOUT, $STDERR;
让我们能够获取到标准输出。
-
public static $stdoutFile = '/dev/null';
-
public static function runAll(){
-
static::checkEnv();
-
static::init();
-
static::parseCommand();
-
static::deamonize();
-
static::saveMasterPid();
-
static::installSignal();
-
static::resetStd();
-
}
-
public static function resetStd(){
-
if(static::$deamonize == false){
-
return;
-
}
-
global $STDOUT, $STDERR;
-
$handle = fopen(self::$stdoutFile, "a");
-
if ($handle) {
-
unset($handle);
-
@fclose(STDOUT);
-
@fclose(STDERR);
-
$STDOUT = fopen(self::$stdoutFile, "a");
-
$STDERR = fopen(self::$stdoutFile, "a");
-
} else {
-
throw new Exception('can not open stdoutFile ' . self::$stdoutFile);
-
}
-
}
Linux 中的 /dev/null
是一个黑洞,丢进去的东西都将消失不见,相当于垃圾桶。
开发步骤八:fork 子进程
重头戏来了,这一步是核心步骤,首先我们需要确定我们要几个 worker 进程,然后循环 fork 出来,值得注意的是,fork 完成后,master 进程和 worker 进程都要使用 while 死循环保持运行状态,否则代码执行完毕就会自动退出了。
-
public static $workers = [];
-
//worker实例
-
public static $instance = null;
-
//worker数量
-
public $count = 2;
-
//worker启动时的回调方法
-
public $onWorkerStart = null;
-
public function __construct(){
-
static::$instance = $this;
-
}
-
public static function runAll(){
-
static::checkEnv();
-
static::init();
-
static::parseCommand();
-
static::deamonize();
-
static::saveMasterPid();
-
static::installSignal();
-
static::resetStd();
-
static::forkWorkers();
-
}
-
public static function forkWorkers(){
-
$worker_count = static::$instance->count;
-
while(count(static::$workers) < $worker_count ){
-
static::forkOneWorker(static::$instance);
-
}
-
}
-
public static function forkOneWorker($instance){
-
$pid = pcntl_fork();
-
if($pid > 0){
-
static::$workers[$pid] = $pid;
-
}elseif($pid == 0){
-
static::log('创建了一个worker');
-
static::setProcessTitle('myworker process');
-
//运行
-
$instance->run();
-
}else{
-
throw new Exception('fork one worker fail');
-
}
-
}
-
public function run(){
-
if($this->onWorkerStart){
-
try {
-
//worker启动,调用onWorkerStart回调
-
call_user_func($this->onWorkerStart, $this);
-
} catch (\Exception $e) {
-
static::log($e);
-
sleep(1);
-
exit(250);
-
} catch (\Error $e) {
-
static::log($e);
-
sleep(1);
-
exit(250);
-
}
-
}
-
//死循环,保持worker运行,并且一有信号来了就调用信号处理函数
-
while (1) {
-
pcntl_signal_dispatch();
-
sleep(1);
-
}
-
}
修改 test.php:
-
<?php
-
require 'Worker.php';
-
$worker = new Worker();
-
$worker->count = 2;
-
$worker->onWorkerStart = function($worker){
-
echo 'onWorkerStart' . PHP_EOL;
-
};
-
Worker::runAll();
业务代码一般写在onWorkerStart
,worker启动时会调用该方法。比如可以开一个socket,阻塞等待用户连接。
调试模式运行:
-
[root@vagrant-centos65 myworker]# php test.php start
-
onWorkerStart
-
onWorkerStart
-
[root@vagrant-centos65 myworker]# ps aux |grep myworker
-
root 8813 0.0 0.1 154664 5532 pts/3 S 14:43 0:00 myworker process
-
root 8814 0.0 0.1 154664 5532 pts/3 S 14:43 0:00 myworker process
运行起来后,发现 onWorkerStart 都正常输出了,但是 master 进程却不见了,这是咋回事?因为上面代码中我们没有让 master 死循环,它执行完代码就退出了。下面我们就来做这件事。
开发步骤九:监控 worker 进程
为什么master 死循环的操作不在上面代码中直接实现,而要单独出来讲,因为master 进程不仅要保持运行,它还肩负着监控 worker 进程的重任,一旦发现 worker 进程意外退出了,就立刻重新 fork 一个 worker 进程,做到高可用。
-
//记录当前进程的状态
-
public static $status = 0;
-
//运行中
-
const STATUS_RUNNING = 1;
-
//停止
-
const STATUS_SHUTDOWN = 2;
-
public static function runAll(){
-
static::checkEnv();
-
static::init();
-
static::parseCommand();
-
static::deamonize();
-
static::saveMasterPid();
-
static::installSignal();
-
static::resetStd();
-
static::forkWorkers();
-
static::monitorWorkers();
-
}
-
public static function monitorWorkers(){
-
//设置当前状态为运行中
-
static::$status = static::STATUS_RUNNING;
-
while (1) {
-
pcntl_signal_dispatch();
-
$status = 0;
-
//阻塞,等待子进程退出
-
$pid = pcntl_wait($status, WUNTRACED);
-
self::log("worker[ $pid ] exit with signal:".pcntl_wstopsig($status));
-
pcntl_signal_dispatch();
-
//child exit
-
if ($pid > 0) {
-
//意外退出时才重新fork,如果是我们想让worker退出,status = STATUS_SHUTDOWN
-
if (static::$status != static::STATUS_SHUTDOWN) {
-
unset(static::$workers[$pid]);
-
static::forkOneWorker(static::$instance);
-
}
-
}
-
}
-
}
上面代码中最关键的就是 pcntl_wait
,该方法的作用是等待子进程退出,一旦有子进程退出,就会返回退出的进程的 id,然后就重新 fork 一个 worker 进程。
注意:到了这一步,要先删除前面测试产生的 master.pid 文件并且 kill -9 前面运行起来的 worker 进程。否则会影响下面的实验
我们来实验一下,先使用守护进程模式启动:
-
[root@vagrant-centos65 myworker]# php test.php start -d
-
[root@vagrant-centos65 myworker]# ps aux | grep myworker
-
root 8844 0.0 0.1 154660 5528 ? Ss 15:20 0:00 myworker: master
-
root 8845 0.0 0.1 154660 5508 ? S 15:20 0:00 myworker process
-
root 8846 0.0 0.1 154660 5508 ? S 15:20 0:00 myworker process
可以看到三个进程都正常运行,进程 id 分别为 8844、8845、8846。
然后我们用kill命令杀死 8845,再次查看进程状态:
-
[root@vagrant-centos65 myworker]# kill -9 8845
-
[root@vagrant-centos65 myworker]# ps aux | grep myworker
-
root 8844 0.0 0.1 154660 5636 ? Ss 15:20 0:00 myworker: master
-
root 8846 0.0 0.1 154660 5508 ? S 15:20 0:00 myworker process
-
root 8849 0.0 0.1 154660 5508 ? S 15:22 0:00 myworker process
根据上面结果,8845 确实被我们杀掉了,并且重新启动了一个新的 worker,进程 id 为 8849。
到此,多进程的启动已经全部实现,并且整个项目已经完成了四分之三。
PS:由于 stop 功能还没实现,所以需要手动 kill master 和 worker 进程,注意要先 kill master,再 kill worker,然后手动删除 master.pid 文件
小知识:pcntlwait() 方法不仅可以监控子进程退出,而且有回收资源的作用。fork 出来的每一个子进程最终都要使用 pcntlwait 进行资源回收,否则就会产生僵尸进程。僵尸进程就是代码执行完了却没有被回收的进程,会白白占用 CPU 等资源,造成系统资源浪费。
开发步骤十:查看进程状态
查看进程状态这一步并不是必须的,你可以直接用 ps aux | grep myworker
查看即可。
但如果你并不满足,想要查看更多的状态,例如查看一些业务状态,那么就必须要开发这个功能。
原理其实很简单,我们给 master 发一个信号,然后 master 分别给各个 worker 发一个信号。worker 收到这个信号,就往一个文件里写入一些信息(状态信息),然后我们再读取这个文件的内容并显示出来就可以了。
这一步,我们需要修改前面的 parseCommand()
和 signalHandler()
方法的代码,如果不明白,请先回头看一看。
-
pubilc static function parseCommand(){
-
//其他代码,重复的不贴出来了
-
switch($command1){
-
// case start 和 case stop的代码就不贴出来了
-
case 'status':
-
//查看状态
-
if(is_file(static::$status_file)){
-
//先删除就得status文件
-
@unlink(static::$status_file);
-
}
-
//给master发送信号
-
posix_kill($master_id,SIGUSR2);
-
//等待worker进程往status文件里写入状态
-
usleep(300000);
-
@readfile(static::$status_file);
-
exit(0);
-
break;
-
default:
-
exit('usage: php your.php start | stop | status !' . PHP_EOL);
-
break;
-
}
-
}
-
public static function signalHandler($signal){
-
switch ($signal) {
-
case SIGINT: // Stop.
-
//static::stopAll();
-
break;
-
case SIGUSR2: // Show status.
-
//master和worker都执行
-
static::writeStatus();
-
break;
-
}
-
}
-
public static function writeStatus(){
-
$pid = posix_getpid();
-
if($pid == static::$master_pid){ //master进程
-
$master_alive = static::$master_pid&& posix_kill(static::$master_pid,0);
-
$master_alive = $master_alive ? 'is running' : 'die';
-
$result = file_put_contents(static::$status_file, 'master[' . static::$master_pid . '] ' . $master_alive . PHP_EOL, FILE_APPEND | LOCK_EX);
-
//给worker进程发信号
-
foreach(static::$workers as $pid){
-
posix_kill($pid,SIGUSR2);
-
}
-
}else{ //worker进程
-
$name = 'worker[' . $pid . ']';
-
$alive = $pid && posix_kill($pid, 0);
-
$alive = $alive ? 'is running' : 'die';
-
file_put_contents(static::$status_file, $name . ' ' . $alive . PHP_EOL, FILE_APPEND | LOCK_EX);
-
}
-
}
测试:
-
[root@vagrant-centos65 myworker]# php test.php start -d
-
[root@vagrant-centos65 myworker]# php test.php status
-
master[8905] is running
-
worker[8906] is running
-
worker[8907] is running
PS:Worker 往文件写状态信息时,可以根据具体的业务写入更多的数据,这点可以自由发挥。
开发步骤十一:停止进程
停止进程的和查看进程状态的原理是一样的,向 master 进程发一个信号叫他go die,然后 master 向 worker 进程发信号叫他们 go die,然后大家一起 go die。
同样的修改 parseCommand()
和 signalHandler()
方法:
-
public static function signalHandler($signal){
-
switch ($signal) {
-
case SIGINT: // Stop.
-
static::stopAll();
-
break;
-
case SIGUSR2: // Show status.
-
//master和worker都执行
-
static::writeStatus();
-
break;
-
}
-
}
-
pubilc static function parseCommand(){
-
//其他代码,重复的不贴出来了
-
switch($command1){
-
// case start 和 case status的代码就不贴出来了
-
case 'stop':
-
//停止进程
-
$master_id && posix_kill($master_id, SIGINT);
-
//只要还没杀死master,就一直杀
-
while ($master_id && posix_kill($master_id, 0)) {
-
usleep(300000);
-
}
-
exit(0);
-
break;
-
default:
-
exit('usage: php your.php start | stop | status !' . PHP_EOL);
-
break;
-
}
-
}
-
public static function stopAll(){
-
$pid = posix_getpid();
-
if($pid == static::$master_pid){ //master进程
-
//将当前状态设为停止,否则子进程一退出master重新fork
-
static::$status = static::STATUS_SHUTDOWN;
-
//通知子进程退出
-
foreach(static::$workers as $pid){
-
posix_kill($pid,SIGINT);
-
}
-
//删除pid文件
-
@unlink(static::$pid_file);
-
exit(0);
-
}else{ //worker进程
-
static::log('worker[' . $pid .'] stop');
-
exit(0);
-
}
-
}
测试:
-
[root@vagrant-centos65 myworker]# php test.php start -d
-
[root@vagrant-centos65 myworker]# php test.php status
-
master[8920] is running
-
worker[8921] is running
-
worker[8922] is running
-
[root@vagrant-centos65 myworker]# php test.php stop
到此,多进程模型全部开发了。根据 status 和 stop 的原理,大家可以自行开发 reload,restart 等功能。
除此之外,我们在 worker 启动时调用了 onWorkerStart 回调,同理的,我们也可以开发 onWorkerStop 等回调。
扩展:定时器 timer
在多进程模型中,我们经常使用到定时器。其实定时器就是用了信号来实现。
pcntl 扩展为我们提供了pcntl_alarm()
方法,调用 pcntl_alarm(1)
,系统就会在 1 秒之后给当前进程发一个SIGALRM
信号,收到信号时,我们再次调用 pcntl_alarm(1)
,如此不断循环,就会每秒执行一次信号处理函数,就这样形成了定时器。
了解了原理,代码其实很简单,仔细阅读一下就明白了。
Timer.php
-
<?php
-
class Timer
-
{
-
public static $tasks = array();
-
public static function init()
-
{
-
pcntl_signal(SIGALRM, array( __CLASS__, 'signalHandle'), false);
-
}
-
public static function signalHandle()
-
{
-
pcntl_alarm(1);
-
if (empty(self::$tasks)) {
-
return;
-
}
-
//执行任务
-
foreach (self::$tasks as $run_time => $task) {
-
$time_now = time();
-
if ($time_now >= $run_time) {
-
$func = $task[0];
-
$args = $task[1];
-
$interval = $task[2];
-
$persistent = $task[3];
-
call_user_func_array($func, $args);
-
unset(self::$tasks[$run_time]);
-
if($persistent){
-
Timer::add($interval, $func, $args,$persistent);
-
}
-
}
-
}
-
}
-
/**
-
* @param $interval 几秒后执行
-
* @param $func 要执行的回调方法
-
* @param array $args 参数
-
* @param bool $persistent 是否持续执行
-
* @return bool
-
*/
-
public static function add($interval, $func, $args = array(),$persistent = true)
-
{
-
if ($interval <= 0) {
-
echo new Exception('wrong interval');
-
return false;
-
}
-
if (!is_callable($func)) {
-
echo new Exception('not callable');
-
return false;
-
} else {
-
$runtime = time() + $interval;
-
self::$tasks[$runtime] = array($func, $args, $interval,$persistent);
-
return true;
-
}
-
}
-
public static function tick()
-
{
-
pcntl_alarm(1);
-
}
-
}
测试:
-
<?php
-
require 'Worker.php';
-
require 'Timer.php';
-
$worker = new Worker();
-
$worker->count = 2;
-
$worker->onWorkerStart = function($worker){
-
Timer::init();
-
//2秒执行一次
-
Timer::add(2,function(){
-
$pid = posix_getpid();
-
echo date('Y-m-d H:i:s') .' pid : ' . $pid.PHP_EOL;
-
},[],true);
-
//注意,一定要调用tick方法定时器才会运行
-
Timer::tick();
-
};
-
Worker::runAll();
为了直观的看到效果,使用调试模式运行:
-
[root@vagrant-centos65 myworker]# php test.php start
-
2018-05-14 16:38:53 pid : 9181
-
2018-05-14 16:38:53 pid : 9182
-
2018-05-14 16:38:55 pid : 9181
-
2018-05-14 16:38:55 pid : 9182
-
2018-05-14 16:38:57 pid : 9182
-
2018-05-14 16:38:57 pid : 9181
-
2018-05-14 16:38:59 pid : 9182
-
2018-05-14 16:38:59 pid : 9181
总结
主要知识点:
- 使用 pcntl_fork 创建子进程
- 守护进程化:fork 一个进程并且杀死自己
- 使用 pcntl_signal 注册信号处理方法
- 使用 posix_kill 向指定进程发送信号
- worker 进程 while 循环等待信号
- master 进程 while 循环中持续监控 worker 进程状态
- 在 onWorkerStart 回调中实现业务代码
完整代码下载:
- CSDN 下载(需要 1 积分):https://download.csdn.net/download/u010837612/10413528
- 网盘下载:链接: https://pan.baidu.com/s/1MI4E1Y521I5Q5UEyvCQvcg 密码: x8wn
建议学习方式:先下载完整代码,然后一边看代码一边看文章内容。最后参照我的源码自己写一遍。
其他学习资料:本人最开始学习这一块内容是通过研究 workerman 的源码,workerman 是非常好的学习资料,有兴趣的朋友可以看看。
最后,感谢大家的支持。