如何讓shell腳本自殺
共 6346字,需瀏覽 13分鐘
·
2024-06-24 08:00
1.腳本自殺正文
有些時候我們寫的shell腳本中有一些后臺任務(wù),當(dāng)腳本的流程已經(jīng)執(zhí)行到結(jié)尾處或?qū)⑵鋕ill掉時,這些后臺任務(wù)會直接掛靠在init/systemd進(jìn)程下,而不會隨著腳本退出而停止。
例如:
[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
從結(jié)果中可以看到,腳本退出后,sleep進(jìn)程的父進(jìn)程變?yōu)榱?,也就是掛在了init/systemd進(jìn)程下。
這時我們可以在腳本中直接使用kill命令殺掉sleep進(jìn)程。
[root@mariadb ~]# cat test1.sh
#!/bin/bash
echo $BASHPID
sleep 50 &
kill $!
但是,如果這個sleep進(jìn)程是在循環(huán)中(for、while、until均可),那就麻煩了。
例如下面的例子,直接將循環(huán)放入后臺,殺掉sleep、或者exit、或者殺掉腳本自身進(jìn)程、或者讓腳本自動退出、甚至exec退出當(dāng)前腳本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
然后在腳本執(zhí)行的60秒內(nèi)查看test2.sh進(jìn)程的信息:
[root@mariadb ~]# pstree -p | grep 'test2.sh'
| `-bash(2687)---test2.sh(2923)-+-sleep(2925)
| `-test2.sh(2924)---sleep(2926)
其中pid=2923的test2.sh進(jìn)程是腳本自身進(jìn)程,pid=2924的test2.sh進(jìn)程是while開始運行后為while提供執(zhí)行環(huán)境的子shell進(jìn)程(為什么會生成這個進(jìn)程,見我的另一篇文章)。
所以,對于前面的test1.sh進(jìn)程,殺掉了 $BASHPID 對應(yīng)的test1.sh進(jìn)程后,其實還有一個為while提供運行環(huán)境的test1.sh進(jìn)程,且這個進(jìn)程在 $BASHPID 結(jié)束后,會掛在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中止了腳本進(jìn)程,這個腳本卻還在后臺不斷運行,且時不時地輸出點信息到終端(我這里是循環(huán)中的echo命令輸出的)。
除非我們手動殺掉新生成的test1.sh,否則這個腳本將無限循環(huán)下去。但是,這不是很麻煩嗎?
那么如何實現(xiàn)'腳本自殺'?其實很簡單,只要在腳本退出前,使用killall命令殺掉腳本進(jìn)程即可。
[root@mariadb ~]# cat test1.sh
#!/bin/bash
echo $BASHPID
while true;do
sleep 50
echo 1
done &
killall `basename $0`
這樣,在腳本退出前,兩個test1.sh進(jìn)程都會被殺掉。
再考慮一個問題,如果腳本已經(jīng)執(zhí)行到了while中的后臺任務(wù),但在執(zhí)行到killall命令之前按下了CTRL+C,這時由于沒有執(zhí)行killall,后臺任務(wù)也將掛在新的腳本進(jìn)程下。我們的目的是保證腳本終止,其內(nèi)進(jìn)程一定終止。所以我們需要對這種情況做出合理的處理。
可以使用trap捕捉ctrl+c信號,捕捉到的時候執(zhí)行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`
這樣就能保證腳本終止時,其內(nèi)一切任務(wù)都將終止的目的。
上面的腳本并不健壯,因為 ./test1.sh 和 bash test1.sh 兩種執(zhí)行方式的進(jìn)程名稱不一樣,前者的進(jìn)程名稱為test1.sh,后者的進(jìn)程名稱為bash,所以killall沒法同時解決這兩種情況。為了健壯性,可以加上殺后臺進(jìn)程'$!'的代碼,并將killall換成pkill,且通過篩選全路徑的方式殺掉進(jìn)程:
[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`
為了讓腳本自殺更健壯、更通用化,并省去上面結(jié)尾處的一大堆額外命令??梢栽趖rap中一次性完成這些任務(wù):
#!/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文件中同步數(shù)據(jù)到b.txt中,然后每分鐘對b.txt文件做處理。
#!/bin/bash
while true;do
(a.txt--->b.txt)
sleep 1
done &
while true;do
(b.txt)
sleep 60
done
此外,對一些比較復(fù)雜的需求(我個人遇到過多次),可能也會使用到后臺的循環(huán)。
本文只是提供一種殺腳本的解決方案。很多情形并非如我這里所描述的,例如不是while循環(huán)放后臺,而是循環(huán)內(nèi)的sleep放后臺,這時(腳本終止時)sleep會掛在init/systemd下,不過這很簡單。相信讀懂了本文,各位已經(jīng)了解了一些trap的功能以及處理這類問題的邏輯,也知道其他各種情形如何處理。
最后,有一種更方便更精確的自殺手段:man kill。在該man手冊中解釋了,如果kill的pid值為0,表示發(fā)送信號給當(dāng)前進(jìn)程組中所有進(jìn)程,對shell腳本來說這意味著殺掉腳本中產(chǎn)生的所有進(jìn)程。方案如下:
#!/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內(nèi)置命令的特殊性
為什么上文運行腳本進(jìn)程,腳本中的后臺while會新生成一個腳本進(jìn)程?在這里補充說明下。
究其原因,是因為while/for/until等是bash內(nèi)置命令,它們的特殊性在于它們有一個很替它們著想的爹:bash進(jìn)程。bash進(jìn)程對他們的孩子非常負(fù)責(zé),所有能直接執(zhí)行的內(nèi)置命令都不會創(chuàng)建新進(jìn)程,它們直接在當(dāng)前bash進(jìn)程內(nèi)部調(diào)用執(zhí)行,所以我們用ps/top等工具是捕捉不到cd、let、expr等等內(nèi)置命令的。但正因為爹太負(fù)責(zé),把孩子們寵壞了,這些bash內(nèi)置命令的執(zhí)行必須依賴于bash進(jìn)程才能執(zhí)行。
內(nèi)置命令中還有幾個比較特殊的關(guān)鍵字:while、for、until、if、case等,它們無法直接執(zhí)行,需要結(jié)合其他關(guān)鍵字(如do/done/then等)才能執(zhí)行。非后臺情況下,它們的爹會直接帶它們執(zhí)行,但當(dāng)它們放進(jìn)后臺后,它們必須先找個bash爹提供執(zhí)行環(huán)境:
如果是在當(dāng)前shell中放進(jìn)后臺,則這個爹是新生成的bash進(jìn)程。這個新的bash進(jìn)程只負(fù)責(zé)一件事,就是負(fù)責(zé)這個后臺,為它的孩子們提供它們依賴的bash環(huán)境。
如果是在腳本中放進(jìn)后臺,則這個爹就是腳本進(jìn)程。由于腳本不是內(nèi)置命令,它能直接負(fù)責(zé)這個后臺(因為腳本進(jìn)程也算是bash進(jìn)程的特殊變體,也相當(dāng)于一個新的bash進(jìn)程)。
驗證下就知道咯。
目前bash進(jìn)程信息為:
[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等語句放進(jìn)后臺。例如:
[root@xuexi ~]# if true;then sleep 10;fi &
然后再查bash進(jìn)程信息:
[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進(jìn)程之前先生成了一個pid=13295的bash進(jìn)程。(注:如果這幾個特殊關(guān)鍵字不進(jìn)入后臺,則是當(dāng)前在bash進(jìn)程下執(zhí)行的)
無論它們的爹是腳本進(jìn)程還是新的bash進(jìn)程,它們都是當(dāng)前shell下的子shell。如果某個子shell中有后臺進(jìn)程,當(dāng)殺掉子shell,意味著殺掉了它們的爹。非內(nèi)置bash命令不依賴于bash,所以直接掛在init/systemd下,而bash內(nèi)置命令嚴(yán)重依賴于bash爹,沒有爹就沒法執(zhí)行,所以在殺掉bash進(jìn)程(上面pid=7008)的時候,bash爹(pid=13295)會立即帶著它下面的進(jìn)程(sleep)掛在init/systemd下。
再來驗證下咯。還是剛才的后臺命令。
[root@xuexi ~]# while true;do sleep 2;done &
另一個窗口,查看bash進(jìn)程信息:
[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進(jìn)程(為什么不殺pid=13468的bash進(jìn)程?它是為while提供環(huán)境的bash進(jìn)程,殺了這個相當(dāng)于殺了while循環(huán)結(jié)構(gòu))。注意,這個bash進(jìn)程是交互式登陸shell,默認(rèn)情況下會忽略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進(jìn)程,而且這個bash進(jìn)程是掛在init/systemd下的,這意味著該bash和終端無關(guān)??聪旅娴臓顟B(tài)為'?'。
[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進(jìn)程竟然會掛在init/systemd下?如此奇怪現(xiàn)象,可能你除了這里外永遠(yuǎn)也不會遇到。
鏈接:https://www.cnblogs.com/f-ck-need-u/p/8661501.html
(版權(quán)歸原作者所有,侵刪)
