2019-06-27

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

2018-09-02

系统设计

语言

PHP

经过 用 PHP 玩转进程之一 — 基础 的回顾复习,我们已经掌握了进程的基础知识,现在可以尝试用 PHP 做一些简单的进程控制和管理,来加深我们对进程的理解。接下来,我将用多进程模型实现一个简单的 PHPServer,基于它你可以做任何事。

PHPServer 完整的源代码,可前往 fan-haobai/php-server 获取。

总流程

该 PHPServer 的 Master 和 Worker 进程主要控制流程,如下图所示:

其中,主要涉及 3 个对象,分别为 入口脚本Master 进程Worker 进程。它们扮演的角色如下:

入口脚本:主要实现 PHPServer 的启动、停止、重载功能,即触发 Master 进程start、stop、reload流程;

Master 进程:负责创建并监控 Worker 进程。在启动阶段,会注册信号处理器,然后创建 Worker;在运行阶段,会持续监控 Worker 进程健康状态,并接受来自入口脚本的控制信号并作出响应;在停止阶段,会停止掉所有 Worker 进程;

Worker 进程:负责执行业务逻辑。在被 Master 进程创建后,就处于持续运行阶段,会监听到来自 Master 进程的信号,以实现自我的停止;

整个过程,又包括 4 个流程

流程 ① :以守护态启动 PHPServer 时的主要流程。入口脚本会进行 daemonize,也就是实现进程的守护态,此时会fork出一个 Master 进程;Master 进程先经过 保存 PID注册信号处理器 操作,然后 创建 Worker 会fork出多个 Worker 进程;

流程 ② :为 Master 进程持续监控的流程,过程中会捕获入口脚本发送来的信号。主要监控 Worker 进程健康状态,当 Worker 进程异常退出时,会尝试创建新的 Worker 进程以维持 Worker 进程数量;

流程 ③ :为 Worker 进程持续运行的流程,过程中会捕获 Master 进程发送来的信号。流程 ① 中 Worker 进程被创建后,就会持续执行业务逻辑,并阻塞于此;

流程 ④ :停止 PHPServer 的主要流程。入口脚本首先会向 Master 进程发送 SIGINT 信号,Master 进程捕获到该信号后,会向所有的 Worker 进程转发 SIGINT 信号(通知所有的 Worker 进程终止),等待所有 Worker 进程终止退出;

在流程 ② 中,Worker 进程被 Master 进程fork出来后,就会 持续运行 并阻塞于此,只有 Master 进程才会继续后续的流程。

代码实现

启动

启动流程见 流程 ①,主要包括 守护进程保存 PID注册信号处理器创建多进程 Worker 这 4 部分。

守护进程

首先,在入口脚本中fork一个子进程,然后该进程退出,并设置新的子进程为会话组长,此时的这个子进程就会脱离当前终端的控制。如下图所示:

这里使用了 2 次fork,所以最后fork的一个子进程才是 Master 进程,其实一次fork也是可以的。代码如下:

protectedstaticfunctiondaemonize()

{

umask(0);

    $pid = pcntl_fork();

if(-1=== $pid) {

exit("process fork fail\n");

}elseif($pid >0) {

exit(0);

    }

// 将当前进程提升为会话leader

if(-1=== posix_setsid()) {

exit("process setsid fail\n");

    }

// 再次fork以避免SVR4这种系统终端再一次获取到进程控制

    $pid = pcntl_fork();

if(-1=== $pid) {

exit("process fork fail\n");

}elseif(0!== $pid) {

exit(0);

    }

}

通常在启动时增加-d参数,表示进程将运行于守护态模式。

当顺利成为一个守护进程后,Master 进程已经脱离了终端控制,所以有必要关闭标准输出和标准错误输出。如下:

protectedstaticfunctionresetStdFd()

{

global$STDERR, $STDOUT;

//重定向标准输出和错误输出

    @fclose(STDOUT);

    fclose(STDERR);

$STDOUT = fopen(static::$stdoutFile,'a');

$STDERR = fopen(static::$stdoutFile,'a');

}

保存PID

为了实现 PHPServer 的重载或停止,我们需要将 Master 进程的 PID 保存于 PID 文件中,如php-server.pid文件。代码如下:

protectedstaticfunctionsaveMasterPid()

{

// 保存pid以实现重载和停止

static::$_masterPid = posix_getpid();

if(false=== file_put_contents(static::$pidFile,static::$_masterPid)) {

exit("can not save pid to".static::$pidFile ."\n");

    }

echo"PHPServer start\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);

}

protectedstaticfunctionsignalHandler($signal)

{

switch($signal) {

caseSIGINT:

caseSIGTERM:

static::stop();

break;

caseSIGQUIT:

caseSIGUSR1:

static::reload();

break;

default:break;

    }

}

其中,SIGINT 和 SIGTERM 信号会触发stop操作,即终止所有进程;SIGQUIT 和 SIGUSR1 信号会触发reload操作,即重新加载所有 Worker 进程;此处忽略了 SIGUSR2 和 SIGHUP 信号,但是并未忽略 SIGKILL 信号,即所有进程都可以被强制kill掉。

创建多进程WORKER

Master 进程通过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("fork one worker fail");

    }

}

protectedstaticfunctionforkWorkers()

{

while(count(static::$_workers)

static::forkOneWorker();

    }

}

Worker进程的持续运行

Worker 进程的持续运行,见 流程 ③ 。其内部调度流程,如下图:

对于 Worker 进程,run()方法主要执行具体业务逻辑,当然 Worker 进程会被阻塞于此。对于 任务 ① 这里简单地使用while来模拟调度,实际中应该使用事件(Select 等)驱动。

publicstaticfunctionrun()

{

// 模拟调度,实际用event实现

while(1) {

// 捕获信号

        pcntl_signal_dispatch();

call_user_func(function(){

// do something

usleep(200);

        });

    }

}

其中,pcntl_signal_dispatch()会在每次调度过程中,捕获信号并执行注册的信号处理器。

Master进程的持续监控

调度流程

Master 进程的持续监控,见 流程 ② 。其内部调度流程,如下图:

对于 Master 进程的调度,这里也使用了while,但是引入了wait的系统调用,它会挂起当前进程,直到一个子进程退出或接收到一个信号。

protectedstaticfunctionmonitor()

{

while(1) {

// 这两处捕获触发信号,很重要

        pcntl_signal_dispatch();

// 挂起当前进程的执行直到一个子进程退出或接收到一个信号

$status =0;

        $pid = pcntl_wait($status, WUNTRACED);

        pcntl_signal_dispatch();

if($pid >=0) {

// worker健康检查

static::checkWorkerAlive();

        }

// 其他你想监控的

    }

}

第两次的pcntl_signal_dispatch()捕获信号,是由于wait挂起时间可能会很长,而这段时间可能恰恰会有信号,所以需要再次进行捕获。

其中,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();

if(is_file(static::$pidFile)) {

@unlink(static::$pidFile);

        }

exit(0);

}else{// 子进程退出

// 退出前可以做一些事

exit(0);

    }

}

若是 Master 进程执行该方法,会先调用stopAllWorkers()方法,向所有的 Worker 进程发送 SIGINT 信号并等待所有 Worker 进程终止退出,再清除 PID 文件并退出。有一种特殊情况,Worker 进程退出超时时,Master 进程则会再次发送 SIGKILL 信号强制杀死所有 Worker 进程;

由于 Master 进程会发送 SIGINT 信号给 Worker 进程,所以 Worker 进程也会执行该方法,并会直接退出。

protectedstaticfunctionstopAllWorkers()

{

$allWorkerPid =static::getAllWorkerPid();

foreach($allWorkerPidas$workerPid) {

        posix_kill($workerPid, SIGINT);

    }

// 子进程退出异常,强制kill

usleep(1000);

if(static::isAlive($allWorkerPid)) {

foreach($allWorkerPidas$workerPid) {

static::forceKill($workerPid);

        }

    }

// 清空worker实例

static::$_workers =array();

}

重载

代码发布后,往往都需要进行重新加载。其实,重载过程只需要重启所有 Worker 进程即可。流程如下图:

整个过程共有 2 个流程,流程 ① 终止所有的 Worker 进程,流程 ② 为 Worker 进程的健康检查 。其中流程 ① ,入口脚本给 Master 进程发送 SIGUSR1 信号,Master 进程捕获到该信号,执行信号处理器调用reload()方法,reload()方法调用stopAllWorkers()方法。如下:

protectedstaticfunctionreload()

{

// 停止所有worker即可,master会自动fork新worker

static::stopAllWorkers();

}

reload()方法只会在 Master 进程中执行,因为 SIGQUIT 和 SIGUSR1 信号不会发送给 Worker 进程。

你可能会纳闷,为什么我们需要重启所有的 Worker 进程,而这里只是停止了所有的 Worker 进程?这是因为,在 Worker 进程终止退出后,由于 Master 进程对 Worker 进程的健康检查作用,会自动重新创建所有 Worker 进程。

运行效果

到这里,我们已经完成了一个多进程 PHPServer。我们来体验一下:

$ phpserver.php

Usage: Commands [mode]

Commands:

start Start worker.

stop Stop worker.

reload Reload codes.

Options:

-dto startinDAEMON mode.

Use"--help"formore information about acommand.

首先,我们启动它:

$ phpserver.php start -d

PHPServer start   [OK]

其次,查看进程树,如下:

$ pstree -p

init(1)-+-init(3)---bash(4)

        |-php(1286)-+-php(1287)

                    `-php(1288)

最后,我们把它停止:

$ phpserver.php stop

PHPServer stopping ...

PHPServer stop success

现在,你是不是感觉进程控制其实很简单,并没有我们想象的那么复杂。( ̄┰ ̄*)

总结

我们已经实现了一个简易的多进程 PHPServer,模拟了进程的管理与控制。需要说明的是,Master 进程可能偶尔也会异常地崩溃,为了避免这种情况的发生:

首先,我们不应该给 Master 进程分配繁重的任务,它更适合做一些类似于调度和管理性质的工作;

其次,可以使用 Supervisor 等工具来管理我们的程序,当 Master 进程异常崩溃时,可以再次尝试被拉起,避免 Master 进程异常退出的情况发生。

相关文章 »

用PHP玩转进程之一 — 基础 (2018-08-28)

本文作者: 樊浩柏科学院

本文链接: https://www.fanhaobai.com/2018/09/process-php-multiprocess-server.html

版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 3.0 许可协议,转载请注明出处!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,179评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,229评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,032评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,533评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,531评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,539评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,916评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,813评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,568评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,654评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,354评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,937评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,918评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,152评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,852评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,378评论 2 342

推荐阅读更多精彩内容

  • 首发于 樊浩柏科学院 经过 用 PHP 玩转进程之一 — 基础 的回顾复习,我们已经掌握了进程的基础知识,现在可以...
    Howborn阅读 522评论 0 1
  • 多态 polymorphism 一个接口,多种方法 使用虚函数实现 基类成员函数添加了virtual关键字,该函数...
    ustclcl阅读 102评论 0 0
  • 动机: 之前的方法都使用固定的先验模型去提取图像的表示,但是不能根据数据调整学到的先验信息。 方法: “top-d...
    kikyou123阅读 390评论 0 0
  • Back in Antarctica,the emperor penguins' trek to their br...
    XY圆圆阅读 285评论 0 0
  • http://lab.haier.com/xhds2017/#/project/228 捧场捧场,谢谢ÿÿ
    莫陌_d0db阅读 114评论 0 0