如何在 Bash 腳本中使用 Linux 信號
已發表: 2022-08-09
Linux 內核向進程發送有關它們需要做出反應的事件的信號。 表現良好的腳本優雅而穩健地處理信號,即使您按下 Ctrl+C 也可以自行清理。 就是這樣。
信號和過程
信號是發送到腳本、程序和守護進程等進程的短而快速的單向消息。 他們讓流程知道已經發生的事情。 用戶可能按下了 Ctrl+C,或者應用程序可能試圖寫入它無權訪問的內存。
如果進程的作者已經預料到某個信號可能會發送給它,他們可以在程序或腳本中編寫一個例程來處理該信號。 這樣的例程稱為信號處理程序。 它捕獲或捕獲信號,並執行一些動作來響應它。
正如我們將看到的,Linux 使用了很多信號,但從腳本的角度來看,只有一小部分信號是您可能感興趣的。特別是在非平凡的腳本中,信號告訴要關閉的腳本應該被捕獲(在可能的情況下)並執行正常關閉。
例如,創建臨時文件或打開防火牆端口的腳本可以有機會刪除臨時文件或在端口關閉之前關閉端口。 如果腳本在收到信號的那一刻就死了,您的計算機可能會處於不可預測的狀態。
下面介紹如何在自己的腳本中處理信號。
滿足信號
一些 Linux 命令具有神秘的名稱。 捕獲信號的命令並非如此。 這叫trap
。 我們還可以使用帶有-l
(列表)選項的trap
來向我們顯示 Linux 使用的整個信號列表。
陷阱-l
儘管我們的編號列表以 64 結束,但實際上有 62 個信號。 信號 32 和 33 丟失。 它們沒有在 Linux 中實現。 它們已被gcc
編譯器中用於處理實時線程的功能所取代。 從信號 34 SIGRTMIN
到信號 64 SIGRTMAX
的所有內容都是實時信號。
您會在不同的類 Unix 操作系統上看到不同的列表。 例如,在 OpenIndiana 上,存在信號 32 和 33,以及一堆額外的信號,使總數達到 73。
信號可以通過名稱、編號或它們的簡稱來引用。 他們的簡稱就是他們的名字,去掉了前面的“SIG”。
發出信號的原因有很多。 如果你能破譯它們,它們的目的就包含在它們的名字中。 信號的影響屬於以下幾類之一:
- 終止:進程終止。
- 忽略:信號不影響進程。 這是一個僅供參考的信號。
- 核心:創建轉儲核心文件。 這通常是因為進程以某種方式越界,例如內存違規。
- 停止:進程停止。 也就是說,它是暫停的,而不是終止的。
- 繼續:告訴停止的進程繼續執行。
這些是您最常遇到的信號。
- SIGHUP :信號 1. 與遠程主機(例如 SSH 服務器)的連接意外斷開或用戶已註銷。 接收到此信號的腳本可能會正常終止,或者可能選擇嘗試重新連接到遠程主機。
- SIGINT : Signal 2. 用戶按下了 Ctrl+C 組合來強制關閉進程,或者
kill
命令已與信號 2 一起使用。從技術上講,這是一個中斷信號,不是終止信號,而是一個沒有中斷的腳本信號處理程序通常會終止。 - SIGQUIT : 信號 3。用戶按下了 Ctrl+D 組合來強制退出進程,或者
kill
命令與信號 3 一起使用。 - SIGFPE :信號 8。進程試圖執行非法(不可能)的數學運算,例如除以零。
- SIGKILL :信號 9。這是相當於斷頭台的信號。 你無法捕捉或忽略它,它會立即發生。 該過程立即終止。
- SIGTERM :信號 15。這是
SIGKILL
更體貼的版本。SIGTERM
還告訴進程終止,但它可以被捕獲並且進程可以在關閉之前運行其清理進程。 這允許正常關閉。 這是kill
命令發出的默認信號。
命令行上的信號
捕獲信號的一種方法是使用帶有信號編號或名稱的trap
,以及在收到信號時您希望發生的響應。 我們可以在終端窗口中演示這一點。
此命令捕獲SIGINT
信號。 響應是將一行文本打印到終端窗口。 我們將-e
(啟用轉義)選項與echo
一起使用,因此我們可以使用“ \n
”格式說明符。
陷阱 'echo -e "\nCtrl+c 檢測到。"' SIGINT
每次按下 Ctrl+C 組合時,都會打印我們的文本行。
要查看是否在信號上設置了陷阱,請使用-p
(打印陷阱)選項。
陷阱 -p SIGINT
使用不帶選項的trap
可以做同樣的事情。
要將信號重置為其未捕獲的正常狀態,請使用連字符“ -
”和捕獲信號的名稱。
陷阱 - SIGINT
陷阱 -p SIGINT
trap -p
命令沒有輸出表明該信號上沒有設置陷阱。
在腳本中捕獲信號
我們可以在腳本中使用相同的通用格式trap
命令。 此腳本捕獲三個不同的信號SIGINT
、 SIGQUIT
和SIGTERM
。
#!/bin/bash 陷阱“回顯我已終止 SIGINT;退出”SIGINT 陷阱“回顯我被 SIGQUIT 終止;退出”SIGQUIT 陷阱“回顯我被 SIGTERM 終止;退出”SIGTERM 迴聲$$ 計數器=0 雖然是真的 做 echo "循環編號:" $((++counter)) 睡覺 1 完畢
三個trap
語句位於腳本的頂部。 請注意,我們在每個信號的響應中都包含了exit
命令。 這意味著腳本會對信號做出反應,然後退出。
將文本複製到編輯器中並將其保存在名為“simple-loop.sh”的文件中,並使用chmod
命令使其可執行。 如果您想在自己的計算機上進行操作,則需要對本文中的所有腳本執行此操作。 只需在每種情況下使用相應腳本的名稱即可。
chmod +x simple-loop.sh

腳本的其餘部分非常簡單。 我們需要知道腳本的進程 ID,所以我們讓腳本回顯給我們。 $$
變量保存腳本的進程 ID。
我們創建一個名為counter
的變量並將其設置為零。
while
循環將永遠運行,除非它被強制停止。 它遞增counter
變量,將其回顯到屏幕上,然後休眠一秒鐘。
讓我們運行腳本並向它發送不同的信號。
./simple-loop.sh
當我們點擊“Ctrl+C”時,我們的消息會打印到終端窗口並且腳本會終止。
讓我們再次運行它並使用kill
命令發送SIGQUIT
信號。 我們需要從另一個終端窗口執行此操作。 您需要使用您自己的腳本報告的進程 ID。
./simple-loop.sh
殺死-SIGQUIT 4575
正如預期的那樣,腳本報告信號到達然後終止。 最後,為了證明這一點,我們將使用SIGTERM
信號再次執行此操作。
./simple-loop.sh
殺死-SIGTERM 4584
我們已經驗證我們可以在一個腳本中捕獲多個信號,並獨立地對每個信號做出反應。 將所有這些從有趣變為有用的步驟是添加信號處理程序。
在腳本中處理信號
我們可以將響應字符串替換為腳本中的函數名稱。 然後,當檢測到信號時, trap
命令會調用該函數。
將此文本複製到編輯器中並將其保存為名為“grace.sh”的文件,並使用chmod
使其可執行。
#!/bin/bash 陷阱 graceful_shutdown SIGINT SIGQUIT SIGTERM 優雅的關機() { echo -e "\n刪除臨時文件:" $temp_file rm -rf "$temp_file" 出口 } temp_file=$(mktemp -p /tmp tmp.XXXXXXXXXX) echo "創建的臨時文件:" $temp_file 計數器=0 雖然是真的 做 echo "循環編號:" $((++counter)) 睡覺 1 完畢
該腳本使用單個trap
語句為三個不同的信號( SIGHUP
、 SIGINT
和SIGTERM
)設置陷阱。 響應是graceful_shutdown()
函數的名稱。 每當接收到三個捕獲信號之一時,都會調用該函數。
該腳本使用mktemp
在“/tmp”目錄中創建一個臨時文件。 文件名模板是“tmp.XXXXXXXXXX”,所以文件名是“tmp”。 後跟十個隨機字母數字字符。 文件名在屏幕上回顯。
腳本的其餘部分與前一個相同,帶有一個counter
變量和一個無限的while
循環。
./grace.sh
當文件被發送一個導致它關閉的信號時, graceful_shutdown()
函數被調用。 這將刪除我們的單個臨時文件。 在實際情況下,它可以執行腳本所需的任何清理工作。
此外,我們將所有捕獲的信號捆綁在一起,並用一個函數處理它們。 您可以單獨捕獲信號並將它們發送到它們自己的專用處理函數。
複製此文本並將其保存在名為“triple.sh”的文件中,並使用chmod
命令使其可執行。
#!/bin/bash 陷阱 sigint_handler SIGINT 陷阱 sigusr1_handler SIGUSR1 陷阱 exit_handler 退出 函數 sigint_handler() { ((++sigint_count)) echo -e "\nSIGINT 收到 $sigint_count 次。" 如果 [[ "$sigint_count" -eq 3 ]]; 然後 echo "開始關閉。" loop_flag=1 菲 } 函數 sigusr1_handler() { echo "SIGUSR1 發送和接收 $((++sigusr1_count)) 次。" } 函數 exit_handler() { echo "退出處理程序:腳本正在關閉..." } 迴聲$$ sigusr1_count=0 sigint_count=0 loop_flag=0 而 [[ $loop_flag -eq 0 ]]; 做 殺死-SIGUSR1 $$ 睡覺 1 完畢
我們在腳本頂部定義了三個陷阱。
- 一個陷阱
SIGINT
並有一個名為sigint_handler()
的處理程序。 - 第二個捕獲一個名為
SIGUSR1
的信號並使用一個名為sigusr1_handler()
的處理程序。 - 第三個陷阱捕獲
EXIT
信號。 該信號在腳本關閉時由腳本本身發出。 為EXIT
設置信號處理程序意味著您可以設置一個在腳本終止時始終調用的函數(除非它被信號SIGKILL
殺死)。 我們的處理程序稱為exit_handler()
。
SIGUSR1
和SIGUSR2
是提供的信號,以便您可以向腳本發送自定義信號。 您如何解釋和對它們做出反應完全取決於您。
暫時將信號處理程序放在一邊,您應該熟悉腳本的主體。 它將進程 ID 回顯到終端窗口並創建一些變量。 變量sigusr1_count
記錄了SIGUSR1
被處理的次數, sigint_count
記錄了SIGINT
被處理的次數。 loop_flag
變量設置為零。
while
循環不是無限循環。 如果loop_flag
變量設置為任何非零值,它將停止循環。 while
循環的每次旋轉都使用kill
將SIGUSR1
信號發送到此腳本,方法是將其發送到腳本的進程 ID。 腳本可以向自己發送信號!
sigusr1_handler()
函數增加sigusr1_count
變量並向終端窗口發送消息。
每次收到SIGINT
信號時, siguint_handler()
函數都會增加sigint_count
變量並將其值回顯到終端窗口。
如果sigint_count
變量等於 3,則loop_flag
變量設置為 1,並向終端窗口發送一條消息,讓用戶知道關閉過程已經開始。
因為loop_flag
不再等於 0,所以while
循環終止並且腳本完成。 但是該操作會自動引發EXIT
信號並exit_handler()
函數。
./triple.sh
在按三下 Ctrl+C 後,腳本終止並自動調用exit_handler()
函數。
閱讀信號
通過捕獲信號並在簡單的處理程序函數中處理它們,即使 Bash 腳本意外終止,您也可以讓它們自己整理。 這為您提供了一個更乾淨的文件系統。 它還可以防止您下次運行腳本時出現不穩定情況,並且根據腳本的用途,它甚至可以防止安全漏洞。
相關:如何使用 Lynis 審核 Linux 系統的安全性