0x00 起因
昨天,大力找到我,想搞点事情…看图
大力一顿描述之后,我心想:大概就是好几个人共享一台服务器,假如客户A正在使用GPU,这时候客户B来了,执行了一条调用GPU的命令,A就会被迫中止使用GPU,GPU资源则会被B占去,A只能重新执行命令,把B挤下去,才能再次获得GPU,既然是这样的话,那就写个守护进程嘛,当我对GPU的调用进程被别人挤掉之后,我再拉起来把别人挤掉不久OK了,SO EASY!
(P.S. 上面一段话是我在听完大力描述之后的心理活动。注意最后两个单词加粗大写,充分说明了我昨天的无知…唉,如果可以,我这辈子再也不要写shell脚本了)
0x01 分析
- 用什么语言写
既然是在Linux下写守护进程,那就很方便了,Python,C,Shell都是很方便的选择啊。到底用啥好呢,emmmmmmm,涉及到Linux命令比较多,还是选Shell脚本吧。 - 实现原理
从需求出发,从过程分析,可以得知,我想要在我进程被人挤掉的时候被拉起来:
1) 如何判断我的进程被挤掉了呢?
我调用GPU的任务是固定的,也就是说,我的进程名是固定不会变的,那我只要对系统当前所有的进程进行一个监测,就可以知道我的进程是否被挤掉了。核心命令如下:
#获取要守护进程的PID
PID=`ps -ef |grep PROCESS_NAME |grep -v grep |awk '{print $2}'`
将命令中的PROCESS_NAME 替换成你自己要守护的进程,该命令执行之后会获取要守护进程的pid值并传给变量PID。
我们只要对变量PID进行判断,是否为空,就能知道我得进程是否还在运行了。如果为空,未获取到进程的PID,则说明进程被挤掉。
2) 当我的进程被挤掉的时候,我当然是要把它拉起来了!
怎么拉起来?在shell脚本里执行一下我调用GPU的命令呗
3) 我应该什么时候去监测我的进程被挤掉了呢?
谁都不知道下一个人什么时候来挤我,那我也只好全天24h跟你干了,写个死循环,不停地监听进程是否被挤掉。
但是我同时又想知道你是什么来怼我的,emmm,写个日志吧。
可是写日志的话,对磁盘读写太频繁了,会影响计算机运行速度(虽然我要写的东西并不多,我就这么一说,你就这么一听好了,嘿嘿),同时日志太多了我也看不过来啊,那咋办?恶魔妈妈买面膜…(emmmmmm…)那我设置个频率吧,每隔多久去监测一次进程好了。
- 好了,看一眼守护进程基本流程图吧,我用的画图画的,丑,有意见我也莫得法~~哈哈哈
0x02 开搞
基本逻辑已经理清楚了,兴奋地开搞吧!(开始哭吧…)
哎,一年多没写Shell了,这该死的语法,太难写了,哭哭哭…
这该死的shell语法:
1. if [ 条件表达式 ] #这里的条件表达式,一定要在中括号里面有两个空格把条件表达式隔开,
#我卡在这里卡了不知道有几个小时,跑程序的时候,各种报错,说附近有语法错误,死活找不到,我擦!
2.唉,看看序号,自己想列出来的坑,突然懒得吐槽了,不写了!什么破语法规则,脑仁疼!
3.大家慢慢....
4.去踩坑吧....
在上述基本逻辑写完之后,先将程序跑起来,新开一个terminal,模拟进程被挤掉,执行:
kill -9 PID值
将GPU调用的进程结束,再去跑GPU的终端查看回显,发现进程被结束之后,又被自动拉起来了,顺利!查看一下日志输出大概就是:
① 正常运行…
②然后监测到进程down了
③进程拉起中…
④拉起成功
⑤运行正常…
此时,满心以为:嗯呐SO~~ EASY!好吧,不SO了,EASY!可恶的语法!
突然,进程又down了。
查看回显信息→回显卡在进程被kill的提示→查看日志→发现日志里写满了拉起失败…
咋回事???一个头,两个大的我赶紧检查GPU,CPU,进程列表,用户状态等等信息,发现其他共享人登上来,占用了GPU…尴尬,没拉起来。此时我的内心:不EASY啊!怎么办啊?
死马当活马医,我试了试kill掉对方的进程,再看看我的terminal,开始回显,再查看日志,哈哈拉起成功。
恶魔妈妈买面膜…看来需要先kill掉别人的进程。于是我在原来的逻辑框架上,加了一个kill别人进程的逻辑。
流程图如下:
那实现获取别人进程以及遍历一个kill一个,是怎么实现的呢?
我一拍脑门,啊哈,SO~~EASY 递归!(注意大写加粗T~T)
于是就有了下面的递归函数
#!/bin/bash
# 使用 |grep -v PROCESS_NAME 来过滤掉自己的进程
PID_ARRAY=(`ps -a |grep -v bash |grep -v watch |grep -v sleep |grep -v grep |grep -v nvidia-smi |grep -v st |grep -v awk |grep -v ps |awk '{print $1}'`)
#递归杀进程
kill_ps()
{
if [ $1 ]
then
kill -9 ${PID_ARRAY[0]} #杀进程
unset PID_ARRAY[0] #删除数组中记录的已被杀掉的进程PID
echo "$1 is killed" >> log_protect.log #输出日志
kill_ps PID_ARRARY[0] #递归
else
return 0
fi
}
#调用递归函数kill进程
kill_ps ${PID_ARRAY[0]}
#shell编程坑啊!unset掉下标为0的元素后,数组下标为1的元素并不会自动更新为0,还是1啊!!!!多么完美的递归函数,哎,我擦!
看到上面的递归,我只能叹气,我知道是我对shell的理解不到位,我只能换个办法,写个for循环:
#!/bin/bash
PID_ARRAY=(`ps -a |grep -v bash |grep -v watch |grep -v sleep |grep -v grep |grep -v nvidia-smi |grep -v st |grep -v awk |grep -v ps |awk '{print $1}'`) #进程过滤
#循环杀进程
for i in ${PID_ARRAY[*]};do
kill -9 $i
echo "$i is killed" >> log_protect.log
done
完美!
for循环真简单啊,两下子就写好了,用什么递归??用什么递归嘛!!我真是个傻子!
这时候,回首 ~ 掏 ~ 看一下日志,额,输出的都是进程PID,杀的是谁啊,不知道啊,咋办呐?于是最后使用了while循环。也是最终使用的方式,因为只有用while循环,才可以一次性接收awk的多列输出(使用awk输出的PID和进程名)。
到这里,基本功能也就全部都实现了…放出代码如下:
#!/bin/bash
FLAG=1
frequency=$1 #延时频率,单位秒
echo "" >> log_protect.log #当守护程序运行时,开始写日志,写到log_protect.log
echo "*START st.sh at $current -------------------------" >> log_protect.log
while [ $FLAG ]
do
cur_date="`date +%Y-%m-%d`" #获取机器系统日期
cur_time=$(date "+%H:%M:%S" --date="8 hours") #获取机器系统时刻,与用户有8小时时差,经计算后时差已消除
#获取进程的PID
PID=`ps -ef |grep PROCESS_NAME|grep -v grep |awk '{print $2}'`
#检查要守护的进程是否活着
if [ "$PID" ]
then #进程活着
echo "[$cur_date:$cur_time] $PID PROCESS_NAME is running! " >>log_protect.log
else #进程down了
echo "[$cur_date:$cur_time][Warning] PROCESS_NAME is down!" >> log_protect.log
echo " [$cur_time] Restar ..." >> log_st.log
echo " [$cur_time] killing other processes ..." >> log_protect.log
#过滤掉己方进程,找出敌方进程
ps -a |grep -v bash |grep -v watch |grep -v sleep |grep -v grep |grep -v nvidia-smi |grep -v st |grep -v awk |grep -v ps |awk '{print $1,$4}' |while read KILL_PID KILL_NAME
do
kill -9 $KILL_PID #kill敌方进程
echo " process [$KILL_PID:$KILL_NAME] is killed" >> log_protect.log #输出到日志
done
echo command & #这里执行拉起要守护进程的命令,最后加上&让其后台执行,不然守护进程会卡在这里等待被守护进程执行结束,才会往下继续执行
sleep 5 #延时5秒再检测进程是否重新拉起
PID=`ps -ef |grep PROCESS_NAME |grep -v grep |awk '{print $2}'` #这里对被守护进程进行监测,检查是否被拉起
if [ $PID ] #重新拉起进程
then
echo " [$cur_time] Restart Success! " >> log_protect.log
else
echo " [$cur_time] Restart Failed! " >> log_protect.log
echo " [$cur_time] Send E-mail ..." >> log_protect.log
#在此处添加发送E-mail的代码,与上一行的echo对齐
fi
fi
sleep $frequency
done
直接在terminal下执行:
bash 守护进程名.sh [参数]
守护进程就跑起来了~
为什么不直接cd到守护进程所在目录然后
./守护进程名.sh
这样跑呢?是因为:
1)bash是大多数linux进程的父进程。
2)使用bash执行守护进程,ps命令最多只能看到守护进程是bash的子进程
3)一般都不敢乱杀bash进程,如果再把守护进程放到系统目录下,就可以给不是很熟悉Linux操作系统的人一个假象,误认为是系统进程而不敢乱杀它
当然了,代码中可以看到变量frequency赋值为$1,这是运行脚本时给它传入的参数,可以控制以每隔多长时间去监听要守护的进程,单位为秒。我本来写死成300秒的。后来为了让守护进程更灵活,于是改成传参的方式。
如果运行时没传参数怎么办?同时,我又想到一个问题:传参和写死其实都是限定了频率一个值,比如说是300秒,即5分钟。如果别人第一次跑GPU,刚好我把他进程kill了,他肯定很纳闷,以为系统故障或者BUG了?于是再跑一次他的进程,调起来了,我下一次kill他则将会是在5分钟后,于是他会发现~诶嘿,好像每隔5分钟,我的进程都会断一次啊,怎么回事?于是打电话给客服,然后大力被封号,哈哈哈(我是不是笑的太早了,万一我被捶怎么办?T…T)
于是随机数定义更新频率的方案又浮现在脑海中。如果我在运行守护进程的时候没有指定频率,就可以随机一个频率给守护进程。并且设置一个时段,例如1h。每过1h更新一下频率,这样就更难让人察觉到是我在使坏了~哈哈哈
具体怎么实现,我懒得说了,哈哈哈,睡觉了~
根据不同的频率获得方式,启动脚本时,向log写入不同信息,可以分辨出当前所使用频率以及是指定获得还是随机分配,如下图所示:
0x03 总结
- 回顾一下这两天走过的坑,恶魔妈妈买面膜…我再也不想写shell了T。T
- 除了已有功能外,为了脚本的可复用性,可以对写死的要守护的进程名,定义成变量,在执行脚本的时候,指定参数,传入进程名,可以实现对任意进程的守护
- 在结束守护进程的时候,是使用ctrl+c来结束的,可以劫持该信号,在守护进程被结束前,向log写入守护进程结束的原因。这样就可以区分守护进程是被手动结束的还是什么时候被别人kill掉的
- 如果守护进程能hook到kill进程对自己发出的信号,拦截下来,可以防止自己被kill