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. 随笔︱MRO-Microsoft R Open使用心得与相应内容总结

    每每以为攀得众山小,可.每每又切实来到起点,大牛们,缓缓脚步来俺笔记葩分享一下吧,please~ --------------------------- 往期回顾:   新工具︱微软Microsoft ...

  2. Android中Java和JavaScript交互

    Android提供了一个很强大的WebView控件用来处理Web网页,而在网页中,JavaScript又是一个很举足轻重的脚本.本文将介绍如何实现Java代码和Javascript代码的相互调用. 如 ...

  3. R语言︱基本函数、统计量、常用操作函数

    先言:R语言常用界面操作 帮助:help(nnet) = ?nnet =??nnet 清除命令框中所有显示内容:Ctrl+L 清除R空间中内存变量:rm(list=ls()).gc() 获取或者设置当 ...

  4. httpclient案例一(调用识别接口)

    public Map<String, Object> pictureRecognition(String recotype, MultipartFile imageFile) { Stri ...

  5. CentOS 7离线安装MySQL 5.7

    系列文章首发平台为果冻想个人博客.果冻想,是一个原创技术文章分享网站.在这里果冻会分享他的技术心得,技术得失,技术人生.我在果冻想等待你,也希望你能和我分享你的技术得与失,期待. 前言 网上已经有那么 ...

  6. PyTorch官方中文文档:自动求导机制

    自动求导机制 本说明将概述Autograd如何工作并记录操作.了解这些并不是绝对必要的,但我们建议您熟悉它,因为它将帮助您编写更高效,更简洁的程序,并可帮助您进行调试. 从后向中排除子图 每个变量都有 ...

  7. 第三篇:爬虫框架 - Scrapy

    前言 Python提供了一个比较实用的爬虫框架 - Scrapy.在这个框架下只要定制好指定的几个模块,就能实现一个爬虫. 本文将讲解Scrapy框架的基本体系结构,以及使用这个框架定制爬虫的具体步骤 ...

  8. Bzoj4199:[NOI2015]品酒大会

    题面 Bzoj4199 Sol 后缀数组 显然的暴力就是求\(LCP\)+差分 \(40\)分 # include <bits/stdc++.h> # define RG register ...

  9. [THUWC 2017]在美妙的数学王国中畅游

    bzoj5020 \[答案误差只要小于 10^{-7}\] 题解 Taylor展开式: \[若f(x)的n阶导数在[a, b]内连续,则f(x)在x_{0}\in[a, b]可表示为\] \[f(x) ...

  10. Python 终端输出字体颜色

    终端的字符颜色是用转义序列控制的,是文本模式下的系统显示功能,和具体的语言无关.             转义序列是以ESC开头,即用\033来完成(ESC的ASCII码用十进制表示是27,用八进制表 ...