bash&shell系列文章:http://www.cnblogs.com/f-ck-need-u/p/7048359.htmlhtml
有些時候咱們寫的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`
#!/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
爲何上文運行腳本進程,腳本中的後臺while會新生成一個腳本進程?在這裏補充說明下。
究其緣由,是由於while/for/until等是bash內置命令,它們的特殊性在於它們有一個很替它們着想的爹:bash進程。bash進程對他們的孩子很是負責,全部能直接執行的內置命令都不會建立新進程,它們直接在當前bash進程內部調用執行,因此咱們用ps/top等工具是捕捉不到cd、let、expr等等內置命令的。但正由於爹太負責,把孩子們寵壞了,這些bash內置命令的執行必須依賴於bash進程才能執行。
內置命令中還有幾個比較特殊的關鍵字:while、for、until、if、case等,它們沒法直接執行,須要結合其餘關鍵字(如do/done/then等)才能執行。非後臺狀況下,它們的爹會直接帶它們執行,但當它們放進後臺後,它們必須先找個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下?如此奇怪現象,可能你除了這裏外永遠也不會遇到。