专栏首页可能是东半球最正规的API社区带着老李折腾山寨Workerman(四)

带着老李折腾山寨Workerman(四)

各位佬们腿子们好,我是老李。

不知不觉已经干到第四章了,三条消息,一好一坏一中性:

  • 好消息是进程部分的基础内容再写一个章节就终于能结束了
  • 坏消息是下下一章节我们要分析WM有关进程的部分实现
  • 然后是后面准备进入socket部分了,开不开心,刺不刺激

昨天晚上做梦梦到了栋子,就想起我俩那会儿一起摸鱼的时光。那还是五年前在[ 黑 ]鹭引擎的时候,我俩被人称为公司两大门神,具体表现在于基本一整个白天都在公司门口歇着摸鱼,就坐楼下的石凳上一边一个,各摸各的十分对称十分默契。

当然了我俩都认为各有各摸鱼的道理,比如栋子总是觉得自己的UI设计风格属于写实主义而不被公司重用,而我则是认为这是因为我尝试在公司推swoole推不动而觉得怀才不遇。相比之下由于我总是能在适当时候用类似于[ 行路难!行路难!多歧路,今安在? ]或[ 念天地之悠悠,独怆然而涕下 ]等多种不同的诗句来花式地表达自己,而栋子则因为长期的文化匮乏每次只能狠嘬一口白沙后恶狠狠地说同一句话[ 尚能饭否!尚能饭否!]...

终于有一天栋子似乎良心发现了,那天我一下去就觉得他有话要说,果不其然(由于事隔已久远,具体已经记不太清了,大概意思如下)。栋子闷了一口白沙后,漠然抬头用散乱的眼神看了一眼前方,然后收了收嗓子很严肃认真地跟我说:

  • ... ...老李,我现在严重怀疑我被公司雪藏了......
  • 我:??????啥意思???
  • 我:... ...或者说是有什么具体表现吗?...
  • 栋子看了我一眼,本来想嘬一口香烟又不自觉挪开了嘴巴,顿了顿神后偷偷摸摸看了看四周,然后神神叨叨地低声跟我说:你说这都半年多了,公司啥活儿也不给我安排... ...
  • 我:??????... ...
  • 我:...扣你工资了?... ...
  • 栋子:没有啊,老样子按时发啊...
  • 我:。。。。。。
  • 我:... 我说铁子,真TM新鲜了,MD我这是头次见有人把骗工资说的这么清新脱俗... ...

五年过去了,人已经回不到过去了,时代也回不去了...

记得后来没多久,领导让我研究一个爬虫脚本,当时为了不让脚本莫名其妙退出就天天看着电脑不关机,再后来就用Linux命令后加一个[ & ]符来跑...莫名其妙挂了几次后,我决定彻底研究一下[ 如何使程序在后台保持稳定 ]这个话题,当然了这也是我们今天的话题。

如何才能使程序溜到后台里?我先说个著名的[ & ]符,感受下:

<?php
while ( true ) { 
  file_put_contents( './daemon.log', time().PHP_EOL, FILE_APPEND );
  sleep( 1 );
}

上面代码保存成daemon.php,然后用下面命令就能放到后台工作:

php daemon.php &

其中的[ 1 ]表示[ 后台 ]任务的序号,daemon.php就是第一号任务,而20041就是其进程PID。如果我们想看下这种[ 后台 ]任务的列表,要在当前终端窗口输入jobs命令,注意是只能在当前这个终端窗口。

如果想要将这些[ 后台 ]任务从后台捞出来,需要用fg + [ 序号 ]方式给捞出来:

此处需要注意的是当任务被捞出来后,使用Ctrl+Z命令会将任务[ 放入后台并暂停 ],暂停是表示代码不再运行了但是进程尚在,你们可以通过tail -f daemon.log文件来观察。如果想让[ 后台 ]任务再次运行起来,需要用bg + [ 序号 ]来恢复后台任务运行,如上图中所示。

然而这种做法有可能出现的情况是:如果关闭当前终端,该进程也有可能会被关闭。只不过我在Mac下和Ubuntu 16.04.1下试了一把,都没能复现出来,诸位佬们知道详细缘由的可以公众号发消息告知下。我们现在依然假设这个结论成立,所以为了保证这种[ 后台 ]进程不会跟随终端关闭而关闭,就有了nohup命令,他的用法非常简单:

其实当我们平时关闭一个终端窗口时,会收到一种叫做SIGHUP的信号,一些进程在收到SIGHUP信号后就会终止退出,而nohup则是顾名思义了:就是忽略SIGHUP信号。

所以,无论是末尾加上&符号亦或是头部加上nohup,并非靠谱或最佳方案。我曾经见过不少nohup后几天后莫名其妙进程丢失的案例,比如这位...

所以我们需要一种正规而又稳定化的进程后台方法。这会儿我又不得不说下当年去HomeLink基础平台部面试时候的一道题目了:当你在终端里输入一个命令按下回车后发生了什么事情。

当然了,众所周知(其实大概就四五个人知道)我回答的并不好。主要是我并不知道这道题是具体想问什么,从马后炮的角度看来我应该把进程组、会话组这些概念说明白就好了。

本质上终端bash也是一个进程,所以实际上在终端bash里输入一个命令后,比如php daemon.php后敲回车,应该就是bash进程fork出了子进程,该子进程中去执行php daemon.php。所以下面的代码保存成daemon.php后在终端里执行,我们可以得到如下的进程树关系:

<?php
$pid = pcntl_fork();
if ( 0 == $pid ) { 
  $ppid = pcntl_fork();
  if ( 0 == $ppid ) { 
    while ( true ) { 
      sleep( 1 );  
    }   
  }
  while ( true ) { 
    sleep( 1 );  
  }
}
while ( true ) { 
  sleep( 1 );  
}

解释一下上图进程树,可以看到bash的进程ID为32614,TA fork出来2095 PID并执行了php daemon.php,而后2095 PID又fork出来2096 PID,最后2096 PID又fork出来2097 PID。

这里我们要引入进程组、会话组的概念了:

  • 进程组:一坨相关的进程会抱团组成一个进程组,每个进程组有一个组长,进程组ID等于组长进程的PID;只有当进程组里没有一个活着的进程了,这个进程组就算彻底完犊子了,否则只要有任何一个进程在,进程组都不算是死绝了。比如上面上图中,PID 32614的bash进程自己就是一个进程组,而php daemon.php的三个2095、2096、2097三个进程组成了另外一个进程组
  • 会话组:一坨相关的进程组抱团形成一个会话组,每个会话组有一个组长。比如上述案例中,bash所在进程组和php daemon.php两个不同的进程组则隶属于同一个会话组。每个会话组都有一个会话首进程。关于会话组的重点难点,在这里,下面这些用红线圈住,考试要考的: 一、使用setsid()函数可以创建一个新的会话组 二、组长进程(此处你可以暂时认为是父进程)无法调用setsid,会报错 三、非组长进程(此处你可以粗暴认为是子进程)可调用setsid创建出一个新的会话组,这个行为会导致[ 该进程会创建一个新的进程组且自身为该进程组组长,该进程会创建出一个新的会话组且自身为该会话组组长,该进程会脱离当前命令行控制终端 ]

大家可以利用[ ps -eo pid,ppid,pgid,sid,command | grep 关键字 ]来获取进程PID、PPID、组ID、会话ID等,我们简单演示一下,代码依然是上面的代码:

此处需仔细对照终端研究一下即可。

上面普及铺垫完了,就可以正式步入正轨了,是时候表演真正的技术了!在*NIX里,后台进程有个标准说法叫做daemon进程,标准翻译叫做守护进程。平日里Redis、Nginx等启动完毕后,都会以守护进程方式跑在系统后台提供服务。包括我们正在山寨的对象Workerman在启动后都是以守护进程方式跑在系统后台,稳稳地提供服务,那么如何利用PHP实现daemon?

<?php
$i_pid = pcntl_fork();
// 在子进程中...
if ( 0 == $i_pid ) {
  // setsid创建新会话组 
  if ( posix_setsid() < 0 ) {
    exit();
  }
  // 在子进程中二次fork(),这里据说是为了避免SVR4种一次fork有时候无法脱离控制终端
  //$i_pid = pcntl_fork();
  //if ( $i_pid > 0  ) {
    //exit;
  //}

  // 守护进程的业务逻辑从这里开始
  // while使得进程不会退出,一般http服务器等都是event-loop不会退出
  while ( true ) {
    sleep( 1 );
  }

}
// 父进程退出
else if ( $i_pid > 0 ) {
  exit();
}

上述代码中我注释了一行关于二次fork的代码,这段代码你可以用,也可以不用,注释里我写了一下原因。上述代码保存好运行一下,然后我们用ps命令感受一下:

此时daemon.php在调用了setsid后自己新建了一个进程组且自己为组长进程、自己新建了一个会话组且自己为会话组长、自己脱离了控制终端且由于父进程已经exit退出所以由1号进程即init进程收养。为了说明我们的daemon进程是完美的、是和大厂出品一样牛13的,我特意整了一把Redis,你们对比感受一下。

现在好了,我们有了daemon的标准做法,就可以尝试做一些东西了,我简单举个例子你们感受一下:

<?php
function daemonize() {
  $i_pid = pcntl_fork();
  // 在子进程中...
  if ( 0 == $i_pid ) {
    // setsid创建新会话组 
    if ( posix_setsid() < 0 ) {
      exit();
    }
    // 在子进程中二次fork(),这里据说是为了避免SVR4种一次fork有时候无法脱离控制终端
    $i_pid = pcntl_fork();
    if ( $i_pid > 0  ) {
      exit;
    }
    //echo "here".PHP_EOL;
  }
  // 父进程退出
  else if ( $i_pid > 0 ) {
    exit();
  }
}
// 首先执行daemonize函数,使得进程daemon化
daemonize();
// 连接redis,在后台做一些事情
$o_redis = new Redis();
$o_redis->connect( '127.0.0.1', 6379 );
while ( true ) {
echo $o_redis->get( 'user:1' ).PHP_EOL;
  sleep( 1 );
}

上面代码运行后,你一定会发现一个问题:那就是当前终端会不会打印出空行。这个嘛,哈,这个是因为我们没有重定向标准输出到文件中导致的,所以上述的daemonize函数实际上并不完善,只是完成了最重要的功能。一个较为完善的daemonize函数,应该具备如下要点:

  • 设置好umask
  • 将目录切换到根目录,避免默认工作目录被daemon进程占据无法卸载
  • 关闭标准输出等或将其重定向到指定地方

所以一个稍微完善点儿的daemonize应该是这样的,你们赶紧复制粘贴走试试:

<?php
function daemonize() {
  // 设置权限掩码,umask大家可以搜一下
  umask( 0 );
  // 将目录更换到指定某个目录,一般是根目录
  // 如果不更换,存在一种问题就是:daemon进程默认目录无法被卸载unmount
  chdir( '/' );
  $i_pid = pcntl_fork();
  // 在子进程中...
  if ( 0 == $i_pid ) {
    // setsid创建新会话组 
    if ( posix_setsid() < 0 ) {
      exit();
    }
    // 在子进程中二次fork(),这里据说是为了避免SVR4种一次fork有时候无法脱离控制终端
    $i_pid = pcntl_fork();
    if ( $i_pid > 0  ) {
      exit;
    }
    // 关闭 标准输入
    // 这里仅仅是关闭,你可以根据你的需要重定向到其他位置,比如某些文件
    fclose( STDOUT );
  }
  // 父进程退出
  else if ( $i_pid > 0 ) {
    exit();
  }
}
// 首先执行daemonize函数,使得进程daemon化
daemonize();
// 连接redis,在后台做一些事情
$o_redis = new Redis();
$o_redis->connect( '127.0.0.1', 6379 );
while ( true ) {
  echo $o_redis->get( 'user:1' ).PHP_EOL;
  sleep( 1 );
}

本文分享自微信公众号 - 高性能API社区(high-performance-api),作者:老李秀

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-12-02

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 持续搞【附近】系列---听说MongoDB是专业的(三)

    一直听说MongoDB才是【专业】搞地理空间查询的,人家才是【专业】的!相当长一段时间来,一说搞【附近】就会相当一批人的脑海里就不自主浮想到MongoDB......

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

    在经过了一个如沐春风、令人神清气爽而又愉悦的工作周后(具体发生了什么你们心里应该有数),总算可以回到以往周六日的节奏了。实际上对于我来说,没有严格意义上的周六日...

    老李秀
  • 恒久学习【附近的人】---老赵大战Apache Thrift入门篇(九)

    想当初在积目的时候,服务端四个人都已经懒到家了。老李压根就指挥不动张大彪、柱子,甚至连一向听话的二营长都指挥不动了,最可怕的是老李连老李自己都指挥不动了。事情已...

    老李秀
  • Centos+PHP+Nginx+Laravel搭建服务

    无忧366
  • LAMP平台的搭建及应用

    L宝宝聊IT
  • CentOS6.7搭建LNMP环境

    3.配置CentOS 6.7 第三方yum源(CentOS默认的标准源里没有nginx软件包)

    流柯
  • Metinfo6.0.0-6.1.3多个CVE漏洞复现

    2018年12月27日,Metinfo被爆出存在存储型跨站脚本漏洞,远程攻击者无需登录可插入恶意代码,管理员在后台管理时即可触发。该XSS漏洞引起的原因是变量覆...

    墙角睡大觉
  • 绕过WAF限制利用php:方法实现OOB-XXE漏洞利用

    几个星期以前,作者在某个OOB-XXE漏洞测试中遇到过这样一种场景:目标应用后端系统WAF防火墙阻挡了包含DNS解析在内的所有出站请求(Outgoing Req...

    FB客服
  • Laravel 请求生命周期

    当需要使用一个框架、工具或者服务时,在使用前应对其运行原理进行研究。随着原理研究工作的不断深入,能让我们在使用时更得心应手。

    柳公子
  • Nginx防盗链+Nginx访问控制+Nginx解析php相关配置+Nginx 代理

    老七Linux

扫码关注云+社区

领取腾讯云代金券