bash&shell系列文章:http://www.cnblogs.com/f-ck-need-u/p/7048359.html


1.脚本自杀正文

有些时候我们写的shell脚本中有一些后台任务,当脚本的流程已经执行到结尾处或将其kill掉时,这些后台任务会直接挂靠在init/systemd进程下,而不会随着脚本退出而停止。

例如:

[root@mariadb ~]# cat test1.sh
#!/bin/bash
echo $BASHPID
sleep & [root@mariadb ~]# ps -elf | grep slee[p]
S root - hrtime : pts/ :: sleep

从结果中可以看到,脚本退出后,sleep进程的父进程变为了1,也就是挂在了init/systemd进程下。

这时我们可以在脚本中直接使用kill命令杀掉sleep进程。

[root@mariadb ~]# cat test1.sh
#!/bin/bash
echo $BASHPID
sleep &
kill $!

但是,如果这个sleep进程是在循环中(for、while、until均可),那就麻烦了。

例如下面的例子,直接将循环放入后台,杀掉sleep、或者exit、或者杀掉脚本自身进程、或者让脚本自动退出、甚至exec退出当前脚本shell都是无效的。

[root@mariadb ~]# cat test1.sh
#!/bin/bash
echo $BASHPID while true;do
sleep
echo
done & killall sleep
kill $BASHPID

为了分析,新建一个脚本test2.sh:

#!/bin/bash
echo $BASHPID while true;do
sleep
echo
done & sleep

然后在脚本执行的60秒内查看test2.sh进程的信息:

[root@mariadb ~]# pstree -p | grep "test2.sh"
| `-bash()---test2.sh()-+-sleep()
| `-test2.sh()---sleep()

其中pid=2923的test2.sh进程是脚本自身进程,pid=2924的test2.sh进程是while开始运行后为while提供执行环境的子shell进程(为什么会生成这个进程,见我的另一篇文章)。

所以,对于前面的test1.sh进程,杀掉了 $BASHPID 对应的test1.sh进程后,其实还有一个为while提供运行环境的test1.sh进程,且这个进程在 $BASHPID 结束后,会挂在init/systemd下。

[root@mariadb ~]# ./test1.sh 

./test1.sh: line :  Terminated              sleep
Terminated [root@mariadb ~]# pstree -p | grep sleep
|-test1.sh()---sleep()

这就是shell脚本中的一个"疑难杂症",CTRL+C中止了脚本进程,这个脚本却还在后台不断运行,且时不时地输出点信息到终端(我这里是循环中的echo命令输出的)

除非我们手动杀掉新生成的test1.sh,否则这个脚本将无限循环下去。但是,这不是很麻烦吗?

那么如何实现"脚本自杀"?其实很简单,只要在脚本退出前,使用killall命令杀掉脚本进程即可。

[root@mariadb ~]# cat test1.sh
#!/bin/bash
echo $BASHPID while true;do
sleep
echo
done & killall `basename $`

这样,在脚本退出前,两个test1.sh进程都会被杀掉。

再考虑一个问题,如果脚本已经执行到了while中的后台任务,但在执行到killall命令之前按下了CTRL+C,这时由于没有执行killall,后台任务也将挂在新的脚本进程下。我们的目的是保证脚本终止,其内进程一定终止。所以我们需要对这种情况做出合理的处理。

可以使用trap捕捉ctrl+c信号,捕捉到的时候执行killall命令即可。例如:

[root@mariadb ~]# cat test1.sh
#!/bin/bash trap "killall `basename $0`" SIGINT
echo $BASHPID while true;do
sleep
echo
done & killall `basename $`

这样就能保证脚本终止时,其内一切任务都将终止的目的。

上面的脚本并不健壮,因为 ./test1.sh 和 bash test1.sh 两种执行方式的进程名称不一样,前者的进程名称为test1.sh,后者的进程名称为bash,所以killall没法同时解决这两种情况。为了健壮性,可以加上杀后台进程"$!"的代码,并将killall换成pkill,且通过筛选全路径的方式杀掉进程:

[root@mariadb ~]# cat test1.sh
#!/bin/bash trap "pkill -f `basename $0`" SIGINT
echo $BASHPID while true;do
sleep
echo
done &
pid=$!
kill $pid
pkill -f `basename $`
为了让脚本自杀更健壮、更通用化,并省去上面结尾处的一大堆额外命令。可以在trap中一次性完成这些任务:
#!/bin/bash

trap "pkill -f $(basename $0);exit 1" SIGINT SIGTERM EXIT ERR

while true;do
sleep
echo "hello world!"
done & # do something
sleep

可能写100个shell脚本也遇不到需要一个脚本需要将while/for/until这样的语句放入后台的。但有时候也是有用的。例如,有个需求:每秒去a.txt文件中同步数据到b.txt中,然后每分钟对b.txt文件做处理。

#!/bin/bash

while true;do
(a.txt--->b.txt)
sleep
done & while true;do
(b.txt)
sleep
done

此外,对一些比较复杂的需求(我个人遇到过多次),可能也会使用到后台的循环。

本文只是提供一种杀脚本的解决方案。很多情形并非如我这里所描述的,例如不是while循环放后台,而是循环内的sleep放后台,这时(脚本终止时)sleep会挂在init/systemd下,不过这很简单。相信读懂了本文,各位已经了解了一些trap的功能以及处理这类问题的逻辑,也知道其他各种情形如何处理。

最后,有一种更方便更精确的自杀手段:man kill。在该man手册中解释了,如果kill的pid值为0,表示发送信号给当前进程组中所有进程,对shell脚本来说这意味着杀掉脚本中产生的所有进程。方案如下:

#!/bin/bash

trap "echo 'signal_handled:';kill 0" SIGINT SIGTERM

while true;do
sleep
echo "hello world! hello world!"
done &
sleep

2.补充:bash内置命令的特殊性

为什么上文运行脚本进程,脚本中的后台while会新生成一个脚本进程?在这里补充说明下。

究其原因,是因为while/for/until等是bash内置命令,它们的特殊性在于它们有一个很替它们着想的爹:bash进程。bash进程对他们的孩子非常负责,所有能直接执行的内置命令都不会创建新进程,它们直接在当前bash进程内部调用执行,所以我们用ps/top等工具是捕捉不到cd、let、expr等等内置命令的。但正因为爹太负责,把孩子们宠坏了,这些bash内置命令的执行必须依赖于bash进程才能执行。

内置命令中还有几个比较特殊的关键字:while、for、until、if、case等,它们无法直接执行,需要结合其他关键字(如do/done/then等)才能执行。非后台情况下,它们的爹会直接带它们执行,但当它们放进后台后,它们必须先找个bash爹提供执行环境:

  • 如果是在当前shell中放进后台,则这个爹是新生成的bash进程。这个新的bash进程只负责一件事,就是负责这个后台,为它的孩子们提供它们依赖的bash环境。
  • 如果是在脚本中放进后台,则这个爹就是脚本进程。由于脚本不是内置命令,它能直接负责这个后台(因为脚本进程也算是bash进程的特殊变体,也相当于一个新的bash进程)。

验证下就知道咯。

目前bash进程信息为:

[root@xuexi ~]# pstree -p | grep bash
|-sshd()-+-sshd()---bash()---mysql()
| `-sshd()-+-bash()
| `-bash()-+-grep()

将for、unitl、while、case、if等语句放进后台。例如:

[root@xuexi ~]# if true;then sleep ;fi &  

然后再查bash进程信息:

[root@xuexi ~]# pstree -p | grep bash
|-sshd()-+-sshd()---bash()---mysql()
| `-sshd()-+-bash()---bash()---sleep()
| `-bash()-+-grep()

不难看出,sleep进程之前先生成了一个pid=13295的bash进程。(注:如果这几个特殊关键字不进入后台,则是当前在bash进程下执行的)

无论它们的爹是脚本进程还是新的bash进程,它们都是当前shell下的子shell。如果某个子shell中有后台进程,当杀掉子shell,意味着杀掉了它们的爹。非内置bash命令不依赖于bash,所以直接挂在init/systemd下,而bash内置命令严重依赖于bash爹,没有爹就没法执行,所以在杀掉bash进程(上面pid=7008)的时候,bash爹(pid=13295)会立即带着它下面的进程(sleep)挂在init/systemd下。

再来验证下咯。还是刚才的后台命令。

[root@xuexi ~]# while true;do sleep ;done &

另一个窗口,查看bash进程信息:

[root@xuexi ~]# pstree -p | grep bash
|-sshd()-+-sshd()---bash()---mysql()
| `-sshd()-+-bash()---bash()---sleep()
| `-bash()-+-grep()

杀掉pid=7008的bash进程(为什么不杀pid=13468的bash进程?它是为while提供环境的bash进程,杀了这个相当于杀了while循环结构)。注意,这个bash进程是交互式登陆shell,默认情况下会忽略SIGTERM信号,所以只能使用SIGKILL信号来杀。

[root@xuexi ~]# kill - 

[root@xuexi ~]# pstree -p | grep bash
|-bash()---sleep()
|-sshd()-+-sshd()---bash()---mysql()
| `-sshd()---bash()-+-grep()

可以看到,新生成了一个bash进程,而且这个bash进程是挂在init/systemd下的,这意味着该bash和终端无关。看下面的状态为"?"。

[root@xuexi ~]# ps aux | grep bas[h]
root 0.0 0.1 pts/ Ss : : -bash
root 0.0 0.1 pts/ Ss : : -bash
root 0.0 0.1 ? S : : -bash

bash进程竟然会挂在init/systemd下?如此奇怪现象,可能你除了这里外永远也不会遇到。

如何让shell脚本自杀的更多相关文章

  1. shell 脚本学习

    Shell简介 概述 Shell是一种具备特殊功能的程序,它提供了用户与内核进行交互操作的一种接口.它接收用户输入的命令,并把它送入内核去执行.内核是Linux系统的心脏,从开机自检就驻留在计算机的内 ...

  2. 第一个shell脚本

    打开文本编辑器,新建一个文件,扩展名为sh(sh代表shell),扩展名并不影响脚本执行,见名知意就好. #!/bin/bash echo "Hello World !" &quo ...

  3. 使用C#给Linux写Shell脚本

    在这个逼格决定人格,鄙视链盛行的年头,尤其是咱们IT界,请问您今天鄙视与被鄙视的次数分别是多少?如果手中没有一点压箱的本事,那就只有看的份了.今天我们也要提升下自己的格调,学习些脑洞大开的东西,学完之 ...

  4. shell脚本规划化模板

    shell脚本规划化模板 Linux运维过程中,shell脚本是不可缺少的工具,但是每个运维人员编程的习惯都不一样,很多时候就是实现某个功能,写出来的脚本都是烂七八糟的.脚本必须规范化,应该从以后几个 ...

  5. Shell脚本编程30分钟入门

    Shell脚本编程30分钟入门 转载地址: Shell脚本编程30分钟入门 什么是Shell脚本 示例 看个例子吧: #!/bin/sh cd ~ mkdir shell_tut cd shell_t ...

  6. Linux Shell脚本逻辑操作符简介

    在写程序时,会用到条件判断,测试条件是否成立.很多时候,判断条件是多个的,这个时候需要用到逻辑操作符.shell脚本中常用的有哪些逻辑操作符呢? 1.逻辑与: -a 格式: conditon1 -a ...

  7. Linux shell脚本编程(三)

    Linux shell脚本编程 流程控制: 循环语句:for,while,until while循环: while CONDITION; do 循环体 done 进入条件:当CONDITION为“真” ...

  8. Linux shell脚本编程(二)

    Linux shell脚本编程(二) 练习:求100以内所有偶数之和; 使用至少三种方法实现; 示例1: #!/bin/bash # declare -i sum=0 #声明一个变量求和,初始值为0 ...

  9. Linux shell脚本编程(一)

    Linux shell脚本编程: 守护进程,服务进程:启动?开机时自动启动: 交互式进程:shell应用程序 广义:GUI,CLI GUI: CLI: 词法分析:命令,选项,参数 内建命令: 外部命令 ...

随机推荐

  1. 用Dw CS6运行静态页面出问题

    1.报错截图一 2.报错截图二

  2. LINUX下printf输出字体的特效

    在学习LINUX网络编程的时候我们做了一个聊天系统,当时为了界面更漂亮点,于是在百度上搜索了下关于printf()函数的用法,和大家分享下:                           给pr ...

  3. Solution for link error:Cannot Open File 'python27_d.lib'

    引自:http://guangboo.org/2013/01/17/solution-link-errorcannot-open-file-python27_dlib 感谢原作者 使用C调用Pytho ...

  4. 百度地图JavaScript API经纬度查询-MAP

    百度地图JavaScript API经纬度查询-MAP-ABCDEFGHIJKMHNOPQRSTUVWXYZ: 搜索:<input type="text" size=&quo ...

  5. java中equals和==以及toString

    我们通常在控制台输出时使用System.out.print(),print只能直接输出基本类型和字符串,对于其他的类型直接输出将会输出@开头的引用,因此若需要输出对应的内容则需要使用toSring方法 ...

  6. Django学习-18-中间件

    为了验证中间件功能和函数执行过程,手动注册3个中间件到setting.py文件中 MIDDLEWARE = [ 'django.middleware.security.SecurityMiddlewa ...

  7. Ajax前后台交互函数

    function AJAX(obj){ //做网络请求的时候参数以对象的形式传递进来 //我们规定obj里面包含属性:url, //请求方式type, //date前端给后端传递的参数 //回调函数s ...

  8. THUWC2018滚粗记

    THUWC2018滚粗记 前言 又是一篇滚粗记, 不过可能还要写过很多很多篇滚粗记, 才会有一篇不是滚粗记的东西啦 总而言之,我现在还是太菜了 还要过一大段时间我才会变强啦 Day -inf 联赛考完 ...

  9. NOIP2010题解

    所有题目链接均来自洛谷 T1机器翻译 原题戳这里 自古T1是水题 因为每一个数字都小于1000,所以对于是否在队列中可以开数组查询 对于大小的限制,弄一个队列维护大小即可(水题呀...) 这题在Win ...

  10. C# Redis实战(三)

    三.程序配置 在C# Redis实战(二)中我们安装好了Redis的系统服务,此时Redis服务已经运行. 现在我们需要让我们的程序能正确读取到Redis服务地址等一系列的配置信息,首先,需要在Web ...