linux shell trap

信號處理(Signal Handling)在 Linux 編程中一直扮演者重要的角色,幾乎每一個系統工具都要用到它,最多見的功能莫過於用信號進行進程間通訊(尤爲是父子進程)以及捕捉SIGINT、SIGTERM之類的退出信號以作一些善後處理(cleanup)。C中自沒必要多說,可使用 wait 族函數;而 shell 腳本中也有捕捉信號的 trap 功能——然而許多人在使用 trap 功能的時候卻存在着這樣那樣的誤解,這些看似可有可無的小細節最後有可能使得你的腳本與你預想的行爲徹底不一樣。html

 

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

0. trap 的使用簡介

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

1
USAGE:  trap  [action condition ... ]

即當捕捉到 condition 列表所對應的任何一個信號時,執行 action 動做(使用 evalaction 來執行,故 action 能夠是 shell 內建指令、外部命令及腳本中的函數等)。action 還但是」"(空)、’-'等,分別表明忽略相應信號及重置相應信號爲默認行爲。bash

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

condition 中的信號到底應該如何書寫?好比終端中斷信號(通常用 CTRL-C 發出),究竟是寫 SIGINT 、 INT 仍是2(大部分系統上該信號對應的信號數)?是大寫仍是小寫?ide

若是你使用最新版的 Bash ,那麼這幾種寫法均可以。而若是你須要在不一樣 shell 中保持可移植性,請使用大寫、不帶前綴的 INT !根據 POSIX 標準, trap 的 condition 不該當加上 SIG 前綴,且必須全大寫,容許帶 SIG 前綴或小寫是某些 shell 的擴展功能。而信號數在不一樣的系統上可能不一樣,因此也不是一個好主意。函數

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

許多資料,尤爲是中文資料中不容申辯地指明—— trap 必須放在腳本中第一個非註釋行。事實果然如此麼?工具

不管是 manpage 仍是 POSIX 文檔中,我都沒有找到任何與之相關的說明。甚至在 TLDP 的 Bash Guide for Beginners 中,多個例子都分明把 trap 放在了腳本的中間。最後我在這篇文檔中找到了下面這句常常被誤讀的話:ui

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

果真,這只是一個爲了保證信號鉤子儘早被設立的一個設計慣例罷了。事實上, 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 -SIGINTpid向這個進程發送 SIGINT 信號。你在原先的終端中看到了什麼?沒有任何反應!若是你願意等上100秒,你最終會看到「INTERRUPTED!」被輸出。這樣看來 trap 是等到當前命令結束之後再處理信號。

這樣的矛盾到底是爲何?問題其實出在 CTRL-C 身上。 Bash 等終端的默認行爲是這樣的:當按下 CTRL-C 以後,它會向當前的整個進程組發出 SIGINT 信號。而 sleep 是由當前腳本調用的,是這個腳本的子進程,默認是在同一個進程組的,因此也會收到 SIGINT 並中止執行,返回主進程之後 trap 捕捉到了信號。

這篇文檔給了咱們一個更準確的說明——若是當前正有一個外部命令在前臺執行,那麼 trap 會等待當前命令結束之後再處理信號隊列中的信號。(而許多教程出錯的另外一個緣由就是——某些 shell 中 sleep 是內建命令,會被打斷。)

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

相關文章
相關標籤/搜索