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