如何讓shell腳本自殺

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


1.腳本自殺正文

有些時候咱們寫的shell腳本中有一些後臺任務,當腳本的流程已經執行到結尾處或將其kill掉時,這些後臺任務會直接掛靠在init/systemd進程下,而不會隨着腳本退出而中止。mysql

例如:sql

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

[root@mariadb ~]# ps -elf | grep slee[p]
0 S root      10806      1  0  80   0 - 26973 hrtime 19:26 pts/1    00:00:00 sleep 50

從結果中能夠看到,腳本退出後,sleep進程的父進程變爲了1,也就是掛在了init/systemd進程下。shell

這時咱們能夠在腳本中直接使用kill命令殺掉sleep進程。bash

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

可是,若是這個sleep進程是在循環中(for、while、until都可),那就麻煩了。ssh

例以下面的例子,直接將循環放入後臺,殺掉sleep、或者exit、或者殺掉腳本自身進程、或者讓腳本自動退出、甚至exec退出當前腳本shell都是無效的。工具

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

while true;do
    sleep 50
    echo 1
done &

killall sleep
kill $BASHPID

爲了分析,新建一個腳本test2.sh:spa

#!/bin/bash
echo $BASHPID

while true;do
    sleep 50
    echo 1
done &

sleep 60

而後在腳本執行的60秒內查看test2.sh進程的信息:code

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

 其中pid=2923的test2.sh進程是腳本自身進程,pid=2924的test2.sh進程是while開始運行後爲while提供執行環境的子shell進程(爲何會生成這個進程,見個人另外一篇文章)。

因此,對於前面的test1.sh進程,殺掉了 $BASHPID 對應的test1.sh進程後,其實還有一個爲while提供運行環境的test1.sh進程,且這個進程在 $BASHPID 結束後,會掛在init/systemd下。htm

[root@mariadb ~]# ./test1.sh 
10859
./test1.sh: line 7: 10862 Terminated              sleep 50
Terminated
1
[root@mariadb ~]# pstree -p | grep sleep
           |-test1.sh(10860)---sleep(10863)

這就是shell腳本中的一個"疑難雜症",CTRL+C停止了腳本進程,這個腳本卻還在後臺不斷運行,且時不時地輸出點信息到終端(我這裏是循環中的echo命令輸出的)

除非咱們手動殺掉新生成的test1.sh,不然這個腳本將無限循環下去。可是,這不是很麻煩嗎?

那麼如何實現"腳本自殺"?其實很簡單,只要在腳本退出前,使用killall命令殺掉腳本進程便可。

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

while true;do
    sleep 50
    echo 1
done &

killall `basename $0`

這樣,在腳本退出前,兩個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 50
    echo 1
done &

killall `basename $0`

這樣就能保證腳本終止時,其內一切任務都將終止的目的。

 上面的腳本並不健壯,由於 ./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 50
    echo 1
done &
pid=$!
kill $pid pkill
-f `basename $0`
爲了讓腳本自殺更健壯、更通用化,並省去上面結尾處的一大堆額外命令。能夠在trap中一次性完成這些任務:
#!/bin/bash

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

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

# do something
sleep 60

可能寫100個shell腳本也遇不到須要一個腳本須要將while/for/until這樣的語句放入後臺的。但有時候也是有用的。例如,有個需求:每秒去a.txt文件中同步數據到b.txt中,而後每分鐘對b.txt文件作處理。

#!/bin/bash

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

while true;do
    (b.txt)
    sleep 60
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 5
    echo "hello world! hello world!"
done &
sleep 60

 

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(1142)-+-sshd(5396)---bash(5398)---mysql(5659)
           |            `-sshd(7006)-+-bash(7008)
           |                         `-bash(12280)-+-grep(13294)

將for、unitl、while、case、if等語句放進後臺。例如:

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

而後再查bash進程信息:

[root@xuexi ~]# pstree -p | grep bash
           |-sshd(1142)-+-sshd(5396)---bash(5398)---mysql(5659)
           |            `-sshd(7006)-+-bash(7008)---bash(13295)---sleep(13296)
           |                         `-bash(12280)-+-grep(13298)

不難看出,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 2;done &

另外一個窗口,查看bash進程信息:

[root@xuexi ~]# pstree -p | grep bash 
           |-sshd(1142)-+-sshd(5396)---bash(5398)---mysql(5659)
           |            `-sshd(7006)-+-bash(7008)---bash(13468)---sleep(13526)
           |                         `-bash(12280)-+-grep(13528)

殺掉pid=7008的bash進程(爲何不殺pid=13468的bash進程?它是爲while提供環境的bash進程,殺了這個至關於殺了while循環結構)。注意,這個bash進程是交互式登錄shell,默認狀況下會忽略SIGTERM信號,因此只能使用SIGKILL信號來殺。

[root@xuexi ~]# kill -9 7008

[root@xuexi ~]# pstree -p | grep bash
           |-bash(13468)---sleep(13562)
           |-sshd(1142)-+-sshd(5396)---bash(5398)---mysql(5659)
           |            `-sshd(7006)---bash(12280)-+-grep(13564)

能夠看到,新生成了一個bash進程,並且這個bash進程是掛在init/systemd下的,這意味着該bash和終端無關。看下面的狀態爲"?"。

[root@xuexi ~]# ps aux | grep bas[h]
root       5398  0.0  0.1 116548  3300 pts/0    Ss   09:04   0:00 -bash
root      12280  0.0  0.1 116568  3340 pts/2    Ss   14:43   0:00 -bash
root      13468  0.0  0.1 116556  1924 ?        S    15:49   0:00 -bash

bash進程居然會掛在init/systemd下?如此奇怪現象,可能你除了這裏外永遠也不會遇到。

相關文章
相關標籤/搜索