2021-05-12 14:32:11
為shell佈置陷阱:trap捕捉信號方法論
本文目錄:
1.1 信號說明
1.2 trap佈置陷阱
1.3 布置完美陷阱必備知識
家裡有老鼠,快消滅它!哎,又給跑了。老鼠這小東西跑那麼快,想直接直接消滅它還真不那麼容易。於是,老鼠藥、老鼠夾子或老鼠籠就派上用場了,它們都是陷阱,放在那靜靜地等待著老鼠的光顧。
在shell中,也可以捉"老鼠",捉到"老鼠"後,可以無視它、殺死它或者抓起來逗一番。只需使用內建命令trap(中文就翻譯為陷阱、圈套)就可以佈置一個陷阱,這個陷阱當然不是捕老鼠的,而是捕捉信號。
通常trap都在指令碼中使用,主要有2種功能:
(1).忽略信號。當執行中的指令碼進程接收到某信號時(例如誤按了CTRL+C),可以將其忽略,免得指令碼執行到一半就被終止。
(2).捕捉到信號後做相應處理。主要是清理一些指令碼建立的臨時檔案,然後退出。
1.1 信號說明
詳細的信號說明見:信號。常見的信號以及它們的數值代號、說明如下:
Signal Value Comment ───────────────────────────── SIGHUP 1 終止進程,特別是終端退出時,此終端內的進程都將被終止 SIGINT 2 中斷進程,幾乎等同於sigterm,會盡可能的釋放執行clean-up,釋放資源,儲存狀態等(CTRL+C) SIGQUIT 3 從鍵盤發出殺死(終止)進程的信號 SIGKILL 9 強制殺死進程,該信號不可被捕捉和忽略,進程收到該信號後不會執行任何clean-up行為,所以資源不會釋放,狀態不會儲存 SIGTERM 15 殺死(終止)進程,幾乎等同於sigint信號,會盡可能的釋放執行clean-up,釋放資源,儲存狀態等 SIGSTOP 19 該信號是不可被捕捉和忽略的進程停止資訊,收到信號後會進入stopped狀態 SIGTSTP 20 該信號是可被忽略的進程停止信號(CTRL+Z)
每個信號其真實名稱並非是SIGXXX,而是去除SIG後的單詞,每個信號還有其對應的數值代號,在使用信號時,可以使用這3種方式中的任一一種。例如SIGHUP,它的信號名稱為HUP,數值代號為1,傳送HUP信號時,以下3種方式均可。
kill -1 PID kill -HUP PID kill -SIGHUP PID
在上面所列的信號列表中,KILL和STOP這兩個信號無法被捕捉。一般來說,在設定信號陷阱時,只會考慮HUP、INT、QUIT、TERM這4個會終止、中斷進程的信號。
1.2 trap佈置陷阱
trap的語法格式為:
1. trap [-lp] 2. trap cmd-body signal_list 3. trap '' signal_list 4. trap signal_list 5. trap - signale_list 語法說明: 語法1:-l選項用於列出當前系統支援的信號列表,和"kill -l"一樣的作用。 -p選項用於列出當前shell環境下已經佈置好的陷阱。 語法2:當捕捉到給定的信號列表中的某個信號時,就執行此處給定cmd-body中的命令。 語法3:命令引數為空字串,這時shell進程和shell進程內的子進程都會忽略信號列表中的信號。 語法4:省略命令引數,重置陷阱為啟動shell時的陷阱。不建議此語法,當給定多個信號時結果會出人意料。 語法5:等價於語法4。 trap不接任何引數和選項時,預設為"-p"。
(1).檢視當前shell已佈置的陷阱。
[root@linuxidc ~]# trap trap -- '' SIGTSTP trap -- '' SIGTTIN trap -- '' SIGTTOU
這3個陷阱都是信號忽略陷阱,當捕獲到TSTP、TTIN或TTOU信號時,將不做任何處理。
(2).設定一個可以忽略CTRL+C和15信號的陷阱。
[root@linuxidc ~]# trap '' SIGINT SIGTERM [root@linuxidc ~]# trap trap -- '' SIGINT trap -- '' SIGTERM trap -- '' SIGTSTP trap -- '' SIGTTIN trap -- '' SIGTTOU
這樣一來,當前的shell就無法被kill -15殺死。
[root@linuxidc ~]# kill $BASHPID;echo kill current bash failed kill current bash failed
(3).設定一個陷阱,當這個陷阱捕捉到15信號時,就列印一條訊息。
[root@linuxidc ~]# trap 'echo caught the TERM signal' TERM [root@linuxidc ~]# kill $BASHPID caught the TERM signal
再檢視已設定的陷阱,之前設定為忽略TERM信號的陷阱已經被覆蓋。
[root@linuxidc ~]# trap trap -- '' SIGINT trap -- 'echo caught the TERM signal' SIGTERM trap -- '' SIGTSTP trap -- '' SIGTTIN trap -- '' SIGTTOU
(4).重置針對INT和TERM這兩個信號的陷阱為初始狀態。
[root@linuxidc ~]# trap - SIGINT SIGTERM [root@linuxidc ~]# trap trap -- '' SIGTSTP trap -- '' SIGTTIN trap -- '' SIGTTOU
(5).在指令碼中設定一個能忽略CTRL+C和SIGTERM信號的陷阱。
[root@linuxidc ~]# cat trap1.sh #!/bin/bash # script_name: trap1.sh # trap '' SIGINT SIGTERM sleep 10 echo sleep success
當執行該指令碼後,將首先陷入睡眠狀態,按下CTRL+C將無效。仍會執行完所有的命令。
[root@linuxidc ~]# ./trap1.sh
^C^C^C^Csleep success
(6).布置一個當指令碼中斷時能清理垃圾並退出立即指令碼的陷阱。
[root@linuxidc ~]# cat trap1.sh #!/bin/bash # script_name: trap1.sh # trap 'echo trap handling...;rm -rf /tmp/$BASHPID$BASHPID;echo TEMP file cleaned;exit' SIGINT SIGTERM SIGQUIT SIGHUP mkdir -p /tmp/$BASHPID$BASHPID/ touch /tmp/$BASHPID$BASHPID/{a.txt,a.log} sleep 10 echo first sleep success sleep 10 echo second sleep success
這樣,無論是什麼情況中斷(除非是SIGKILL),指令碼總能清理掉臨時垃圾。
1.3 布置完美陷阱必備知識
(1).陷阱的守護物件是shell進程本身,不會守護shell環境內的子進程。但如果是信號忽略型陷阱,則會守護整個shell行程群組使其忽略給定信號。
以下面這個指令碼為例,設定的陷阱會捕捉到SIGING和SIGTERM兩個信號,捕捉到信號時將輸出陷阱做出處理的時間點。
[root@linuxidc ~]# cat trap2.sh #!/bin/bash # script_name: trap2.sh # trap 'echo trap_handle_time: $(date +"%F %T")' SIGINT SIGTERM echo time_start: $(date +"%F %T") sleep 10 echo time_end1: $(date +"%F %T") sleep 10 echo time_end2: $(date +"%F %T")
執行該指令碼,並另開一個對談視窗,殺死trap2.sh指令碼。
[root@linuxidc ~]# ./trap2.sh [root@linuxidc ~]# killall -s SIGTERM trap2.sh
執行結果如下。
time_start: 2017-08-14 12:59:23 trap_handle_time: 2017-08-14 12:59:33 time_end1: 2017-08-14 12:59:33 time_end2: 2017-08-14 12:59:43
結果中的trap_handle_time證明,指令碼所在shell進程收到SIGTERM信號後,trap成功進行了處理。如果細心的話,會發現trap處理的時間正好是10秒之後,這並不是因為正好10秒之後才傳送SIGTERM信號,而是因為trap就是這麼工作的,這是另一個需要注意的點,稍後見下文的(2)。
再次執行指令碼,在另個對談視窗下殺死指令碼中正在執行的sleep進程和trap2.sh指令碼所在進程。
[root@linuxidc ~]# ./trap2.sh [root@linuxidc ~]# killall -s SIGTERM sleep ;sleep 3; killall -s SIGINT trap2.sh # 另一個對談終端下執行此命令
最終將返回如下結果:
time_start: 2017-08-14 12:23:06 Terminated # 接收到對sleep傳送的SIGTERM信號 time_end1: 2017-08-14 12:23:09 # 沒有trap_handle_time,陷阱沒有守護sleep進程 trap_handle_time: 2017-08-14 12:23:19 # shell進程本身收到了SIGINT信號,並被陷阱處理了 time_end2: 2017-08-14 12:23:19
結果說明指令碼中的trap陷阱沒有守護shell內的sleep進程,只守護了shell本身。同樣也發現了,雖然是在3秒後傳送INT信號給指令碼進程,但陷阱同樣是在10秒之後才開始處理的。
再修改指令碼中的陷阱為信號忽略陷阱。
[root@linuxidc ~]# cat ./trap3.sh #!/bin/bash # script_name: trap3.sh # trap '' SIGINT SIGTERM echo time_start: $(date +"%F %T") sleep 10 echo time_end1: $(date +"%F %T") sleep 10 echo time_end2: $(date +"%F %T")
執行trap3.sh,並在另一個對談終端下殺死sleep進程。
[root@linuxidc ~]# ./trap3.sh [root@linuxidc ~]# killall -s SIGTERM sleep;sleep 3;killall -s SIGINT sleep # 另一個對談終端下執行此命令
結果如下。從時間差可以看出,無論是SIGTERM還是SIGINT信號,sleep進程都被忽略型trap守護了。
time_start: 2017-08-14 12:31:54 time_end1: 2017-08-14 12:32:04 time_end2: 2017-08-14 12:32:14
(2).如果shell中針對某信號設定了陷阱,則該shell進程接收到該信號時,會等待其內正在執行的命令結束才開始處理陷阱。
其實(1)中的幾個範例的結果已經證明了這一點。只要是向shell進程傳送的信號,都會等待當前正在執行的命令結束後才處理信號,然後繼續指令碼向下執行。
(3).CTRL+C和SIGINT不是等價的。當某一時刻按下CTRL+C,它是在向整個當前執行的行程群組傳送SIGINT信號。對shell指令碼來說,SIGINT不僅傳送給shell指令碼進程,還傳送給指令碼中當前正在執行的進程。
所以,如果shell中設定SIGINT陷阱,不僅會終止指令碼中當前正在執行的進程,trap還會立即進行對應的處理。
以下面的指令碼trap4.sh為例。
[root@linuxidc ~]# cat trap4.sh #!/bin/bash # script_name: trap4.sh # trap 'echo trap_handle_time: $(date +"%F %T")' SIGINT echo time_start: $(date +"%F %T") sleep 10 echo time_end1: $(date +"%F %T") sleep 10 echo time_end2: $(date +"%F %T")
如果使用kill命令向trap4.sh傳送信號,正常情況下trap會在當前執行的sleep進程完成後才進行相關處理。但如果是按下CTRL+C,先看結果。
[root@linuxidc ~]# ./trap4.sh time_start: 2017-08-14 13:41:30 ^Ctrap_handle_time: 2017-08-14 13:41:31 time_end1: 2017-08-14 13:41:31 ^Ctrap_handle_time: 2017-08-14 13:41:32 time_end2: 2017-08-14 13:41:32
結果中顯示,兩次按下CTRL+C後,不僅sleep立刻結束了,trap也立即進行處理了。這說明CTRL+C不僅讓指令碼進程收到了SIGINT信號,也讓當前正在執行的進程收到了SIGINT信號。
需要特別說明的是,如果當前正在執行的進程處在迴圈內,當該進程收到了終止進程後,僅僅只是立即終止當次進程,而不會終止整個迴圈,也就是說,它還會繼續向下執行後續命令並進入下一個迴圈。如果此時是使用CTRL+C傳送SIGINT,則每次CTRL+C時,trap也會一次次進行處理。
注意點(1)(2)(3)很重要,因為搞清楚了它們,才能明白指令碼中當前正在執行的進程是先完成還是立即結束,這在寫複雜指令碼或任務型指令碼極其重要。例如大量文件中www.example.com需要替換成www.example.net,假如使用sed進行處理,我們肯定不希望替換了一部分檔案的時候被臨時終止。
(4).每個陷阱都有守護範圍。每一個陷阱只將守護它後面的所有進程,直到遇到下一個相同信號的陷阱。
以shell指令碼為例,如下圖所示。
(5).當shell環境下設定了信號忽略陷阱時,子shell在啟動時將繼承該陷阱,且這些信號忽略陷阱不可再改變或重置。信號忽略陷阱是子shell唯一繼承的陷阱型別。
先在當前shell環境下設定一個忽略SIGINT的陷阱,和一個不忽略SIGTERM的陷阱。
[root@linuxidc ~]# trap '' SIGINT [root@linuxidc ~]# trap 'echo haha' SIGTERM
以下是測試指令碼。指令碼中首先輸出指令碼剛啟動時的最初陷阱列表,隨後修改陷阱並輸出新的陷阱列表,最後重置陷阱並輸出重置後的陷阱列表。
[root@linuxidc ~]# cat trap6.sh #!/bin/bash # script_name: trap6.sh echo old_trap:-------- trap -p trap 'echo haha' SIGINT SIGTERM echo new_trap:-------- trap -p echo "reset trap:------" trap - SIGINT SIGTERM trap -p
執行結果如下。
[root@linuxidc ~]# ./trap6.sh old_trap:-------- trap -- '' SIGINT new_trap:-------- trap -- '' SIGINT trap -- 'echo haha' SIGTERM reset trap:------ trap -- '' SIGINT
從結果中可以看出,啟動指令碼時,父shell中忽略SIGINT的陷阱被繼承了,但不忽略信號的陷阱未被繼承。而且指令碼繼承的信號忽略陷阱無法被修改和重置。
(6).互動式的shell下,如果沒有定義任何SIGTERM信號的陷阱,則會忽略該信號。
所以,在預設(未定義SIGTERM陷阱)時,無法直接通過15信號殺死當前bash進程。
[root@linuxidc ~]# kill $BASHPID;echo passed;kill -9 $BASHPID passed # 此處當前bash已被kill -9強制殺死
(7).除了kill -l或trap -l列出的信號列表,trap還有4種特殊的信號:EXIT(或信號程式碼0)、ERR、DEBUG和RETURN。DEBUG和RETURN這兩種信號陷阱無需關注。
EXIT信號也是0信號,當設定了EXIT陷阱時,每次exit的時候都會被捕捉,並做相關處理。
ERR陷阱是在設定了"set -e"時生效的,當設定了"set -e"選項,每次遇到非0退出狀態碼時會退出當前shell,如果寫在指令碼中,就是退出指令碼。有了它就不用再在指令碼中書寫對"$?"是否(不)等於0的判斷語句,不過它主要用於避免指令碼中產生錯誤時,錯誤被滾雪球式的不斷放大。很多人將這一設定當作寫shell指令碼的一項行為規範,但我個人不完全認同,很多時候非0退出狀態碼是無關緊要的,甚至有時候非0狀態碼才是繼續執行的必要條件。
回到話題上。先看看"set -e"的效果。以下面的指令碼為例,在指令碼中,mv命令少給了一個引數,它是錯誤命令,返回的是非0狀態碼。
[root@linuxidc ~]# vim trap8.sh #!/bin/bash set -e echo "right here" mv ~/a.txt [ "$?" -eq 0 ] && echo "right again" || echo "wrong here"
如果不設定"set -e",那麼會被下一條語句判斷,但因為設定了"set -e",使得在mv錯誤發生時,就立即退出指令碼所在的shell。也就是說,對"$?"的判斷語句根本就是多餘的。結果如下。
[root@linuxidc ~]# ./trap8.sh right here mv: missing destination file operand after ‘/root/a.txt’ Try 'mv --help' for more information.
可以設定ERR陷阱,專門捕獲"set -e"起作用時的信號。例如,當命令錯誤時,做一些臨時檔案清理動作等。注意,當捕獲到了ERR信號時,指令碼不會再繼續向下執行,而是trap處理結束後就立即退出。例如:
[root@linuxidc ~]# vim trap8.sh #!/bin/bash set -e trap 'echo continue' ERR echo "right here" mv ~/a.txt [ "$?" -eq 0 ] && echo "right again" || echo "wrong here" echo haha
執行結果如下:
[root@linuxidc ~]# ./trap8.sh right here mv: missing destination file operand after ‘/root/a.txt’ Try 'mv --help' for more information. continue
(8).在trap中兩個很好用的變數:BASH_COMMAND和LINENO。BASH_COMMAND變數記錄的是當前正在執行的命令列,如果是用在陷阱中,則記錄的是陷阱觸發時正在執行的命令列。LINENO記錄的是正在執行的命令所處行號。
例如:
[root@linuxidc ~]# vim trap8.sh #!/bin/bash set -e trap 'echo "error line: $LINENO,error cmd: $BASH_COMMAND"' ERR echo "right here" mv ~/a.txt
執行結果。
[root@linuxidc ~]# ./trap8.sh right here mv: missing destination file operand after ‘/root/a.txt’ Try 'mv --help' for more information. error line: 5,error cmd: mv ~/a.txt
(9).處理指令碼中啟動的後台進程。
通常trap在指令碼中的作用之一是在突然被中斷時清理一些臨時檔案然後退出,雖然它會等待指令碼中當前正在執行的命令結束,然後清理並退出。但是,很多時候會在指令碼中使用後台進程,以加快指令碼的速度。而後台進程是獨立掛靠在init/systemd下的,所以它不受終端以及shell環境的影響。換句話說,當指令碼突然被中斷時,即使陷阱捕獲到了該信號,並清理了臨時檔案後退出,但是那些指令碼中啟動的後台進程還會繼續執行。
這就給指令碼帶來了一些不可預測性,一個健壯的指令碼必須能夠正確處理這種情況。trap可以實現比較好的解決這種問題,方法是在trap的命令列中加上向後台進程傳送信號的語句,然後再退出。
以下面的指令碼為例。
[root@linuxidc ~]# vim trap10.sh #!/bin/bash trap 'echo first trap $(date +"%F %T");exit' SIGTERM echo first sleep $(date +"%F %T") sleep 20 & echo second sleep $(date +"%F %T") sleep 5
該指令碼中首先將一個sleep放入後台執行。正常情況下,該指令碼執行5秒後就會退出,但在20秒後後台進程sleep才會結束,即使突然傳送中斷信號TERM觸發trap也一樣。
於是現在的目標是,在sleep 5的過程中突然中斷指令碼時,能殺死後台sleep進程。可以使用"!"這個特殊變數。修改後的指令碼如下。
[root@linuxidc ~]# vim trap10.sh #!/bin/bash trap 'echo first trap $(date +"%F %T");kill $pid;exit' SIGTERM echo first sleep $(date +"%F %T") sleep 20 & pid="$!" sleep 30 & pid="$! $pid" echo second sleep $(date +"%F %T") sleep 5
執行該指令碼,並在另一個對談視窗傳送SIGTERM信號給該指令碼進程。
[root@linuxidc ~]# ./trap10.sh ; ps aux | grep sleep [root@linuxidc ~]# kill trap10.sh # 另一個對談視窗執行
執行結果如下。可見sleep被正常終止。
first sleep 2017-08-14 21:29:19 second sleep 2017-08-14 21:29:19 first trap 2017-08-14 21:29:24 root 69096 0.0 0.0 112644 952 pts/0 S+ 21:29 0:00 grep --color=auto sleep
本文永久更新連結地址:http://www.linuxidc.com/Linux/2017-08/146607.htm
相關文章