前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >这次让我们真的读一下Workerman源码(六)

这次让我们真的读一下Workerman源码(六)

作者头像
老李秀
发布2019-12-26 15:03:25
1.6K0
发布2019-12-26 15:03:25
举报
文章被收录于专栏:可能是东半球最正规的API社区

各位佬们、腿子们好,我是老李。今天我就少讲段子和番外,以最快速度直捣黄龙了。

在经过了一个如沐春风、令人神清气爽而又愉悦的工作周后(具体发生了什么你们心里应该有数),总算可以回到以往周六日的节奏了。实际上对于我来说,没有严格意义上的周六日,一直在做事情,只不过所做事情的贡献对象不同而已。WM我已经叨叨了五个章节了,今天我想聊聊关于Workerman进程管理部分的相关源码,如果前五个章节你们都已经仔细研究过了,那么现在阅读Workerman进程管理部分的源码应该会是易如反掌了。

但是按照我一贯的秉性,光阅读分析一下人家的肯定是不够的,事后肯定要自己动手写一下的。不过我这个人吧,有个很大的缺陷,就是非常不擅长[ 写关于分析别人源码的文章 ]。源码分析实际上最适合自己在有了基础后找个安静的时刻配合上一个大屏幕,自己去静静地品,一会儿就有[ 内味儿了 ]。

所以,今天大概就两个任务咯:

  • 感受一下Workerman进程管理相关代码
  • 自己完成山寨WM进程管理相关的代码

请你准备好Workerman最新master分支,Let's ROCK~

先了解下Workerman的进程模型,这是一种单Master多Worker的进程模型,在这其中:

  • Master进程fork出固定数量的Worker进程,并在服务运行后负责监控Worker进程的状态,比如Worker挂了后再重新拉起一个,又或者收到信号后reload全部Worker进程
  • Worker进程的职责就相对进程,每个Worker进程持有一个event-loop,负责监听各种网络事件,只不过event-loop内容并非本篇所覆盖的范围了

这里的工作流程就是一初始化配置,二daemonize化,三Master进程fork出固定数量的Worker进程,四给Master进程以及Worker进程安装信号处理器。好了,我们就根据这四点对代码进行重点感受。

先从入口函数看起,我们就重点关注与进程相关的几个咯,我分别在上面加了注释:

代码语言:javascript
复制
    public static function runAll()
    {    
        static::checkSapiEnv();
        static::init();
        static::lock();
        // 对start、stop、restart、reload动作的解析
        // 这个函数我就不单独解析了,比较简单
        static::parseCommand();
        // 这个没什么好说的,就是daemon化
        static::daemonize();
        static::initWorkers();
        // 安装信号
        static::installSignal();
        static::saveMasterPid();
        static::unlock();
        static::displayUI();
        // fork出配置数量的Worker进程
        static::forkWorkers();
        static::resetStd();
        // 监控Worker进程
        static::monitorWorkers();
    }

好了,我们看下static::daemonize()函数,在前面章节里我们是单独解析过daemon进程的底层原理以及其实现的,现如今看下WM的,看看是不是觉得已经更加容易接受理解了:

代码语言:javascript
复制
    protected static function daemonize() {
        // 如果配置中daemonize为false 或者 操作系统不是Linux
        // 那么直接返回
        if (!static::$daemonize || static::$_OS !== \OS_TYPE_LINUX) {
            return;
        }
        // 设置umask掩码,umask与chmod权限息息相关
        \umask(0);
        // fork进程,主进程退出执行,子进程继续执行
        // 注意这里的子进程不是指Worker进程
        // 尽管是子进程,但他却依然是Master进程
        $pid = \pcntl_fork();
        if (-1 === $pid) {
            throw new Exception('Fork fail');
        } elseif ($pid > 0) {
            exit(0);
        }
        // 子进程(Master进程)使用posix_setsid()创建新会话和进程组
        // 这一句话便足以让当前进程脱离控制终端!
        if (-1 === \posix_setsid()) {
            throw new Exception("Setsid fail");
        }
        // 下面这句英文注释是 亮哥 写的,大概意思之前也说过,就是
        // 避免SVR4某些情况下情况下进程会再次获得控制终端
        // Fork again avoid SVR4 system regain the control of terminal.
        $pid = \pcntl_fork();
        // 主进程再次终止运行,最终的子进程会成为Master进程变成
        // daemon程序运行在后台,然后继续fork出Worker进程
        if (-1 === $pid) {
            throw new Exception("Fork fail");
        } elseif (0 !== $pid) {
            exit(0);
        }
    }

然后是安装信号处理,大概是这样的:

(突发事件插播:TMD,现在是凌晨2点[ 写完的时候已经凌晨五点了 ],我本来睡地刚刚好,突然听到外面好像有人在唱歌,关掉还在播放着的我桃儿正在讲着的关于谦儿哥爸爸大肠刺身的事儿,这才听清楚原来是一男一女在楼下的草地里不知道干啥,女的好像在哭,男的好像在吐,这TM大半夜的,一个哭一个吐,我是被折腾地睡不着了,索性起来写文章了... ...我楼层低,吐的时候那哗啦哗啦地声音好像就跟在我屋里吐似的。这不仅让我想起刚来北京那会儿住在宋家庄贫民窟的日子里,当时我房租300块而且水电费还不用管,房子就是三合板搞定的,自带空调效果,温度随外界变化而变化。住在我隔壁的是一情侣,他们还养了一条狗子。每天晚上老子回来的时候一推门进去,TA们家的那傻狗就以为是有人要进TA们屋了,然后就是一顿顿哇哇狂吠;这还不是最沙雕的,最沙雕的是TA俩办事儿的时候,由于隔音以及隔震动的效果都贼差,我总有一种TA俩就在我床上办事儿的错觉,每当我忍不住笑的时候,TA俩就停了,我估计TA也有我就在TA们床上笑的错觉,然后停个几十秒后就悉悉嗦嗦又开始了...)

代码语言:javascript
复制
    protected static function installSignal() {
        // 如果不是Linux就GG吧
        if (static::$_OS !== \OS_TYPE_LINUX) {
            return;
        }
        // signalHandler便是具体的信号处理函数
        $signalHandler = '\Workerman\Worker::signalHandler';
        // stop
        // 捕获SIGINT信号,实现stop命令
        \pcntl_signal(\SIGINT, $signalHandler, false);
        // graceful stop
        // 捕获SIGTERM信号,实现柔性停止
        \pcntl_signal(\SIGTERM, $signalHandler, false);
        // reload
        // 捕获SIGUSR1,实现reload
        \pcntl_signal(\SIGUSR1, $signalHandler, false);
        // graceful reload
        // 捕获SIGQUIT信号,实现柔性加载
        \pcntl_signal(\SIGQUIT, $signalHandler, false);
        // status
        // 捕获SIGUSR2信号,实现status
        \pcntl_signal(\SIGUSR2, $signalHandler, false);
        // connection status
        // SIGIO信号,不属于本节内容,后面会继续提
        \pcntl_signal(\SIGIO, $signalHandler, false);
        // ignore
        // 捕获SIGPIPE信号,忽略掉所有管道事件
        \pcntl_signal(\SIGPIPE, \SIG_IGN, false);
    }

    // 然后我们需要再继续关注下signalHandler函数,这个函数里是对各个
    // 信号响应的具体业务方法
    public static function signalHandler($signal) {
        switch ($signal) {
            // Stop.
            // 实际上case SIGINT和case SIGTERM的逻辑非常简单,
            // 唯一区别就是\$_gracefulStop参数
            // 所以重点只需要放在stopAll()函数上了
            // 这个函数实现了Workerman停止的逻辑
            case \SIGINT:
                static::$_gracefulStop = false;
                static::stopAll();
                break;
            // Graceful stop.
            case \SIGTERM:
                static::$_gracefulStop = true;
                static::stopAll();
                break;
            // Reload.
            // 下面也就简单了,重点放在reload()函数上即可
            // 这个函数实现了对所有Worker进程reload热加载
            // 而在reload函数之前,首先需要使用getAllWorkerPids()
            // 获取到所有待reload的Worker进程的pid们
            case \SIGQUIT:
            case \SIGUSR1:
                if($signal === \SIGQUIT){
                    static::$_gracefulStop = true;
                }else{
                    static::$_gracefulStop = false;
                }
                static::$_pidsToRestart = static::getAllWorkerPids();
                static::reload();
                break;
            // Show status.
            case \SIGUSR2:
                static::writeStatisticsToStatusFile();
                break;
            // Show connection status.
            case \SIGIO:
                static::writeConnectionsStatisticsToStatusFile();
                break;
        }
    }

好了,让我们重点看stop和reload大概是如何实现的,我们约定顺序为先看stop后看reload:

代码语言:javascript
复制
    public static function stopAll() {
        // 首先将\$_status成员属性设置为shutdown状态
        // 估计是这个成员属性在其他地方必须要被用到
        static::$_status = static::STATUS_SHUTDOWN;
        // For master process.
        // 重点开始来了,stop指令针对Master进程和Worker进程处理
        // 是分开的
        // 如果当前进程是Master进程
        if (static::$_masterPid === \posix_getpid()) {
            // 使用log函数在log文件里打一行日志...
            static::log("Workerman[" . \basename(static::$_startFile) . "] stopping ...");
            // 获取到当前Master进程的所有Worker进程的pid们
            $worker_pid_array = static::getAllWorkerPids();
            // Send stop signal to all child processes.
            // 是否需要 柔性停止,区别是非常简单的,我相信大家都知道啥意思
            if (static::$_gracefulStop) {
                $sig = \SIGTERM;
            } else {
                $sig = \SIGINT;
            }
            // 循环遍历Worker进程的pid数组,然后使用posix_kill向
            // 每一个子进程发送SIGTERM信号或者SIGINT信号
            foreach ($worker_pid_array as $worker_pid) {
                \posix_kill($worker_pid, $sig);
                // 既然上面已经使用posix_kill向worker发送过终止指令了
                // 那么?为什么又有定时器向worker进程发送SIGKILL呢?
                // 我感觉这里就是为了保证worker进程一定要被干死
                // SIGKILL信号是非常终极的,这个信号不能被忽略也不能
                // 被捕捉,是必须100%要响应的,进程收到必死!
                if(!static::$_gracefulStop){
                    Timer::add(static::KILL_WORKER_TIMER_TIME, '\posix_kill', array($worker_pid, \SIGKILL), false);
                }
            }
            // 1秒钟后检测进程是否还活着,如果确定都挂了
            // 将进程id从保存的数组中unset掉
            // 这个函数见下方
            Timer::add(1, "\\Workerman\\Worker::checkIfChildRunning");
            // Remove statistics file.
            // 删除掉所有statistics文件,workerman会在后台运行期间将一些数据
            // 记录到statistics文件中去,当我们需要在服务运行期间查看一些数据
            // 的时候,一些数据就是从这个文件获取到的
            // 随着服务stop,这些文件也要删除掉了
            if (\is_file(static::$_statisticsFile)) {
                @\unlink(static::$_statisticsFile);
            }
        } // For child processes.
        else {
            // Execute exit.
            // 这里说下static::$_workers保存的是啥哈,大家知道在用Workerman
            // 的时候,需要有这么一句:
            // $http_worker = new Worker("http://0.0.0.0:2345");
            // $ws_worker = new Worker("websocket://0.0.0.0:2000");
            // 每次new Worker时候,会生成一个独一无二的worker_id
            // static::$_workers中保存的就是如下结构:
            // static::$_workers[独一无二worker-id] = 当前对象
            // 比如你一次同时启动了一个http服务和一个websocket服务
            // 那么就会保存为:
            // static::$_workers[http-worker-id] = $this
            // static::$_workers[websocket-worker-id] = $this
            // ⚠️⚠️⚠️⚠️⚠️⚠️⚠️此Worker实例不是指worker进程⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️
            foreach (static::$_workers as $worker) {
                // 如果当前worker实例不处于停止,那么就执行stop方法
                // 因为如果一个worker实例如果设置了多个子进程,每个
                // 子进程收到stop后都会停止当前worker实例,所以这里
                // 要判断下是否已经停止了,如果已经停止了,其余子进程
                // 就不需要再停止当前Worker实例了
                // 注意这里的stop,仅仅为了触发onWorkerStop回调
                // 并不是exit进程,千万注意
                if(!$worker->stopping){
                    // 所以我们关注重点需要集中到stop()函数方法上去了
                    $worker->stop();
                    $worker->stopping = true;
                }
            }
            // 前面说过了,每个Worker进程会持有一个event-loop
            // 所以要关闭Worker进程,下面就是把关于网络链接的
            // 一些乱七八糟的玩意给关掉
            if (!static::$_gracefulStop || ConnectionInterface::$statistics['connection_count'] <= 0) {
                static::$_workers = array();
                if (static::$globalEvent) {
                    static::$globalEvent->destroy();
                }
                // 到这里,才算是真正地退出子进程们...
                exit(0);
            }
        }
    }

    // 这个函数没啥好看的,他这里使用了一个很常见的技巧
    // 就是如何得知一个进程是死是活呢?
    // 技巧就是向这个进程发送值为0的信号,实际上并不存在数值
    // 为0的信号,这只是一个常见技巧,在APUE里作者提到过的
    public static function checkIfChildRunning() {
        foreach (static::$_pidMap as $worker_id => $worker_pid_array) {
            foreach ($worker_pid_array as $pid => $worker_pid) {
                // 使用0号信号进行进程探活
                if (!\posix_kill($pid, 0)) {
                    unset(static::$_pidMap[$worker_id][$pid]);
                }
            }
        }
    }
    
    // 关闭Worker实例
    public function stop() {
        // Try to emit onWorkerStop callback.
        // 会触发onWorkerStop事件~~~~
        if ($this->onWorkerStop) {
            try {
                // 这里有一个小技巧就是使用call_user_func实现PHP
                // 中的on回调~~~
                \call_user_func($this->onWorkerStop, $this);
            } catch (\Exception $e) {
                static::log($e);
                exit(250);
            } catch (\Error $e) {
                static::log($e);
                exit(250);
            }
        }
        // 下面这些乱七八糟的我们就暂时不关注。。
        // 后面到网络章节会讲到的
        // Remove listener for server socket.
        $this->unlisten();
        // Close all connections for the worker.
        if (!static::$_gracefulStop) {
            foreach ($this->connections as $connection) {
                $connection->close();
            }
        }
        // Clear callback.
        $this->onMessage = $this->onClose = $this->onError = $this->onBufferDrain = $this->onBufferFull = null;
    }

上述就是php index.php stop后的逻辑,估计会有老哥会问平滑停止的平滑体现在哪儿了呢?在Workerman里,亮哥使用SIGINT实现粗暴stop,使用SIGTERM实现优雅stop。SIGINT是键盘上的Ctrl+C组合键产生的。这两个信号的默认动作都是直接终止程序,注意是默认动作,什么叫默认动作:就是你不额外安装信号处理捕捉这两个信号,一旦你捕捉了两个信号,实际上他们都会让你程序中代码运行完毕,然后再去关闭程序。

是不是有点儿绕?好,看下下面这个demo,你们自己复制粘贴走运行一下:

代码语言:javascript
复制
<?php
echo posix_getpid().PHP_EOL;
pcntl_async_signals( true );
// 给进程安装信号...
pcntl_signal( SIGTERM, function() {
  for( $i = 1; $i <= 10; $i++ ){
    echo $i.PHP_EOL;
    sleep( 1 );
  }
  exit; 
} );
pcntl_signal( SIGINT, function() {
   for( $i = 1; $i <= 10; $i++ ){
    echo $i.PHP_EOL;
    sleep( 1 );
  }
  exit; 
} );
// while保持进程不要退出..
while ( true ) { 
  sleep( 1 );
}

当你运行起来程序后,按下Ctrl+C键后,你看看啥反应。

然后再次运行程序,打开另外一个终端,kill -15 PID,你看看会是啥反应。

是的,他们都是让那个15秒钟的for循环跑完后才会终止程序的。不过在Workerman里,SIGINT粗暴结束的时候,就有很多粗暴地终止当前链接的代码,比如这里:

代码语言:javascript
复制
        if (!static::$_gracefulStop) {
            foreach ($this->connections as $connection) {
                $connection->close();
            }
        }

而如果你用SIGTERM优雅停止服务的时候,就不会直接粗暴的掐断当前连接。综上,我认为这就是Workerman里[ stop ]和[ stop -g ]的不同的具体体现。

行吧,看在已经凌晨五点的份上,reload、fork和monitor以及动手山寨的部分就到下一篇吧,你们自己好好慢慢消化一下上面的代码。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2019-12-16,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 高性能API社区 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档