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

首发于 樊浩柏科学院

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

预览图

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

总流程

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

控制流程

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

  • 入口脚本:主要实现 PHPServer 的启动、中止、重载功能,即触发 Master 进程startstopreload流程;
  • Master 进程:负责建立并监控 Worker 进程。在启动阶段,会注册信号处理器,而后建立 Worker;在运行阶段,会持续监控 Worker 进程健康状态,并接受来自入口脚本的控制信号并做出响应;在中止阶段,会中止掉全部 Worker 进程;
  • Worker 进程:负责执行业务逻辑。在被 Master 进程建立后,就处于持续运行阶段,会监听到来自 Master 进程的信号,以实现自个人中止;

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

  • 流程 ① :以守护态启动 PHPServer 时的主要流程。入口脚本会进行 daemonize,也就是实现进程的守护态,此时会fork出一个 Master 进程;Master 进程先通过 保存 PID注册信号处理器 操做,而后 建立 Workerfork出多个 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一个子进程,而后该进程退出,并设置新的子进程为会话组长,此时的这个子进程就会脱离当前终端的控制。以下图所示:spa

守护进程流程

这里使用了 2 次fork,因此最后fork的一个子进程才是 Master 进程,其实一次fork也是能够的。代码以下:code

protected static function daemonize()
{
    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 进程已经脱离了终端控制,因此有必要关闭标准输出和标准错误输出。以下:orm

protected static function resetStdFd()
{
    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文件。代码以下:server

protected static function saveMasterPid()
{
    // 保存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";
}

注册信号处理器

由于守护进程一旦脱离了终端控制,就犹如一匹脱缰的野马,任由其奔腾可能会随心所欲,因此咱们须要去驯服它。

这里使用信号来实现进程间通讯并控制进程的行为,注册信号处理器以下:

protected static function installSignal()
{
    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);
}

protected static function signalHandler($signal)
{
    switch($signal) {
        case SIGINT:
        case SIGTERM:
            static::stop();
            break;
        case SIGQUIT:
        case SIGUSR1:
            static::reload();
            break;
        default: break;
    }
}

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

建立多进程Worker

Master 进程经过fork系统调用,就能建立多个 Worker 进程。实现代码,以下:

protected static function forkOneWorker()
{
    $pid = pcntl_fork();

    // 父进程
    if ($pid > 0) {
        static::$_workers[] = $pid;
    } else if ($pid === 0) { // 子进程
        static::setProcessTitle('PHPServer: worker');

        // 子进程会阻塞在这里
        static::run();

        // 子进程退出
        exit(0);
    } else {
        throw new \Exception("fork one worker fail");
    }
}

protected static function forkWorkers()
{
    while(count(static::$_workers) < static::$workerCount) {
        static::forkOneWorker();
    }
}

Worker进程的持续运行

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

Worker进程的持续运行

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

public static function run()
{
    // 模拟调度,实际用event实现
    while (1) {
        // 捕获信号
        pcntl_signal_dispatch();

        call_user_func(function() {
            // do something
            usleep(200);
        });
    }
}

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

Master进程的持续监控

调度流程

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

Master持续监控流程

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

protected static function monitor()
{
    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 进程。健康检查流程,以下图:

健康检查流程

代码实现,以下:

protected static function checkWorkerAlive()
{
    $allWorkerPid = static::getAllWorkerPid();
    foreach ($allWorkerPid as $index => $pid) {
        if (!static::isAlive($pid)) {
            unset(static::$_workers[$index]);
        }
    }

    static::forkWorkers();
}

中止

Master 进程的持续监控,见 流程 ④ 。其详细流程,以下图:

中止流程

入口脚本给 Master 进程发送 SIGINT 信号,Master 进程捕获到该信号并执行 信号处理器,调用stop()方法。以下:

protected static function stop()
{
    // 主进程给全部子进程发送退出信号
    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 进程也会执行该方法,并会直接退出。

protected static function stopAllWorkers()
{
    $allWorkerPid = static::getAllWorkerPid();
    foreach ($allWorkerPid as $workerPid) {
        posix_kill($workerPid, SIGINT);
    }

    // 子进程退出异常,强制kill
    usleep(1000);
    if (static::isAlive($allWorkerPid)) {
        foreach ($allWorkerPid as $workerPid) {
            static::forceKill($workerPid);
        }
    }

    // 清空worker实例
    static::$_workers = array();
}

重载

代码发布后,每每都须要进行从新加载。其实,重载过程只须要重启全部 Worker 进程便可。流程以下图:

重载流程

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

protected static function reload()
{
    // 中止全部worker便可,master会自动fork新worker
    static::stopAllWorkers();
}
reload()方法只会在 Master 进程中执行,由于 SIGQUIT 和 SIGUSR1 信号不会发送给 Worker 进程。

你可能会纳闷,为何咱们须要重启全部的 Worker 进程,而这里只是中止了全部的 Worker 进程?这是由于,在 Worker 进程终止退出后,因为 Master 进程对 Worker 进程的健康检查 做用,会自动从新建立全部 Worker 进程。

运行效果

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

$ php server.php 
Usage: Commands [mode] 

Commands:
start        Start worker.
stop        Stop worker.
reload        Reload codes.

Options:
-d        to start in DAEMON mode.

Use "--help" for more information about a command.

首先,咱们启动它:

$ php server.php start -d
PHPServer start      [OK]

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

$ pstree -p
init(1)-+-init(3)---bash(4)
        |-php(1286)-+-php(1287)
                    `-php(1288)

最后,咱们把它中止:

$ php server.php stop
PHPServer stopping ...
PHPServer stop success

如今,你是否是感受进程控制其实很简单,并无咱们想象的那么复杂。( ̄┰ ̄*)

总结

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

首先,咱们不该该给 Master 进程分配繁重的任务,它更适合作一些相似于调度和管理性质的工做;
其次,可使用 Supervisor 等工具来管理咱们的程序,当 Master 进程异常崩溃时,能够再次尝试被拉起,避免 Master 进程异常退出的状况发生。

相关文章 »

相关文章
相关标签/搜索