一篇科普文章,介紹什麼是 Linux 信號,以及它的基本用法,內含精美圖表。 ...
本文是一篇科普文章,介紹什麼是 Linux 信號,以及它的基本用法。原文鏈接見底部參考。
Linux中有許多處於不同狀態的進程。這些進程屬於用戶應用程式或操作系統。我們需要一種機制讓內核和這些進程協調它們的活動。其中一種方式是在一個進程有重大改變時通知其他進程,因此我們有了 信號 的概念。
信號基本上是一種單向通知。信號可以由內核發送給一個進程,或由一個進程發送給另一個進程,或者一個進程發送給它自己。
Linux信號的概念來源於Unix。在後來的Linux版本中,加入了實時(real-time)信號。信號是一種簡單和輕量級的進程間通信形式,因此適用於嵌入式系統。
有關信號的討論
什麼是信號?
總共有 31 個標準信號,編號為 1-31。每個信號命名為“SIG”開頭,後跟一個尾碼(如INT、HUP、KILL等)。從 2.2 版開始,Linux 內核支持 33 種不同的實時信號,編號為 32-64,但應用程式應改為使用 SIGRTMIN + n 表示法。標準信號有特定用途,但 SIGUSR1 和 SIGUSR2 的使用可以由程式自定義。實時信號也可由程式定義。
0號信號,即 POSIX.1 標準中所說的null信號,一般不使用,但在 kill 函數中有個特殊的用途。使用時沒有信號被髮送,但可以用來(相當不可靠)檢查進程是否仍然存在。
Linux中的信號實現完全符合 POSIX 標準。最新的實現應該傾向於使用 sigaction 而不是傳統的信號介面。
正如硬體子系統可以中斷處理器一樣,信號可以中斷進程的執行。因此,它們被看作是軟體中斷。一般來說,中斷處理程式(interrupt handlers)處理硬體中斷,而信號處理程式(signal handlers)則處理信號導致的中斷。
通常信號被映射到特定的按鍵輸入,比如,SIGINT代表ctrl+c,SIGSTOP代表ctrl+z,SIGQUIT代表ctrl+\。
信號如何影響進程的狀態?
一些信號會終止正在接受信號的進程:SIGHUP、SIGINT、SIGTERM、SIGKILL。有一些信號不僅可以終止進程還會輸出一些內核信息,以幫助程式員調試出錯的地方,如SIGABRT(abort)、SIGBUS(bus error)、SIGILL(illegal instruction)、SIGSEGV(invalid memory reference無效記憶體引用)、SIGSYS(bad system call錯誤的系統調用) )。用於停止進程的信號有:SIGSTOP、SIGTSTP。 SIGCONT 是恢復已停止的進程。
一個程式可以覆蓋信號的預設行為。例如,一個互動式程式可以忽略SIGINT(由ctrl+c輸入產生)。不過有兩個例外需要註意,SIGKILL和SIGSTOP,它們不能被忽略、阻止或用這種方式覆蓋。
讓我們看一個父進程和其子進程的例子。假設子進程向自己發送了SIGSTOP,子進程將被停止。這反過來又會觸發SIGCHLD到父進程。然後,父進程可以使用SIGCONT向子進程發出繼續運行的信號。當子進程從停止狀態重新運行時,另一個SIGCHLD被髮送到父進程。如果後來,子進程退出了,最後的SIGCHLD會被髮送到父進程。
信號類似於異常(exception)嗎?
一些編程語言能夠使用諸如try-throw-catch這樣的結構進行異常處理。
但信號與異常並不類似。相反,失敗的系統或庫調用會返回非零的退出代碼。當一個進程被終止時,它的退出代碼是128加信號編號。例如,一個被SIGKILL殺死的進程將返回137(128+9)。
信號是同步還是非同步的?
信號既可以是同步,也可以是非同步。
同步信號的出現是由於指令導致了一個無法恢復的錯誤,如非法地址訪問。這些信號被髮送到導致它的線程。這些信號也被稱為陷阱(trap),因為它們也會導致陷阱進入內核的陷阱處理程式(trap handler)。
非同步信號是對當前執行環境的外部信號。從另一個進程中發送 SIGKILL 就是這樣一個例子。這些也被稱為軟體中斷。
信號的生命周期是什麼?
一個信號經歷三個階段:
-
Generation:信號可以由內核或任何進程生成,生成後會將其發送給特定的進程。信號由其編號表示,沒有額外的數據或參數。因此,信號是輕量級的。但是,POSIX 實時信號傳遞額外的數據。可以生成信號的系統調用和函數包括 raise、kill、killpg、pthread_kill、tgkill 和 sigqueue。
-
Delivery:信號在傳遞之前一直處於待處理狀態。通常,內核會儘快將信號傳遞給進程。但是,如果對應的進程阻塞了信號,它將保持未處理狀態直到解除阻塞。
-
Processing:一旦信號被傳遞到,就會以多種方式中其中一種進行處理。每個信號都有一個預設的行為:忽略信號;或終止進程,有時使用核心轉儲(core dump);或停止/繼續該過程。對於非預設行為,對應的處理函數會被調用。通過 sigaction 函數指定究竟採用哪一種處理方式。
什麼是信號阻塞和解除阻塞?
信號打斷了程式執行的正常流程。當進程正在執行一些關鍵代碼或更新與信號處理程式共用的數據時,這是不希望看到的。阻斷的引入解決了這個問題。不過代價是,信號處理被延遲了。
每個進程都可以指定它是否要阻塞一個特定的信號。如果被阻斷,而信號確實發生了,操作系統將把該信號作為待處理信號。一旦進程解除阻斷,該信號將被傳遞。當前被屏蔽的信號集合被稱為信號屏蔽(signal mask)。
無限期地阻斷一個信號是沒有意義的。為了這個目的,進程可以在接受到信號後選擇忽略它,被一個進程屏蔽的信號不會影響其他進程,他們可以正常接收信號。
信號屏蔽(Signal mask)可以用 sigprocmask(單線程)或 pthread_sigmask(多線程)來設置。 當一個進程有多個線程時,信號可以針對每個線程分別設置是否屏蔽。信號將被傳遞給任何一個沒有阻斷它的線程。從本質上講,信號處理程式是針對某個進程的,信號掩碼是針對某個線程的。
一個進程可以有多個待處理的信號嗎?
是的,許多標準信號可以在進程中被掛起。然而,一個給定的信號類型只能有一個實例被掛起。這是因為信號的掛起和阻塞是作為位掩碼(bitmask)實現的,每個信號類型只有一個位。例如,我們可以讓 SIGALRM 和 SIGTERM 同時掛起,但我們不能有兩個 SIGALRM 信號掛起。進程將只收到一個SIGALRM信號,即使是多次拋出。
通過實時信號,信號可以和數據一起排隊,這樣每個信號的實例都可以單獨傳遞和處理。
POSIX沒有規定標準信號的傳遞順序,也沒有規定如果標準信號和實時信號都在等待中會如何處理。然而在Linux中,會優先處理標準信號。對於實時信號,編號較低的信號首先被傳遞,如果一個信號類型有很多在排隊,最早的一個會被首先傳遞。
大事記
- 1990 信號在 POSIX.1-1990 標準中得到了描述。可以追溯至 IEEE標準1003.1-1988。
- 1993 實時擴展作為 POSIX.1b 發佈。其中包含實時信號。
- 1999 隨著內核版本 2.2 的發佈,Linux 開始支持實時信號。
- 2001 POSIX.1-2001 標準中增加了更多信號:SIGBUS、SIGPOLL、SIGPROF、SIGSYS、SIGTRAP、SIGURG、SIGVTALRM、SIGXCPU、SIGXFSZ。
示例代碼
// Example shows a custom handler for SIGINT
// but the handler reverts to default action for future signals.
// Thus, first ctrl+c will allow program to continue
// and second ctrl+c will terminate the program.
#include <unistd.h>
#include <stdio.h>
#include <signal.h>
void sig_handler1(int num)
{
printf("You are here becoz of signal: %d\n", num);
signal(SIGINT, SIG_DFL);
}
int main()
{
signal(SIGINT, sig_handler1);
while(1)
{
printf("Hello\n");
sleep(2);
}
}
參考
- 原文鏈接:linux-signals