前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >带着老李折腾山寨Workerman(四)

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

作者头像
老李秀
发布2019-12-11 10:21:32
7730
发布2019-12-11 10:21:32
举报

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

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

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

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

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

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

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

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

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

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

代码语言:javascript
复制
<?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后在终端里执行,我们可以得到如下的进程树关系:

代码语言:javascript
复制
<?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?

代码语言:javascript
复制
<?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的标准做法,就可以尝试做一些东西了,我简单举个例子你们感受一下:

代码语言:javascript
复制
<?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应该是这样的,你们赶紧复制粘贴走试试:

代码语言:javascript
复制
<?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 );
}
本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-12-02,如有侵权请联系 cloudcommunity@tencent.com 删除

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

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

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

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
云数据库 Redis
腾讯云数据库 Redis(TencentDB for Redis)是腾讯云打造的兼容 Redis 协议的缓存和存储服务。丰富的数据结构能帮助您完成不同类型的业务场景开发。支持主从热备,提供自动容灾切换、数据备份、故障迁移、实例监控、在线扩容、数据回档等全套的数据库服务。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档