如何在 Bash 腳本中使用 Linux 信號

已發表: 2022-08-09
顯示 bash 提示符的 Linux 筆記本電腦
fatmawati achmad zaenuri/Shutterstock.com

Linux 內核向進程發送有關它們需要做出反應的事件的信號。 表現良好的腳本優雅而穩健地處理信號,即使您按下 Ctrl+C 也可以自行清理。 就是這樣。

信號和過程

信號是發送到腳本、程序和守護進程等進程的短而快速的單向消息。 他們讓流程知道已經發生的事情。 用戶可能按下了 Ctrl+C,或者應用程序可能試圖寫入它無權訪問的內存。

如果進程的作者已經預料到某個信號可能會發送給它,他們可以在程序或腳本中編寫一個例程來處理該信號。 這樣的例程稱為信號處理程序。 它捕獲或捕獲信號,並執行一些動作來響應它。

如何從 Linux 終端管理進程:你需要知道的 10 個命令
相關如何從 Linux 終端管理進程:你需要知道的 10 個命令

正如我們將看到的,Linux 使用了很多信號,但從腳本的角度來看,只有一小部分信號是您可能感興趣的。特別是在非平凡的腳本中,信號告訴要關閉的腳本應該被捕獲(在可能的情況下)並執行正常關閉。

例如,創建臨時文件或打開防火牆端口的腳本可以有機會刪除臨時文件或在端口關閉之前關閉端口。 如果腳本在收到信號的那一刻就死了,您的計算機可能會處於不可預測的狀態。

下面介紹如何在自己的腳本中處理信號。

滿足信號

一些 Linux 命令具有神秘的名稱。 捕獲信號的命令並非如此。 這叫trap 。 我們還可以使用帶有-l (列表)選項的trap來向我們顯示 Linux 使用的整個信號列表。

 陷阱-l 

使用 trap -l 列出 Ubuntu 中的信號

儘管我們的編號列表以 64 結束,但實際上有 62 個信號。 信號 32 和 33 丟失。 它們沒有在 Linux 中實現。 它們已被gcc編譯器中用於處理實時線程的功能所取代。 從信號 34 SIGRTMIN到信號 64 SIGRTMAX的所有內容都是實時信號。

您會在不同的類 Unix 操作系統上看到不同的列表。 例如,在 OpenIndiana 上,存在信號 32 和 33,以及一堆額外的信號,使總數達到 73。

使用 trap -l 列出 OpenIndiana 中的信號

信號可以通過名稱、編號或它們的簡稱來引用。 他們的簡稱就是他們的名字,去掉了前面的“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

每次按下 Ctrl+C 組合時,都會打印我們的文本行。

要查看是否在信號上設置了陷阱,請使用-p (打印陷阱)選項。

 陷阱 -p SIGINT 

檢查是否在信號上設置了陷阱

使用不帶選項的trap可以做同樣的事情。

要將信號重置為其未捕獲的正常狀態,請使用連字符“ - ”和捕獲信號的名稱。

 陷阱 - SIGINT
 陷阱 -p SIGINT 

從信號中移除陷阱

trap -p命令沒有輸出表明該信號上沒有設置陷阱。

在腳本中捕獲信號

我們可以在腳本中使用相同的通用格式trap命令。 此腳本捕獲三個不同的信號SIGINTSIGQUITSIGTERM

 #!/bin/bash

陷阱“回顯我已終止 SIGINT;退出”SIGINT
陷阱“回顯我被 SIGQUIT 終止;退出”SIGQUIT
陷阱“回顯我被 SIGTERM 終止;退出”SIGTERM

迴聲$$
計數器=0

雖然是真的
做 
  echo "循環編號:" $((++counter))
  睡覺 1
完畢

三個trap語句位於腳本的頂部。 請注意,我們在每個信號的響應中都包含了exit命令。 這意味著腳本會對信號做出反應,然後退出。

將文本複製到編輯器中並將其保存在名為“simple-loop.sh”的文件中,並使用chmod命令使其可執行。 如果您想在自己的計算機上進行操作,則需要對本文中的所有腳本執行此操作。 只需在每種情況下使用相應腳本的名稱即可。

 chmod +x simple-loop.sh 

使用 chmod 使腳本可執行

腳本的其餘部分非常簡單。 我們需要知道腳本的進程 ID,所以我們讓腳本回顯給我們。 $$變量保存腳本的進程 ID。

我們創建一個名為counter的變量並將其設置為零。

while循環將永遠運行,除非它被強制停止。 它遞增counter變量,將其回顯到屏幕上,然後休眠一秒鐘。

讓我們運行腳​​本並向它發送不同的信號。

 ./simple-loop.sh 

標識它的腳本已被 Ctrl+C 終止

當我們點擊“Ctrl+C”時,我們的消息會打印到終端窗口並且腳本會終止。

讓我們再次運行它並使用kill命令發送SIGQUIT信號。 我們需要從另一個終端窗口執行此操作。 您需要使用您自己的腳本報告的進程 ID。

 ./simple-loop.sh
 殺死-SIGQUIT 4575 

標識它的腳本已被 SIGQUIT 終止

正如預期的那樣,腳本報告信號到達然後終止。 最後,為了證明這一點,我們將使用SIGTERM信號再次執行此操作。

 ./simple-loop.sh
 殺死-SIGTERM 4584 

標識它的腳本已用 SIGTERM 終止

我們已經驗證我們可以在一個腳本中捕獲多個信號,並獨立地對每個信號做出反應。 將所有這些從有趣變為有用的步驟是添加信號處理程序。

在腳本中處理信號

我們可以將響應字符串替換為腳本中的函數名稱。 然後,當檢測到信號時, 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語句為三個不同的信號( SIGHUPSIGINTSIGTERM )設置陷阱。 響應是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()

SIGUSR1SIGUSR2是提供的信號,以便您可以向腳本發送自定義信號。 您如何解釋和對它們做出反應完全取決於您。

暫時將信號處理程序放在一邊,您應該熟悉腳本的主體。 它將進程 ID 回顯到終端窗口並創建一些變量。 變量sigusr1_count記錄了SIGUSR1被處理的次數, sigint_count記錄了SIGINT被處理的次數。 loop_flag變量設置為零。

while循環不是無限循環。 如果loop_flag變量設置為任何非零值,它將停止循環。 while循環的每次旋轉都使用killSIGUSR1信號發送到此腳本,方法是將其發送到腳本的進程 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 

使用 SIGUSR1 的腳本,需要三個 Ctrl+C 組合才能關閉,並在關閉時捕獲 EXIT 信號

在按三下 Ctrl+C 後,腳本終止並自動調用exit_handler()函數。

閱讀信號

通過捕獲信號並在簡單的處理程序函數中處理它們,即使 Bash 腳本意外終止,您也可以讓它們自己整理。 這為您提供了一個更乾淨的文件系統。 它還可以防止您下次運行腳本時出現不穩定情況,並且根據腳本的用途,它甚至可以防止安全漏洞。

相關:如何使用 Lynis 審核 Linux 系統的安全性