c/c++ signal(信號)解析

什麼是信號(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
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章