Bourne Shell 指令碼/除錯和訊號處理
在前面的章節中,我們已經向你介紹了 Bourne Shell 及其如何使用 shell 的語言編寫指令碼。我們已經盡力包括我們能想到的所有細節,這樣你就可以編寫出最好的指令碼。但是,無論你多麼認真地關注,無論你多麼小心地編寫指令碼,總會有那麼一天,你寫的某些東西就是無法正常工作,無論你多麼確信它應該正常工作。那麼,你該怎麼辦呢?
在本模組中,我們將介紹 Bourne Shell 提供的工具,用來處理這些意外情況。包括你指令碼的意外行為(你需要除錯指令碼才能解決)以及腳本週圍的意外行為(由作業系統向你的指令碼傳送訊號導致)。
所以,現在是深夜,你剛剛完成了一個漫長而複雜的 shell 指令碼,你傾注了三天的心血,只吃咖啡、可樂和披薩...... 它就是無法正常工作。在裡面某個地方,有一個你無法找到的 bug。有些東西出錯了,出現了某種意外行為,或者其他一些事情讓你抓狂。那麼,你該怎麼除錯這個指令碼呢?當然,你可以用大量的“echo”命令來除錯,但有沒有更簡單的方法呢?
一般來說,除錯任何程式最有效的方法是逐語句跟蹤程式的執行過程,以檢視程式究竟在做什麼。最先進的形式(由現代 IDE 提供)允許你透過在特定點停止執行並檢查程式的內部狀態來跟蹤程式。不幸的是,Bourne Shell 沒有那麼先進。但它提供了次好的選擇:命令跟蹤。shell 可以打印出執行的每個命令。
跟蹤功能(有兩個)可以使用“set”命令啟用,或者在呼叫 shell 可執行檔案時直接傳遞引數來啟用。在任何一種情況下,你都可以使用“-x”引數、“-v”引數或兩者。
- -v
- 啟用詳細模式;shell 會在讀取每個命令時列印它。
- -x
- 這將啟用命令跟蹤;shell 會在執行每個命令時列印它。
讓我們考慮以下指令碼
#!/bin/sh
DIVISOR=${1:-0}
echo $DIVISOR
expr 12 / $DIVISOR
讓我們執行這個指令碼,並且不傳遞命令列引數(所以我們使用預設值 0 作為 DIVISOR 變數)
當然,這並不難找出問題所在,但讓我們再仔細看看。讓我們看看 shell 到底執行了什麼,使用-x引數
所以,很明顯,shell 嘗試執行除以零的操作。為了防止我們對零的來源感到困惑,讓我們看看 shell 實際上讀取了哪些命令
$ sh -v divider.sh
DIVISOR=${1:-0}
echo $DIVISOR
0
expr 12 / $DIVISOR
所以很明顯,指令碼讀取了一個帶有變數替換的命令,但它並沒有正常工作。如果我們將這兩個引數結合起來,得到的輸出將告訴我們整個悲慘的故事
$ sh -xv divider.sh
DIVISOR=${1:-0}
+ DIVISOR=0
echo $DIVISOR
+ echo 0
0
expr 12 / $DIVISOR
+ expr 12 / 0
還有一個引數可以用來除錯指令碼,即-n引數。這會導致 shell 讀取命令但不執行它們。你可以使用此引數對指令碼進行語法檢查。
正如你在上一節中看到的,我們透過將引數作為命令列引數傳遞給 shell 可執行檔案來使用 shell 引數。但我們不能將引數放在指令碼本身中嗎?畢竟,裡面有一個直譯器提示... 而且,確實可以這樣做。讓我們稍微修改一下指令碼,然後嘗試一下。
#!/bin/sh -xv
DIVISOR=${1:-0}
echo $DIVISOR
expr 12 / $DIVISOR
$ chmod +x divider.sh
$ ./divider.sh
DIVISOR=${1:-0}
+ DIVISOR=0
echo $DIVISOR
+ echo 0
0
expr 12 / $DIVISOR
+ expr 12 / 0
expr: 除以零
所以,沒有問題。但有一個小陷阱。讓我們再次嘗試執行該指令碼
那麼,除錯去哪裡了呢?好吧,你必須記住,當你嘗試以可執行檔案本身的形式執行指令碼時,會使用直譯器提示。但在上一個示例中,我們沒有這樣做。在上一個示例中,我們自己呼叫了 shell,並將其作為引數傳遞給它。所以 shell 在沒有啟用除錯的情況下執行。如果我們執行“sh -xv divider.sh”,它會正常工作。
那麼,原始碼指令碼(即使用點符號)呢?
這次指令碼是由執行我們互動式 shell 的同一個 shell 程序執行的。同樣的原理也適用:那裡也沒有除錯。因為互動式 shell 並沒有以除錯標誌啟動。但我們也可以解決這個問題;這就是“set”命令的用武之地
$ set -xv
$ . divider.sh
+ . divider.sh
#!/bin/sh -vx
DIVISOR=${1:-0}
++ DIVISOR=0
echo $DIVISOR
++ echo 0
0
expr 12 / $DIVISOR
++ expr 12 / 0
expr: 除以零
現在我們在互動式 shell 中啟用了除錯,並且可以獲得指令碼的完整跟蹤。事實上,我們甚至可以獲得互動式 shell 呼叫指令碼的跟蹤!但現在,如果我們在互動式 shell 中使用除錯啟動一個新的 shell 程序,它會繼承除錯嗎?
好吧,我們肯定獲得了指令碼被呼叫的跟蹤,但沒有指令碼本身的跟蹤。故事的寓意是:除錯時,確保你知道你是在哪個 shell 中激活了跟蹤。
順便說一下,要關閉互動式 shell 中的跟蹤,你可以執行“set +xv”或簡單地執行“set -”。
在編寫或除錯 shell 指令碼時,有時需要在特定點退出(停止指令碼執行)。你可以使用“exit”內建命令來執行此操作。該命令看起來像這樣
如果你省略了可選的退出狀態,指令碼的退出狀態將是呼叫“exit”之前執行的最後一個命令的退出狀態。
例如
#!/bin/sh -x
echo hello
exit 1
如果你執行這個指令碼,然後測試輸出狀態,你會看到(使用“$?”內建變數)
在使用“exit”時需要注意的一點是:“exit”實際上會終止正在執行的程序。因此,如果你正在執行帶有直譯器提示的可執行指令碼,或者顯式呼叫了 shell 並將指令碼作為引數傳遞給它,那麼這沒問題。但如果你已經原始碼指令碼(使用了點符號),那麼你的指令碼將由執行你的互動式 shell 的程序執行。因此,使用“exit”命令可能會意外終止你的 shell 會話並使你登出!
“exit”有一個變體,專門用於不是自身程序的程式碼塊。這就是“return”命令,它與“exit”命令非常相似
return 的語義與 'exit' 完全相同,但主要用於 shell 函式(它使函式返回而不會終止指令碼)。以下是一個示例
#!/bin/sh
sayHello() {
echo 'Hi there!!'
return 2
}
echo 'Hello World!!'
sayHello
echo $?
echo 'Goodbye!!'
exit
如果執行此指令碼,將會看到以下內容
函式以 2 的可測試退出狀態返回。但是,由於指令碼執行的最後一個命令 ('echo Goodbye!!') 退出時沒有錯誤,因此指令碼的總體退出狀態為零。
您也可以使用 'return' 語句退出透過原始碼執行的 shell 指令碼(該指令碼將由執行互動式 shell 的程序執行,因此這不是子程序)。但這通常不是一個好主意,因為這會限制指令碼的原始碼執行方式:如果您嘗試以其他方式執行它,僅使用 'return' 語句會導致錯誤。
語法、命令錯誤或呼叫 'exit' 不是唯一可以阻止指令碼執行的東西。執行指令碼的程序也可能突然從作業系統接收訊號。訊號是一種簡單的事件通知形式:將訊號視為房間裡突然亮起的一盞小燈,以告知您房間外有人需要您的注意。只是不止一盞燈。Unix 系統通常允許使用許多不同的訊號,因此它更像是擁有一堵滿是小燈的牆,每盞燈都可能突然開始閃爍。
在像 MS-DOS 這樣的單程序作業系統上,生活很簡單。環境是單程序的,這意味著您的程式碼(一旦執行)擁有完整的機器控制權。任何到達的訊號始終是硬體中斷(例如,計算機發出訊號表示軟盤已準備好讀取),如果您不需要外部硬體,您可以安全地忽略所有這些訊號;要麼是您不感興趣的某些裝置事件,要麼是出了問題——在這種情況下,計算機無論如何都會崩潰,您無能為力。
在 Unix 系統上,生活並不那麼容易。在 Unix 上,訊號可以來自任何地方(包括其他程式)。您也永遠無法完全控制系統。訊號可能是硬體中斷,也可能是另一個程式發出的訊號,或者可能是厭倦了等待的使用者登入到第二個 shell 會話,現在正在命令您的程序死亡。從好的方面來說,生活仍然不那麼複雜。大多數 Unix 系統(當然還有 Bourne Shell)都為大多數訊號提供了預設處理。通常,您仍然可以安全地忽略訊號並讓 shell 或作業系統處理它們。事實上,如果所討論的訊號是 9 號(鬆散地翻譯:KILL!! KILL!! DIE!! DIE, RIGHT NOW!!),您可能應該忽略它並讓作業系統殺死您的程序。
但有時您只需要進行自己的訊號處理。可能是因為您一直在處理檔案,並且希望在程序死亡之前進行一些清理。或者因為訊號是您的多程序程式設計的一部分(例如,偵聽訊號 16,即“使用者定義的訊號 1”)。這就是 Bourne Shell 為我們提供 'trap' 命令的原因。
trap 命令實際上非常簡單(尤其是如果您曾經做過任何形式的事件驅動程式設計)。本質上,trap 命令表示“如果此程序接收以下訊號之一,請執行此操作”。它看起來像這樣
而 signaln 是要捕獲的訊號。
例如,要捕獲使用者定義的訊號 1(通常稱為 SIGUSR1)並在出現時列印“Hello World”,您需要執行以下操作
$ trap "echo Hello World" 16
大多數 Unix 系統還允許您使用符號名稱(我們稍後會回到這些名稱)。因此,您也可以執行以下操作
$ trap "echo Hello World" SIGUSR1
如果您能做到這一點,您通常也可以做到以下幾點
$ trap "echo Hello World" USR1
傳遞給 'trap' 的命令字串是一個包含命令列表的字串。但它不會被視為命令列表;它只是一個字串,並且只有在捕獲到訊號後才會被解釋。命令字串可以是以下任何內容
- 一個字串
- 包含命令列表的字串。允許使用任何和所有命令,並且您還可以使用以分號分隔的多個命令(即命令列表)。
- ''
- 空字串。實際上這與前一種情況相同,因為這是空的命令字串。這會導致 shell 在捕獲到訊號時不執行任何操作——換句話說,忽略訊號。
- 空,空字串。這會將訊號處理重置為預設訊號操作(通常是“殺死程序”)。
在命令列表之後,您可以列出您希望與該命令列表關聯的任意多個訊號。您以這種方式設定的陷阱對跟隨 'trap' 命令的每個命令都有效。
現在,可能需要檢視一個示例來澄清一下。您可以在任何地方(照常)使用 'trap',包括互動式 shell。但大多數情況下,您希望將陷阱引入指令碼而不是互動式 shell 程序。讓我們建立一個使用 'trap' 命令的簡單指令碼
#!/bin/sh
trap 'echo Hello World' SIGUSR1
while [ 1 -gt 0 ]
do
echo Running....
sleep 5
done
這個指令碼本身是一個無限迴圈,它列印“Running...”然後休眠五秒鐘。但我們添加了一個 'trap' 命令(在迴圈之前,否則陷阱永遠不會被執行,它不會影響迴圈),它在程序接收 SIGUSR1 訊號時列印“Hello World”。因此,讓我們透過執行指令碼來啟動該程序
$ ./trap_signal.sh
Running....
Running....
Running....
Running....
Running....
...
要啟動陷阱,我們必須向正在執行的程序傳送訊號。為此,登入到一個新的 shell 會話,並使用一個程序工具(如 'ps')來查詢正確的程序 ID(PID)
$ ps -ef | grep signal
bzt 10808 10415 0 15:12 pts/1 00:00:00 fgrep signal
現在,要向該程序傳送訊號,我們使用內置於 Bourne Shell 的 'kill' 命令
而 ID 是要傳送訊號的程序的 PID(至少其中一個)
顧名思義,'kill' 的初衷實際上是殺死程序(這與預設訊號為 SIGTERM 以及預設訊號處理程式為終止相符)。但實際上它做的只是向程序傳送訊號。因此,例如,我們可以像這樣向我們的程序傳送 SIGUSR1
kill -SIGUSR1 10865
Running....
Running....
Running....
Running....
Running....
Hello World
Running....
Running....
...
您可能會注意到,在“Hello World!”出現之前有一個短暫的暫停;它不會在執行的 'sleep' 命令完成之前出現。但之後,它就出現了。但您可能有點驚訝:訊號並沒有殺死程序。這是因為 'trap'完全用您設定的命令替換了訊號處理程式。而單獨的 'echo Hello World' 不會殺死程序...這裡的教訓很簡單:如果您希望您的訊號陷阱終止程序,請確保您包含一個 'exit' 命令。
在命令列表中包含多個命令以及可能捕獲許多訊號的情況下,您可能擔心 'trap' 語句可能會變得很亂。幸運的是,您還可以使用 shell 函式作為 'trap' 中的命令。以下示例說明了這一點以及退出事件處理程式和非退出事件處理程式之間的區別
#!/bin/sh
exit_with_grace() {
echo Goodbye World
exit
}
trap "exit_with_grace" USR1 TERM QUIT
trap "echo Hello World" USR2
while [ 1 -gt 0 ]
do
echo Running....
sleep 5
done
以下是 POSIX-1003 2001 版標準中對訊號的正式定義
A mechanism by which a process or thread may be notified of, or affected by, an event occurring in the system.
Examples of such events include hardware exceptions and specific actions by processes.
The term signal is also used to refer to the event itself.
換句話說,訊號是從一個程序(可能是系統程序)傳送到另一個程序的某種簡短訊息。但這究竟意味著什麼?訊號是什麼樣的?上面給出的定義有點含糊...
如果您對在計算中給出含糊定義時會發生什麼有任何瞭解,您已經知道上面問題的答案:每個開發的 Unix 版本都提出了自己對“訊號”的定義。它們幾乎都選擇了由一個整陣列成(因為這很簡單)的訊息,但並非所有地方都完全相同。然後進行了一些標準化,Unix 系統將自己組織成 System V 和 BSD 版本,最後每個人都同意以下定義
The system signals are the signals listed in /usr/include/sys/signal.h .
天啊,這太有幫助了...
從那時起發生了很多事情,包括 POSIX-1003 標準的定義。該標準標準化了大多數 Unix 介面(包括第 1 部分 (1003.1) 中的 shell),最終提出了一個標準的符號訊號名稱列表和預設處理程式。因此,通常情況下,現在您可以利用該列表並期望您的指令碼在大多數系統上都能正常工作。只是要注意,它並不完全萬無一失...
POSIX-1003 定義了以下表格中列出的訊號。給出的值是典型數值,但不是強制性的,您不應該依賴它們(但另一方面,您使用符號值是為了不使用實際值)。
| 訊號 | 預設操作 | 描述 | 典型值 |
|---|---|---|---|
| SIGABRT | 中止並轉儲核心檔案 | 中止程序並生成核心轉儲 | 6 |
| SIGALRM | 終止 | 鬧鐘。 | 14 |
| SIGBUS | 中止並轉儲核心檔案 | 訪問記憶體物件的未定義部分。 | 7, 10 |
| SIGCHLD | 忽略 | 子程序終止、停止 | 20, 17, 18 |
| SIGCONT | 繼續程序(如果已停止) | 繼續執行,如果已停止。 | 19,18,25 |
| SIGFPE | 中止並轉儲核心檔案 | 錯誤的算術運算。 | 8 |
| SIGHUP | 終止 | 掛起。 | 1 |
| SIGILL | 中止並轉儲核心檔案 | 非法指令。 | 4 |
| SIGINT | 終止 | 終端中斷訊號。 | 2 |
| SIGKILL | 終止 | 殺死(無法捕獲或忽略)。 | 9 |
| SIGPIPE | 終止 | 寫入沒有讀取者的管道(即斷開管道)。 | 13 |
| SIGQUIT | 終止 | 終端退出訊號。 | 3 |
| SIGSEGV | 中止並轉儲核心檔案 | 無效的記憶體引用。 | 11 |
| SIGSTOP | 停止程序 | 停止執行(無法捕獲或忽略)。 | 17,19,23 |
| SIGTERM | 終止 | 終止訊號。 | 15 |
| SIGTSTP | 停止程序 | 終端停止訊號。 | 18,20,24 |
| SIGTTIN | 停止程序 | 後臺程序嘗試讀取。 | 21,21,26 |
| SIGTTOU | 停止程序 | 後臺程序嘗試寫入。 | 22,22,27 |
| SIGUSR1 | 終止 | 使用者定義訊號 1。 | 30,10,16 |
| SIGUSR2 | 終止 | 使用者定義訊號 2。 | 31,12,17 |
| SIGPOLL | 終止 | 可輪詢事件。 | - |
| SIGPROF | 終止 | 效能分析計時器超時。 | 27,27,29 |
| SIGSYS | 中止並轉儲核心檔案 | 錯誤的系統呼叫。 | 12 |
| SIGTRAP | 中止並轉儲核心檔案 | 跟蹤/斷點陷阱 | 5 |
| SIGURG | 忽略 | 套接字上有高頻寬資料可用。 | 16,23,21 |
| SIGVTALRM | 終止 | 虛擬計時器超時。 | 26,28 |
| SIGXCPU | 中止並轉儲核心檔案 | CPU 時間限制已超過。 | 24,30 |
| SIGXFSZ | 中止並轉儲核心檔案 | 檔案大小限制已超過。 | 25,31 |
之前我們談到了作業控制以及掛起和恢復作業。作業掛起和恢復實際上完全基於向程序傳送訊號,因此實際上可以使用 'kill' 和訊號列表完全控制作業停止和啟動。要掛起程序,請向其傳送 SIGSTOP 訊號。要恢復,請向其傳送 SIGCONT 訊號。
如果您線上閱讀有關 'trap' 的內容,您可能會遇到另一種稱為 **ERR** 的“訊號”。它與 'trap' 的使用方式與常規訊號相同,但實際上它根本不是訊號。它用於捕獲命令錯誤(即非零退出狀態),例如
$ trap 'echo HELLO WORLD' ERR
$ expr 1 / 0
HELLO WORLD
那麼為什麼我們在討論 'trap' 時沒有早點介紹這個“訊號”呢?嗯,我們把它留到系統訊號和非系統訊號的討論中是有原因的:ERR 根本不是標準。它是由 Korn Shell 新增的,以使生活更輕鬆,但沒有被 POSIX 標準採用,它肯定不是原始 Bourne Shell 的一部分。因此,如果您使用它,請記住您的指令碼可能不再可移植。