關於Linux Shell的訊號trap功能你必須知道的細節

perfychi發表於2013-08-16

From: http://blog.robotshell.org/2012/necessary-details-about-signal-trap-in-shell/

訊號處理(Signal Handling)在 Linux 程式設計中一直扮演者重要的角色,幾乎每個系統工具都要用到它,最常見的功能莫過於用訊號進行程式間通訊(尤其是父子程式)以及捕捉SIGINT、 SIGTERM之類的退出訊號以做一些善後處理(cleanup)。C中自不必多說,可以使用 wait 族函式;而 shell 指令碼中也有捕捉訊號的 trap 功能——然而許多人在使用 trap 功能的時候卻存在著這樣那樣的誤解,這些看似無關緊要的小細節最後有可能使得你的指令碼與你預想的行為完全不同。

如無特殊說明,下文所指 shell 均以 Bash 為例。

0. trap 的使用簡介

雖然我很想說這些應當要自己看 manpage ,但考慮到也許正在讀文章的你手邊沒有 Linux ,還是簡單說一下吧。

1
USAGE: trap [action condition ...]

即當捕捉到 condition 列表所對應的任何一個訊號時,執行 action 動作(使用 eval action 來執行,故 action 可以是 shell 內建指令、外部命令及指令碼中的函式等)。action 還可是”"(空)、’-'等,分別代表忽略相應訊號及重置相應訊號為預設行為。

1. condition 的標準格式是什麼?

condition 中的訊號到底應該如何書寫?比如終端中斷訊號(一般用 CTRL-C 發出),到底是寫 SIGINT 、 INT 還是2(大部分系統上該訊號對應的訊號數)?是大寫還是小寫?

如果你使用最新版的 Bash ,那麼這幾種寫法都可以。而如果你需要在不同 shell 中保持可移植性,請使用大寫、不帶字首的 INT !根據 POSIX 標準, trap 的 condition 不應當加上 SIG 字首,且必須全大寫,允許帶 SIG 字首或小寫是某些 shell 的擴充套件功能。而訊號數在不同的系統上可能不同,所以也不是一個好主意。

2. trap 必須放在第一行麼?

許多資料,尤其是中文資料中不容申辯地指明—— trap 必須放在指令碼中第一個非註釋行。事實果真如此麼?

不論是 manpage 還是 POSIX 文件中,我都沒有找到任何與之相關的說明。甚至在 TLDP 的 中,多個例子都分明把 trap 放在了指令碼的中間。最後我在中找到了下面這句經常被誤讀的話:

Normally, all traps are set before other executable code in the shell script. is encountered, i.e., at the beginning of the shell script.

果然,這只是一個為了保證訊號鉤子儘早被設立的一個設計慣例罷了。事實上, trap 可以根據你的需要放在指令碼中的任何位置。指令碼中也可以有多個 trap ,可以為不同的訊號定義不同的行為,或是修改、刪除已定義的 trap 。更進一步地, trap 也有作用範圍,你可以把它放在函式中,它將只在這個函式里起效!你看,其實 trap 的行為是很符合 UNIX 的慣例的。

3. 訊號究竟是在什麼時候被 trap 處理?

這是本文最重要的一點。訊號到底是什麼時候被處理的?更準確地說,比如指令碼正在執行某個命令時收到了某個訊號,那麼它會被立即處理,還是要等待當前命令完成?

我不打算直接說明答案。為了讓我們對這個問題有更透徹的理解,讓我們來做一下實驗。看下面這個時常被用來講解 trap 的指令碼:

1
2
3
#!/bin/bash
trap 'echo "INTERRUPTED!"; exit' INT
sleep 100

大多數教程都是這麼做的,執行這個指令碼,按下 CTRL-C 。你看到了什麼?指令碼打出了“INTERRUPTED!”並停止了執行。這看起來似乎很正常、很直覺——以此看來, trap 會立即捕捉到訊號並執行,不管當前正在執行的命令。許多指令碼也正是在這個假設下寫的。

然而真的是這樣麼?讓我們做另一個實驗——在一個終端執行這個指令碼,並開啟另一個終端,用ps -ef|grep bash找到這個指令碼的程式號,然後用kill -SIGINT pid向這個程式傳送 SIGINT 訊號。你在原先的終端中看到了什麼?沒有任何反應!如果你願意等上100秒,你最終會看到“INTERRUPTED!”被輸出。這樣看來 trap 是等到當前命令結束以後再處理訊號。

這樣的矛盾究竟是為什麼?問題其實出在 CTRL-C 身上。 Bash 等終端的預設行為是這樣的:當按下 CTRL-C 之後,它會向當前的整個程式組發出 SIGINT 訊號。而 sleep 是由當前指令碼呼叫的,是這個指令碼的子程式,預設是在同一個程式組的,所以也會收到 SIGINT 並停止執行,返回主程式以後 trap 捕捉到了訊號

給了我們一個更準確的說明——如果當前正有一個外部命令在前臺執行,那麼 trap 會等待當前命令結束以後再處理訊號佇列中的訊號。(而許多教程出錯的另一個原因就是——某些 shell 中 sleep 是內建命令,會被打斷。)

那麼上文的例子應當要如何寫才能達到想要的效果呢?有兩種方法:一、把 sleep 放到後臺進行,再用內建的 wait 去等待其執行結束(詳見上一段提到的那篇文件);二、暴力一點,把一長段 sleep 拆成一秒的小 sleep 的迴圈,這在對精度要求不高的情況下也是一個可行的辦法(這應該不用寫範例了吧?)

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/27042095/viewspace-768698/,如需轉載,請註明出處,否則將追究法律責任。

相關文章