《LINUX學習:為shell布置陷阱:trap捕捉信號方法論》要點:
本文介紹了LINUX學習:為shell布置陷阱:trap捕捉信號方法論,希望對您有用。如果有疑問,可以聯(lián)系我們。
本文目次:
1.1 信號闡明
1.2 trap布置陷阱
1.3 布置完善陷阱必備知識
家里有老鼠,快消滅它!哎,又給跑了.老鼠這小東西跑那么快,想直接直接消滅它還真不那么容易.于是,老鼠藥、老鼠夾子或老鼠籠就派上用場了,它們都是陷阱,放在那靜靜地期待著老鼠的光顧.
在shell中,也可以捉"老鼠",捉到"老鼠"后,可以無視它、殺死它或者抓起來逗一番.只需使用內(nèi)置命令trap(中文就翻譯為陷阱、圈套)就可以布置一個陷阱,這個陷阱當然不是捕老鼠的,而是捕獲信號.
通常trap都在腳本中使用,主要有2種功效:
(1).忽略信號.當運行中的腳本過程接收到某信號時(例如誤按了CTRL+C),可以將其忽略,免得腳本執(zhí)行到一半就被終止.
(2).捕獲到信號后做相應處理.主要是清理一些腳本創(chuàng)建的臨時文件,然后退出.
詳細的信號闡明見:信號.常見的信號以及它們的數(shù)值代號、闡明如下:
Signal Value Comment ───────────────────────────── SIGHUP 1 終止進程,特別是終端退出時,此終端內(nèi)的進程都將被終止 SIGINT 2 中斷進程,幾乎等同于sigterm,會盡可能的釋放執(zhí)行clean-up,釋放資源,保留狀態(tài)等(CTRL+C) SIGQUIT 3 從鍵盤發(fā)出殺死(終止)進程的信號 SIGKILL 9 強制殺死進程,該信號不可被捕捉和忽略,進程收到該信號后不會執(zhí)行任何clean-up行為,所以資源不會釋放,狀態(tài)不會保留 SIGTERM 15 殺死(終止)進程,幾乎等同于sigint信號,會盡可能的釋放執(zhí)行clean-up,釋放資源,保留狀態(tài)等 SIGSTOP 19 該信號是不可被捕捉和忽略的進程停止信息,收到信號后會進入stopped狀態(tài) SIGTSTP 20 該信號是可被忽略的進程停止信號(CTRL+Z)
每個信號其真實名稱并非是SIGXXX,而是去除SIG后的單詞,每個信號還有其對應的數(shù)值代號,在使用信號時,可以使用這3種方式中的任一一種.例如SIGHUP,它的信號名稱為HUP,數(shù)值代號為1,發(fā)送HUP信號時,以下3種方式均可.
kill -1 PID kill -HUP PID kill -SIGHUP PID
在上面所列的信號列表中,KILL和STOP這兩個信號無法被捕獲.一般來說,在設(shè)置信號陷阱時,只會考慮HUP、INT、QUIT、TERM這4個會終止、中斷進程的信號.
trap的語法格局為:
1. trap [-lp] 2. trap cmd-body signal_list 3. trap '' signal_list 4. trap signal_list 5. trap - signale_list 語法說明: 語法1:-l選項用于列出當前系統(tǒng)支持的信號列表,和"kill -l"一樣的作用. -p選項用于列出當前shell環(huán)境下已經(jīng)布置好的陷阱. 語法2:當捕獲到給定的信號列表中的某個信號時,就執(zhí)行此處給定cmd-body中的命令. 語法3:命令參數(shù)為空字符串,這時shell進程和shell進程內(nèi)的子進程都會忽略信號列表中的信號. 語法4:省略命令參數(shù),重置陷阱為啟動shell時的陷阱.不建議此語法,當給定多個信號時結(jié)果會出人意料. 語法5:等價于語法4. trap不接任何參數(shù)和選項時,默認為"-p".
(1).查看當前shell已布置的陷阱.
[root@linuxidc ~]# trap trap -- '' SIGTSTP trap -- '' SIGTTIN trap -- '' SIGTTOU
這3個陷阱都是信號忽略陷阱,當捕捉到TSTP、TTIN或TTOU信號時,將不做任何處理.
(2).設(shè)置一個可以疏忽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).設(shè)置一個陷阱,當這個陷阱捕獲到15信號時,就打印一條消息.
[root@linuxidc ~]# trap 'echo caught the TERM signal' TERM [root@linuxidc ~]# kill $BASHPID caught the TERM signal
再查看已設(shè)置的陷阱,之前設(shè)置為忽略TERM信號的陷阱已經(jīng)被籠罩.
[root@linuxidc ~]# trap trap -- '' SIGINT trap -- 'echo caught the TERM signal' SIGTERM trap -- '' SIGTSTP trap -- '' SIGTTIN trap -- '' SIGTTOU
(4).重置針對INT和TERM這兩個旌旗燈號的陷阱為初始狀態(tài).
[root@linuxidc ~]# trap - SIGINT SIGTERM [root@linuxidc ~]# trap trap -- '' SIGTSTP trap -- '' SIGTTIN trap -- '' SIGTTOU
(5).在腳本中設(shè)置一個能疏忽CTRL+C和SIGTERM信號的陷阱.
[root@linuxidc ~]# cat trap1.sh #!/bin/bash # script_name: trap1.sh # trap '' SIGINT SIGTERM sleep 10 echo sleep success
當執(zhí)行該劇本后,將首先陷入睡眠狀態(tài),按下CTRL+C將無效.仍會執(zhí)行完所有的命令.
[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).陷阱的守護對象是shell進程自己,不會守護shell環(huán)境內(nèi)的子進程.但如果是信號忽略型陷阱,則會守護整個shell進程組使其忽略給定信號.
以下面這個腳本為例,設(shè)置的陷阱會捕獲到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")
執(zhí)行該劇本,并另開一個會話窗口,殺死trap2.sh劇本.
[root@linuxidc ~]# ./trap2.sh [root@linuxidc ~]# killall -s SIGTERM trap2.sh
執(zhí)行成果如下.
結(jié)果中的trap_handle_time證明,腳本所在shell進程收到SIGTERM信號后,trap成功進行了處理.如果細心的話,會發(fā)現(xiàn)trap處理的時間正好是10秒之后,這并不是因為正好10秒之后才發(fā)送SIGTERM信號,而是因為trap就是這么工作的,這是另一個必要注意的點,稍后見下文的(2).
再次執(zhí)行腳本,在另個會話窗口下殺死腳本中正在運行的sleep進程和trap2.sh腳本地點進程.
[root@linuxidc ~]# ./trap2.sh [root@linuxidc ~]# killall -s SIGTERM sleep ;sleep 3; killall -s SIGINT trap2.sh # 另一個會話終端下執(zhí)行此命令
最終將返回如下成果:
time_start: 2017-08-14 12:23:06 Terminated # 接收到對sleep發(fā)送的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
結(jié)果說明腳本中的trap陷阱沒有守護shell內(nèi)的sleep進程,只守護了shell自己.同樣也發(fā)現(xiàn)了,雖然是在3秒后發(fā)送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")
執(zhí)行trap3.sh,并在另一個會話終端下殺死sleep過程.
[root@linuxidc ~]# ./trap3.sh [root@linuxidc ~]# killall -s SIGTERM sleep;sleep 3;killall -s SIGINT sleep # 另一個會話終端下執(zhí)行此命令
成果如下.從時間差可以看出,無論是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中針對某信號設(shè)置了陷阱,則該shell進程接收到該信號時,會等待其內(nèi)正在運行的命令結(jié)束才開始處置陷阱.
其實(1)中的幾個示例的成果已經(jīng)證明了這一點.只要是向shell進程發(fā)送的信號,都會等待當前正在運行的命令結(jié)束后才處理信號,然后繼續(xù)腳本向下運行.
(3).CTRL+C和SIGINT不是等價的.當某一時刻按下CTRL+C,它是在向整個當前運行的進程組發(fā)送SIGINT信號.對shell腳原來說,SIGINT不僅發(fā)送給shell腳本進程,還發(fā)送給腳本中當前正在運行的進程.
所以,如果shell中設(shè)置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發(fā)送信號,正常情況下trap會在當前運行的sleep進程完成后才進行相關(guān)處理.但如果是按下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
結(jié)果中顯示,兩次按下CTRL+C后,不僅sleep立刻結(jié)束了,trap也立即進行處理了.這闡明CTRL+C不僅讓腳本進程收到了SIGINT信號,也讓當前正在運行的進程收到了SIGINT信號.
必要特別說明的是,如果當前正在運行的進程處在循環(huán)內(nèi),當該進程收到了終止進程后,僅僅只是立即終止當次進程,而不會終止整個循環(huán),也就是說,它還會繼續(xù)向下執(zhí)行后續(xù)命令并進入下一個循環(huán).如果此時是使用CTRL+C發(fā)送SIGINT,則每次CTRL+C時,trap也會一次次進行處理.
注意點(1)(2)(3)很重要,因為搞清楚了它們,才能明白腳本中當前正在運行的進程是先完成還是立即結(jié)束,這在寫復雜腳本或任務型腳本極其重要.例如大量文檔中www.example.com必要替換成www.example.net,假如使用sed進行處理,我們肯定不希望替換了一部分文件的時候被臨時終止.
(4).每個陷阱都有守護規(guī)模.每一個陷阱只將守護它后面的所有進程,直到遇到下一個相同信號的陷阱.
以shell劇本為例,如下圖所示.
(5).當shell環(huán)境下設(shè)置了信號忽略陷阱時,子shell在啟動時將繼承該陷阱,且這些信號忽略陷阱弗成再改變或重置.信號忽略陷阱是子shell唯一繼承的陷阱類型.
先在當前shell環(huán)境下設(shè)置一個疏忽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
執(zhí)行成果如下.
[root@linuxidc ~]# ./trap6.sh old_trap:-------- trap -- '' SIGINT new_trap:-------- trap -- '' SIGINT trap -- 'echo haha' SIGTERM reset trap:------ trap -- '' SIGINT
從結(jié)果中可以看出,啟動腳本時,父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信號,當設(shè)置了EXIT陷阱時,每次exit的時候都會被捕獲,并做相關(guān)處理.
ERR陷阱是在設(shè)置了"set -e"時生效的,當設(shè)置了"set -e"選項,每次遇到非0退出狀態(tài)碼時會退出當前shell,如果寫在腳本中,就是退出腳本.有了它就不用再在腳本中書寫對"$?"是否(不)等于0的判斷語句,不過它主要用于避免腳本中產(chǎn)生錯誤時,錯誤被滾雪球式的不斷放大.很多人將這一設(shè)置當作寫shell腳本的一項行為規(guī)范,但我個人不完全認同,很多時候非0退出狀態(tài)碼是無關(guān)緊要的,甚至有時候非0狀態(tài)碼才是繼續(xù)執(zhí)行的需要條件.
回到話題上.先看看"set -e"的效果.以下面的腳本為例,在腳本中,mv命令少給了一個參數(shù),它是差錯命令,返回的是非0狀態(tài)碼.
[root@linuxidc ~]# vim trap8.sh #!/bin/bash set -e echo "right here" mv ~/a.txt [ "$?" -eq 0 ] && echo "right again" || echo "wrong here"
如果不設(shè)置"set -e",那么會被下一條語句判斷,但因為設(shè)置了"set -e",使得在mv錯誤發(fā)生時,就立即退出腳本所在的shell.也便是說,對"$?"的判斷語句根本便是多余的.結(jié)果如下.
[root@linuxidc ~]# ./trap8.sh right here mv: missing destination file operand after ‘/root/a.txt’ Try 'mv --help' for more information.
可以設(shè)置ERR陷阱,專門捕捉"set -e"起作用時的信號.例如,當命令錯誤時,做一些臨時文件清理動作等.注意,當捕捉到了ERR信號時,腳本不會再繼續(xù)向下運行,而是trap處理結(jié)束后就立即退出.例如:
[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
執(zhí)行成果如下:
[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變量記載的是當前正在執(zhí)行的命令行,如果是用在陷阱中,則記載的是陷阱觸發(fā)時正在運行的命令行.LINENO記載的是正在執(zhí)行的命令所處行號.
例如:
[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
執(zhí)行成果.
[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在腳本中的作用之一是在突然被中斷時清理一些臨時文件然后退出,雖然它會等待腳本中當前正在運行的命令結(jié)束,然后清理并退出.但是,很多時候會在腳本中使用后臺進程,以加快腳本的速度.而后臺進程是獨立掛靠在init/systemd下的,所以它不受終端以及shell環(huán)境的影響.換句話說,當腳本突然被中斷時,即使陷阱捕捉到了該信號,并清理了臨時文件后退出,但是那些腳本中啟動的后臺進程還會繼續(xù)運行.
這就給腳本帶來了一些不可預測性,一個健壯的腳本必須能夠正確處理這種情況.trap可以實現(xiàn)比較好的解決這種問題,辦法是在trap的命令行中加上向后臺進程發(fā)送信號的語句,然后再退出.
以下面的劇本為例.
[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放入后臺運行.正常情況下,該腳本執(zhí)行5秒后就會退出,但在20秒后后臺過程sleep才會結(jié)束,即使突然發(fā)送中斷信號TERM觸發(fā)trap也一樣.
于是現(xiàn)在的目標是,在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
執(zhí)行該腳本,并在另一個會話窗口發(fā)送SIGTERM信號給該腳本過程.
[root@linuxidc ~]# ./trap10.sh ; ps aux | grep sleep [root@linuxidc ~]# kill trap10.sh # 另一個會話窗口執(zhí)行
執(zhí)行成果如下.可見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
本文永遠更新鏈接地址:
維易PHP培訓學院每天發(fā)布《LINUX學習:為shell布置陷阱:trap捕捉信號方法論》等實戰(zhàn)技能,PHP、MYSQL、LINUX、APP、JS,CSS全面培養(yǎng)人才。
轉(zhuǎn)載請注明本頁網(wǎng)址:
http://www.snjht.com/jiaocheng/8850.html