如何讓shell指令碼自殺

駿馬金龍發表於2018-03-28

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 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程式下。

這時我們可以在指令碼中直接使用kill命令殺掉sleep程式。

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

但是,如果這個sleep程式是在迴圈中(for、while、until均可),那就麻煩了。

例如下面的例子,直接將迴圈放入後臺,殺掉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:

#!/bin/bash
echo $BASHPID

while true;do
    sleep 50
    echo 1
done &

sleep 60

然後在指令碼執行的60秒內檢視test2.sh程式的資訊:

[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下。

[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`

可能寫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和指令碼尾部的killall加上sleep即可killall sleep `basename $0`。相信讀懂了本文,其他各種情形都知道如何去處理。

 

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下?如此奇怪現象,可能你除了這裡外永遠也不會遇到。

相關文章