什麼是信號(signal)
信號是一種軟件中斷,一種向進程傳遞有關其他進程,操作系統和硬件狀態的信息的方法。信號是一種中斷,因爲它可以改變程序的流程。當信號傳遞給進程時,進程將停止其執行的操作,處理或忽略信號,或者在某些情況下終止,取決於信號。
由於信號可能源自當前正在執行的過程之外的事實,信號也可能以不可預測的方式傳遞,與程序不一致。查看信號的另一種方法是一種處理異步事件的機制。與同步事件相反,同步事件是標準程序執行迭代,即一行代碼跟隨另一行。當程序的某些部分按順序執行時,就會發生異步事件。異步事件通常由於源自硬件或操作系統的外部事件而發生;信號本身是操作系統將這些事件傳遞給進程的方式,以便進程可以採取適當的操作。
如何使用它們
信號在Unix編程中用於各種各樣的目的,我們已經在較小的上下文中使用它們。例如,當我們在shell中工作並希望“殺死所有cat程序”時,我們輸入命令:
#> killall cat
killall命令將向所有名爲cat的進程發送一個信號,表示“終止”。發送的實際信號是SIGTERM,其目的是將終止請求傳送給給定進程,但該進程實際上不必終止…稍後將詳細說明。
我們還在終端信令的上下文中使用和查看信號,這是程序停止,啓動和終止的方式。我們輸入Ctrl-c與發送SIGINT信號相同,輸入Ctrl-z與發送SIGTSTP信號相同,我們輸入fg或bg與發送SIGCONT信號相同。
這些信號中的每一個都描述了該過程應該採取的響應動作。此操作超出了程序的正常控制流程,事件異步到達,要求進程中斷其當前操作以響應事件。對於上述信號,響應是明確的 - SIGTERM終止,SIGSTOP停止,SIGCONT繼續 - 但對於其他信號,程序員可以選擇正確的響應,這可能只是簡單地忽略信號。
常用的Signals
每個信號都有一個名稱,它以SIG開頭,以描述結束。我們可以在手冊頁的第7節中查看所有信號,下面是您可能與之交互的標準Linux信號:
Signal | Value | Action | Comment |
---|---|---|---|
SIGHUP | 1 | Term | Hangup detected on controlling terminal or death of controlling process |
SIGINT | 2 | Term | Interrupt from keyboard |
SIGQUIT | 3 | Core | Quit from keyboard |
SIGILL | 4 | Core | Illegal Instruction |
SIGABRT | 6 | Core | Abort signal from abort(3) |
SIGFPE | 8 | Core | Floating point exception |
SIGKILL | 9 | Term | Kill signal |
SIGSEGV | 11 | Core | Invalid memory reference |
SIGPIPE | 13 | Term | Broken pipe: write to pipe with no readers |
SIGALRM | 14 | Term | Timer signal from alarm(2) |
SIGTERM | 15 | Term | Termination signal |
SIGUSR1 | 30,10,16 | Term | User-defined signal 1 |
SIGUSR2 | 31,12,17 | Term | User-defined signal 2 |
SIGCHLD | 20,17,18 | Ign | Child stopped or terminated |
SIGCONT | 19,18,25 | Cont | Continue if stopped |
SIGSTOP | 17,19,23 | Stop | Stop process |
SIGTSTP | 18,20,24 | Stop | Stop typed at tty |
SIGTTIN | 21,21,26 | Stop | tty input for background process |
SIGTTOU | 22,22,27 | Stop | tty output for background process |
信號的name和value關聯
每個信號都有名稱,值和默認操作。信號名稱應該開始變得更加熟悉,信號的值實際上與信號本身相同。實際上,信號名稱只是一個#defined值,我們可以通過查看sys / signal.h頭文件來看到:
#define SIGHUP 1 /* hangup */
#define SIGINT 2 /* interrupt */
#define SIGQUIT 3 /* quit */
#define SIGILL 4 /* illegal instruction (not reset when caught) */
#define SIGTRAP 5 /* trace trap (not reset when caught) */
#define SIGABRT 6 /* abort() */
#define SIGPOLL 7 /* pollable event ([XSR] generated, not supported) */
#define SIGFPE 8 /* floating point exception */
#define SIGKILL 9 /* kill (cannot be caught or ignored) */
信號的 Action
每個信號都有一個默認動作。表中描述了四種:
Term : The process will terminate
Core : The process will terminate and produce a core dump file that traces the process state at the time of termination.
Ign : The process will ignore the signal
Stop : The process will stop, like with a Ctrl-Z
Cont : The process will continue from being stopped
對於某些信號,我們可以更改默認操作。一些信號,即控制信號,不能改變它們的默認動作,包括SIGKILL和SIGABRT,這就是爲什麼“kill 9”是最終的kill語句。
處理和生成信號
1. Hello信號處理世界
信號處理的主要系統調用是signal(),它給出信號和功能,只要信號被傳送就會執行該功能。此函數稱爲信號處理程序,因爲它處理信號。 signal()函數有一個奇怪的聲明:
int signal(int signum, void (*handler)(int))
也就是說,signal有兩個參數:第一個參數是信號編號,例如SIGSTOP或SIGINT,第二個參數是對第一個參數爲int並返回void的處理函數的引用。
#include <stdlib.h>
#include <stdio.h>
#include <signal.h> /*for signal() and raise()*/
void hello(int signum){
printf("Hello World!\n");
}
int main(){
//execute hello() when receiving signal SIGUSR1
signal(SIGUSR1, hello);
//send SIGUSR1 to the calling process
raise(SIGUSR1);
}
上述程序首先爲用戶信號SIGUSR1建立信號處理程序。信號處理函數hello()按預期執行:打印“Hello World!”到stdout。程序然後發送SIGUSR1信號,這是通過raise()完成的,執行程序的結果是漂亮的短語:
#> ./hello_signal
Hello World!
2.異步執行
從hello程序中取消的一些關鍵點是signal()的第二個參數是一個函數指針,一個對要調用的函數的引用。這告訴操作系統無論何時將此信號發送到此進程,都要將此函數作爲信號處理程序運行。
此外,信號處理程序的執行是異步的,這意味着程序的當前狀態將在信號處理程序執行時暫停,然後執行將從暫停點恢復,就像上下文切換一樣。
讓我們看看另一個示例hello world程序:
/* hello_loop.c*/
void hello(int signum){
printf("Hello World!\n");
}
int main(){
//Handle SIGINT with hello
signal(SIGINT, hello);
//loop forever!
while(1);
}
上面的程序將爲SIGINT設置一個信號處理程序,鍵入Ctrl-C時生成的信號。問題是,當我們執行這個程序時,鍵入Ctrl-C會發生什麼?
首先,讓我們考慮一下程序的執行情況。它將註冊信號處理程序,然後進入無限循環。當我們按下Ctrl-C時,我們都同意信號處理程序hello()應該執行並且“Hello World!”打印到屏幕上,但程序處於無限循環中。爲了打印“Hello World!”一定是它打破循環執行信號處理程序的情況,對嗎?所以它應該退出循環以及程序。讓我們來看看:
#> ./hello_loop
^CHello World!
^CHello World!
^CHello World!
^CHello World!
^CHello World!
^CHello World!
^CHello World!
^\Quit: 3
如輸出所示,每次我們發出Ctrl-C“Hello World!”打印,但程序返回無限循環。只有在用Ctrl- \發出SIGQUIT信號後,程序才真正退出。
雖然循環將退出的解釋是合理的,但它沒有考慮信號處理的主要原因,即異步事件處理。這意味着信號處理程序的行爲超出了程序控制的標準流程;事實上,整個程序都保存在一個上下文中,並且只爲信號處理程序創建了一個新的上下文。如果你再考慮一下,你會發現這很酷,也是一種全新的方式。查看編程。
3.進程間通信
信號也是進程間通信的關鍵手段。一個進程可以向另一個進程發送信號,指示應該採取措施。要向特定進程發送信號,我們使用kill()系統調用。功能聲明如下。
int kill(pid_t pid, int signum);
與命令行版本非常相似,kill()接受一個進程標識符和一個信號,在這種情況下,信號值爲int,但值爲#defined,因此您可以使用該名稱。讓我們看看它在使用中。
/*ipc_signal.c*/
void hello(){
printf("Hello World!\n");
}
int main(){
pid_t cpid;
pid_t ppid;
//set handler for SIGUSR1 to hello()
signal(SIGUSR1, hello);
if ( (cpid = fork()) == 0){
/*CHILD*/
//get parent's pid
ppid = getppid();
//send SIGUSR1 signal to parrent
kill(ppid, SIGUSR1);
exit(0);
}else{
/*PARENT*/
//just wait for child to terminate
wait(NULL);
}
}
在這個程序中,首先爲SIGUSR1建立一個信號處理程序,即hello()函數。在fork之後,父進程調用wait(),並且子進程將通過SIGUSR1信號“殺死”它來與父進行通信。結果是在父級和“Hello World!”中調用了處理程序。從父級打印到stdout。
雖然這只是一個小例子,但信號對於進程間通信是不可或缺的。在前面的課程中,我們討論瞭如何使用pipe()在進程之間傳遞數據,信號是進程傳遞狀態更改和其他異步事件的方式。也許最相關的是兒童過程中的狀態變化。 SIGCHLD信號是孩子終止時傳遞給父母的信號。到目前爲止,我們一直在通過wait()隱式處理這個信號,但您可以選擇處理SIGCHLD並在子進程終止時採取不同的操作。
4.忽略信號
到目前爲止,我們的處理程序一直在打印“Hello World!” - 但我們可能只是希望我們的處理程序什麼也不做,基本上是忽略了信號。這很容易寫入代碼,例如,這是一個程序,它將通過處理信號忽略SIGINT並且什麼都不做:
/*ingore_sigint.c*/
#include <signal.h>
#include <sys/signal.h>
void nothing(int signum){ /*DO NOTHING*/ }
int main(){
signal(SIGINT, nothing);
while(1);
}
如果我們運行這個程序,我們會看到,是的,Ctrl-c無效,我們必須使用Ctrl- \來退出程序:
>./ignore_sigint
^C^C^C^C^C^C^C^C^C^C^\Quit: 3
signal.h標頭定義了一組可用於代替處理程序的操作:
- SIG_IGN:忽略信號
- SIG_DFL:用默認處理程序替換當前信號處理程序
使用這些關鍵字,我們可以簡單地將程序重寫爲:
int main(){
// using SIG_IGN
signal(SIGINT, SIG_IGN);
while(1);
}
5.更改並恢復默認處理程序
設置信號處理程序不是一個單一事件。您始終可以更改處理程序,還可以將處理程序恢復爲默認狀態。例如,請考慮以下程序:
/*you_shot_me.c*/
void handler_3(int signum){
printf("Don't you dare shoot me one more time!\n");
//Revert to default handler, will exit on next SIGINT
signal(SIGINT, SIG_DFL);
}
void handler_2(int signum){
printf("Hey, you shot me again!\n");
//switch handler to handler_3
signal(SIGINT, handler_3);
}
void handler_1(int signum){
printf("You shot me!\n");
//switch handler to handler_2
signal(SIGINT, handler_2);
}
int main(){
//Handle SIGINT with handler_1
signal(SIGINT, handler_1);
//loop forever!
while(1);
}
#> ./you_shout_me
^CYou shot me!
^CHey, you shot me again!
^CDon't you dare shoot me one more time!
^C
程序首先啓動handler_1()作爲SIGINT的信號處理程序。在第一個Ctrl-c之後,在信號處理程序中,處理程序更改爲handler_2(),在第二個Ctrl-c之後,它再次從handler_2()更改爲handler_3()。最後,在handler_3()中重新建立默認信號處理程序,即在SIGINT上終止,這就是我們在輸出中看到的:
#> ./you_shout_me
^CYou shot me!
^CHey, you shot me again!
^CDon't you dare shoot me one more time!
^C
6.有些信號比其他信號更平等
關於信號處理的最後一點是,並非所有信號都是相同的。這意味着,您無法處理所有信號,因爲它可能會使系統處於不可恢復的狀態。
永遠不會被忽略或處理的兩個信號是:SIGKILL和SIGSTOP。我們來看一個例子:
/* ignore_stop.c */
int main(){
//ignore SIGSTOP ?
signal(SIGSTOP, SIG_IGN);
//infinite loop
while(1);
}
上面的程序試圖爲SIGSTOP設置忽略信號處理程序,然後進入無限循環。如果我們執行該計劃,我們發現這些努力沒有結果:
#>./ignore_stop
^Z
[1]+ Stopped ./ignore_stop
對於忽略SIGKILL的程序,我們可以看到相同的內容。
int main(){
//ignore SIGSTOP ?
signal(SIGKILL, SIG_IGN);
//infinite loop
while(1);
}
#>./ignore_kill &
[1] 13129
#>kill -SIGKILL 13129
[1]+ Killed: 9 ./ignore_kill
7.檢查信號錯誤()
signal()函數返回一個指向前一個信號處理程序的指針,這意味着這裏再次是一個系統調用,我們不能通過檢查返回值是否小於0來以典型的方式進行錯誤檢查。這是因爲指針類型是無符號的,沒有負指針這樣的東西。
相反,使用特殊值SIG_ERR,我們可以比較signal()的返回值。這裏再次是我們嘗試忽略SIGKILL的程序,但這次正確的錯誤檢查:
/*signal_errorcheck.c*/
int main(){
//ignore SIGSTOP ?
if( signal(SIGKILL, SIG_IGN) == SIG_ERR){
perror("signal");;
exit(1);
}
//infinite loop
while(1);
}
輸出
#>./signal_errorcheck
signal: Invalid argument