各位佬们腿子们好,我是老李。
不知不觉已经干到第四章了,三条消息,一好一坏一中性:
昨天晚上做梦梦到了栋子,就想起我俩那会儿一起摸鱼的时光。那还是五年前在[ 黑 ]鹭引擎的时候,我俩被人称为公司两大门神,具体表现在于基本一整个白天都在公司门口歇着摸鱼,就坐楼下的石凳上一边一个,各摸各的十分对称十分默契。
当然了我俩都认为各有各摸鱼的道理,比如栋子总是觉得自己的UI设计风格属于写实主义而不被公司重用,而我则是认为这是因为我尝试在公司推swoole推不动而觉得怀才不遇。相比之下由于我总是能在适当时候用类似于[ 行路难!行路难!多歧路,今安在? ]或[ 念天地之悠悠,独怆然而涕下 ]等多种不同的诗句来花式地表达自己,而栋子则因为长期的文化匮乏每次只能狠嘬一口白沙后恶狠狠地说同一句话[ 尚能饭否!尚能饭否!]...
终于有一天栋子似乎良心发现了,那天我一下去就觉得他有话要说,果不其然(由于事隔已久远,具体已经记不太清了,大概意思如下)。栋子闷了一口白沙后,漠然抬头用散乱的眼神看了一眼前方,然后收了收嗓子很严肃认真地跟我说:
五年过去了,人已经回不到过去了,时代也回不去了...
记得后来没多久,领导让我研究一个爬虫脚本,当时为了不让脚本莫名其妙退出就天天看着电脑不关机,再后来就用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。
这里我们要引入进程组、会话组的概念了:
大家可以利用[ 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函数,应该具备如下要点:
所以一个稍微完善点儿的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 );
}