20130713

如何永生

如何永生

本文介绍通过 守护进程 或 at提交作业
监控某个进程保持运行,本案例中被监控的进程是amule电驴。守护进程方案是李记者提出的,用一个进程看着amule没死,也是李记者提出的。

1. 问题,amule不知道什么时候退出了

李记者,记者二字并不是他的职业,而是名字。这名字是我起的。因为常需要他在故事里扮演角色,而他的本名"粲"字估计很多读者都不太认识,所以就叫李记者。有出租车司机很谨慎地问,"你是记者啊",我哈哈大笑,"他的名字就叫记者。"

李记者来看我,带个黑色的大塑料袋,装垃圾那种,吭哧吭哧扒开,露出里面两大瓶黑可乐。坐定,照例是"你近来一向可好。"

他说,他刚刚休假了一周,天天躺在床上。他说,"我也腰间盘突出了。"有那么一瞬间,我觉得眼泪在眼圈里,腰间盘突出的种种从眼前一闪而过。

我拍腿大笑,哈哈,你也有今天啊,老了吧。然后开始东扯西扯,我们认识的各位。中间准备酒的时候,我看看了机器,抱怨,"电驴不定什么时候就退出去了,真是烦人呐。"

关键就是,你不知道它什么时候死掉了。所以,得隔一会看一眼,看着它干活。可能你忙了一阵,或者开了一天机,回家一看,它死了--而且不知道什么时候死的。

2. 永生之法

李记者说,开个进程,看 (读作一声) 着amule,死了就再启动一次。你才老了呢。

我叹气,看来我确实是老了啊。

这是个标准方案,标准到连病毒和流氓软件都用它。它们一般都用这样的方案保证永生:开几个进程,一个是驱动,一个是应用程序,一个是服务,它们之间互相监视大家的存在,如果发现哪一个挂了,就再把它启动起来。

要求24X7服务的那种应用,一般也需要这样的机制,比如HA (High Ability)
。同时开两台以上的机器,其中一台机器对外提供服务,并向其他的机器发送心跳信号,意思就是我还活着。一旦备用的机器发现主服务器宕掉了,立马启动自己的服务。从用户的角度上看,根本不知道中间还换了个服务生。

这么长时间以来电驴莫名退出困扰着我,我居然没有想到这个标准方案。

3. 守护进程

它的基本机制是轮询,隔一会看看服务还在不在了。像电驴这样的任务,轮询时间2分钟左右就行,反正丢失2分钟也不是很重要。这一点,我和李记者很快达成一致意见。但是,对于使用什么方式轮询,我们各执一端。

他认为,应该开个脚本,作为守护进程,始终在后台。我不同意,觉得这不够酷。李记者说这是标准方案,我说那也白扯,这不够酷。我的意见一会儿再说,他的方案就像下面这样:

$ cat amule_monitor.sh
1 #!/bin/bash
2 amule_status=''
3 while true; do
4 amule_status=$(ps aux | grep -w amule | wc -l)
5 if [ $amule_status = '2' ]; then
6 echo 'amule running.' | tee /var/log/amule_monitor.log
7 else
8 echo 'amule starting.' | tee /var/log/amule_monitor.log
9 amule 2> /dev/null 1> /dev/null &
10 fi
11 sleep 60;
12 done

第1行是指定用哪个程序解释下面的东西。对于熟悉perl,python的同学,这种写法应不陌生。

第2行的 amule_status 是个变量,用来记录 amule 到底是否活着。

第3行 和 第12 之间,是个循环,死循环,没有跳出条件。说起来这是件好玩的事,要想永"生",就得使用"死"循环。

第4行,取得 amule 的状态。状态是这样取得的 $(ps aux | grep -w amule | wc -l),加上$ ()
以后,里面的命令执行的结果就成了个变量。其中,管道分隔的前面两段,执行结果类似于这样:

$ ps aux | grep -w amule
young 5488 14.3 2.4 204256 47632 ? Sl 20:51 2:25 amule
young 5730 0.0 0.0 3324 824 pts/0 S+ 21:08 0:00 grep --color=auto -w amule

其中后一行,是grep本身,前一行,就表明amule正执行。

"wc -l" 是对"ps aux | grep -w amule
"执行的结果统计一下行数。显然,如果一行,那就是amule挂掉了,两行,那就是amule还健在。

在循环之中判断条件,然后有选择地做一些事情--很多算法的核心思想都是这个路子。

现在,我们得到了用于判断的变量 amule_status,接着,我们就要判断了。上面那段代码的判断部分如下:

5 if [ $amule_status = '2' ]; then
6 echo 'amule running.' | tee /var/log/amule_monitor.log
7 else
8 echo 'amule starting.' | tee /var/log/amule_monitor.log
9 amule 2> /dev/null 1> /dev/null &
10 fi
11 sleep 60;

第5行,如果两行的话,那么,第6行,写个日志,说"健在着呢";第7行,否则的话,第8行,日志,"启动一次",然后在第9行启动一次。

其中有些奇怪的符号,都具有特殊的含义。"[];"是条件;"2>"是错误重写向;"1>"是标准输出重定向。fi就是if结束的地方。

第11行,睡60秒。

睡60秒以后呢,就又回到了上面提到的死循环之中。再检查一次amule是否运行着呢,如果没有,启运一次。

这跟你父母看着你学习看书练琴背英语是一个路子:每隔一分钟,露个头,看到你学习着,就说,"啊,我给你拿点水果进来",如果没学习,就咆哮体。这种行为甚是普遍,所以名之为
monitor。也用来称呼班长--替老师监控同学们的人。

当然,小资同学们不会明白,能拥有你自己的屋子,就表明你是生在红旗下长在蜜罐里身在福中不知福。

运行上述程序的方法是:

$ ./amule_monitor.sh &

用"&"代表要求它后台运行,少吱声。然后amule就启运了。因为循环第一圈的时候,发现amule没有启运。然后2分钟之后,日志变成了"amule
running"。不是增加,tee把日志覆盖了。如果增量的话,按李记者的说法,没有时间戳,这样的日志没啥意义,而且,一个多月以后得多少记录啊。这时,我们可以把amule杀了,退出,2分钟以内,monitor就又循环回来,并觉查到amule死了,所以再启动一次amule。

这样,amule就永生了。所以,永生的方法不是永远不被打倒,而是被打倒了以后,马上就爬起来。

3. at,作业

3.1 另一个脚本

虽然amule永生了,但是如上面提到的,我觉得这个方法不酷。我要用at作业的方法实现。

这个版本是 amule_at.sh,在每一次 shell script 执行结束前,执行at,要求2分钟后再启动一次这个 shell script,这样:

1 at now + 2 minutes 2>/dev/null <<EOF
2 amule_at.sh
3 EOF

然后,我们失败了好几次。李记者说,"你老啦,应该跟踪日志输出。"跟踪日志表明,脚本确实执行了很多次,amule活着与否也判断准确,但是,amule启动失败。

李记者又说,"你老啦,应该跟踪日志输出。"我们发现 at 之下执行 amule,失败。又是跟踪日志表明,amule给出了失败信息:

Error: Unable to initialize gtk, is DISPLAY set properly?

李记者说,"你真是老了,怎么不输出日志呢,非常有把握?"我说,"是啊,我本以为一定如此。"

然后我们开始了相对漫长的讨论,关于为什么 在at里amule启动不了,而在 eshell/shell-mode/xterm 下就能启动呢?

3.2 环境变量?

term下能启动amule,而at下不能启动amule。我们猜测 环境变量 没有传递从term传到at,查了一下。

(1) eshell/shell-mode/xterm 下的环境变量:

env | sort > env.log

(2) at 下的环境变量:

1 $ at now <<EOF
2 env | sort > at.log
3 EOF
4 > > warning: commands will be executed using /bin/sh
5 job 6065 at Sat Jul 13 22:02:00 2013

以上,在at下执行"env | sort > at.log"。

为什么要排序呢,因为term下和at下的env顺序可能不同,如果顺序不同,diff
起来就麻烦了。

(3) at 和 非at下的环境变量

1 diff env.log at.log
2
3 6d5
4 < DISPLAY=:0.0
5 26c25
6 < OLDPWD=/home/young/tools
7 ---
8 > OLDPWD=/
9 29c28
10 < PWD=/home/young
11 ---
12 > PWD=/home/young/tools
13 32d30
14 < SHELL=/bin/bash
15 38d35
16 < TERM=dumb
17 41d37
18 < _=/usr/bin/env

我们猜,极可能是第4行"DISPLAY=:0.0"。第4行中的"<",表明后面的这个环境变量只有左边的文件env.log存在,而在at.log中不存在。

我们在at之前置这个环境变量,在at里跑amule,启动成功。

3.3 at版的脚本

1 #!/bin/bash
2 # check if amule is running
3 export DISPLAY=:0.0
4 amule_status=$(ps aux | grep -w amule | wc -l)
5 if [ $amule_status = '2' ]; then
6 echo 'amule running.'
7 else
8 date >> /var/log/amule_monitor.log
9 echo 'amule starting/restarting.' >> /var/log/amule_monitor.log
10 amule 1> /dev/null 2> /dev/null &
11 fi
12
13 # next cycle
14 at now + 2 minutes 2>/dev/null <<EOF
15 amule_at.sh
16 EOF

(1) 环境变量

第3行,"export DISPLAY=:0.0",就是3.2测试的结果。

(2) amule的运行状态

第4行,
4 amule_status=$(ps aux | grep -w amule | wc -l)

这行的内容,其实与死循环的版本没有任何区别。连行号都一样。

(3) 判断

在死循环版本中,有一段判断amule的状态,如果不是两行,就启动amule。在at这一版本中,也有这样一个判断,就是第5行到第11行。

这几行的代码与死循环版本也非常相似,除了加入了时间戳和日志:

8 date >> /var/log/amule_monitor.log
9 echo 'amule starting/restarting.' >> /var/log/amule_monitor.log

但是,在at这一版本中,判断单独存在,并不像死循环版本中;在列循环版本
中,判断是在循环之中的。

(4) 没有死循环

没有死循环,如何实现"永生"的效果呢?这个shell脚本执行完毕,就要退出了,而不是隐藏在后台继续执行。退出以后,如果amule死了,谁来生列肉骨呢?

所谓永生,不外乎一遍一遍跑,发现死了就救活。除了死循环,我们也可以这么干,at版本的第13行至第15行:

13 # next cycle
14 at now + 2 minutes 2>/dev/null <<EOF
15 amule_at.sh
16 EOF

第14行可以逐词译为中文:在 此刻 以后 2 分钟,执行 <<EOF...EOF 括起的语句。

"2>/dev/null"仍是表示错误重定向,重定向给null设备。这个设备有个八卦。最初的unix系统是BSD (还是AT&T?)
开发的,他们发行的是源代码,并在磁带
(没错,不是光盘)的卷标上写着,如果你有啥意见,就发到/dev/null去吧。/dev/null,那是个黑洞,数据只入不出,你只管吐槽,它不起微澜。

在at中指定的作业恰恰就是 amule_at.sh,当前的这个脚本自己。所以说,递归和迭代是多么地相似。

第14行把 amule_at.sh 排在了 atq队列
中,2分钟后,又检查一遍amule是不是活着--同时也很重要的,把amule_at.sh又一次排在了atq中。

循环仍然在,它只是换了个面目,不在shell script中,而是在 atq队列中。

就像我们刚刚从DOS的程序开始学习windows消息循环一样,循环仍然在,只是不再由你来实施,而是由"系统"提供了。这也软件工程中一件可怕的事,系统越来越多地承担任务,最后我们就无事可做,因此无业可就了。

4. 还有一种死亡

amule除了活着运行中和列了不在运行两种状态外,还有一种状态,另一种死亡的状态。

它可能是僵尸--即没有退出,因此还在ps中,amule_status结果有两行;同时也不是在运行中,因为它不对添加任务或列任务清单做出任何反应。

你是不是想到了一些人?

有个命令行工具 amulecmd可以用来帮助检查amule的状态。

如果amule 运行着:

1 $ amulecmd -c status -Pyour_password
2 This is amulecmd 2.2.6
3
4 Creating client...
5 Succeeded! Connection established to aMule 2.2.6
6 > eD2k: Now connecting
7 > Kad: Connected (ok)
8 > Download: 72.73 kB/s
9 > Upload: 49.96 kB/s
10 > Clients in queue: 358
11 > Total sources: 278

如果amule没运行:

1 young@young-laptop:~/media$ amulecmd -v -c status
2 This is amulecmd 2.2.6
3
4 Creating client...
5 Connection Failed. Unable to connect to localhost:4712

如果 amule 僵死着呢?

1 young@young-laptop:~/media$ amulecmd -v -c status
2 This is amulecmd 2.2.6
3
4 Creating client...
5 C-c C-c

第5行的"C-c C-c"不是输出,而是amulecmd一直不结束,我按键中止它运行。

但是对于shell script就麻烦了,你一直不结束,我怎么知道你到底是什么状态呢?这让我们想起了图灵停机问题。

注意:以下代码未经过测试,不保证正确。因为僵死状态我现在没有能成功重现,所以没测试过。李记者说,为了让amule僵死,我可以拔网络摔硬盘砸机器,甚至诅咒...
注意结束。

我试图用下面的代码侦测amule是否僵死:

amule_status=$( (amulecmd -c status &) | (read -t 1; echo $?; killall
amulecmd 2> /dev/null 1> /dev/null))

"amule_status=$(...)"是取后面命令运行的结果,作为变量 amule_status 的值。这个命令是:

(amulecmd -c status &) | (read -t 1; echo $?; killall amulecmd 2>
/dev/null 1> /dev/null)

管道的前半段,用"()"开了一个子进程,后台执行amulecmd -c
status,所以无论amulecmd执行能否"卡住",后面都会继续进行。amulecmd的输出给了管道的后半段:

(read -t 1; echo $?; killall amulecmd 2> /dev/null 1> /dev/null)

这后半段,其中,"read -t 1"是延时1秒,读标准输入
(已被前面的管道转为amulecmd的输出);如果read读到了东西,"echo
$?"的结果是0,否则就是别的什么。"$?"是个变量,表示前一命令执行的错误号。read读到东西了,错误号就是0,否则就不是0。

然后,"echo $?"就变成为了 amule_status的值。后面的呢?后面的killall杀掉amulecmd,以防它"卡"住了。

接着,我们判断这个新的amule_status,如果是0,那么amule还活着,或者死透了。活着,就什么也不用管,死透了,2分钟后at会通过amule_at.sh启动它。都不用担心。如果amule_status不是0,杀死amule,等它死透,再次启动amule。像这样:

1 if [ $amule_status != '0' ]; then
2 date >> /var/log/amule_monitor.log
3 echo 'amule is dead.' >> /var/log/amule_monitor.log
4 killall amule
5 sleep 15
6 amule 1> /dev/null 2> /dev/null &
7 fi


综上所述,at + 处理僵死的 完整版 如下。

1 #!/bin/bash
2 # check if amule is running
3 export DISPLAY=:0.0
4 # echo 'in script.' >> /var/log/amule_monitor.log
5 amule_status=$(ps aux | grep -w amule | wc -l)
6 if [ $amule_status = '2' ]; then
7 # echo 'amule running.' >> /var/log/amule_monitor.log
8 echo 'amule running.'
9 else
10 date >> /var/log/amule_monitor.log
11 echo 'amule starting/restarting.' >> /var/log/amule_monitor.log
12 amule 1> /dev/null 2> /dev/null &
13 fi
14
15 # check if amule is alive
16 amule_status=$( (amulecmd -c status -Pyour_password &) | (read -t
1; echo $?; killall amulecmd 2> /dev/null 1> /dev/null))
17 if [ $amule_status != '0' ]; then
18 date >> /var/log/amule_monitor.log
19 echo 'amule is dead.' >> /var/log/amule_monitor.log
20 killall amule
21 sleep 15
22 amule 1> /dev/null 2> /dev/null &
23 fi
24
25 # next cycle
26 at now + 2 minutes 2>/dev/null <<EOF
27 amule_at.sh
28 EOF

5. 生存状态

(1) 有一种生存状态,是活着,它接受输入,对外输出,做出响应,我们把这种状态叫做活着;有一种生存状态,在进程列表里,它不存在,因此,系统迅速做出响应"此人已死,有事烧纸",这是死亡;还有一种生存状态,它活着,占着CPU和内存,有时还占着屏幕,但是不对外界做出任何响应,这种状态,叫做僵尸?

不过,我们有时称为禅定。有些人看到的是东方的智慧吧。我看到的,是对恶的姑息。

(2) 有同学可能纠结于永生和再生的区别。其实,这一只草履虫和那一只草履虫又有多大的区别呢?《自私的基因》里提到,DNA从产生起,就一直没有"死亡",而只有在一代代生物间遗传或变异,即使肉体死了,DNA还在继续。它还提到,即使肉体死了,你传递给别人的思想也会一直生存下去。有点像日本动画片里的话,"带着你的份儿活下去,把你的份儿也活出来。"

我们把Unix操作系统kernel以外的那部分叫做shell,那么shell里面的是什么呢?也有部动画片以题目回答了这个问题,"Ghost
in the Shell"。

----------

本文的命令在 emacs eshell 不好使,因为 eshell 对管道支持不充分,shell-mode则没有问题。

--------------------

博客会手工同步到以下地址:

[http://giftdotyoung.blogspot.com]

[http://blog.csdn.net/younggift]

No comments: