信號在最早的Unix系統中被引入,內核可用信號通知進程系統所發生的事件。在現實生活中,我們每天都在接觸信號,下課鈴聲、紅綠燈、鬧鐘等都是信號。
信號的本質
操作系統給進程發送信號,本質上是給進程的PCB中寫入數據,修改相應的PCB字段,進程在合適的時間去處理所接受的信號。我們模擬一下這樣的場景:
(1)用戶輸入一個命令,在shell下啓動一個前臺進程;
(2)用戶按下Ctrl+c,通過鍵盤輸入產生一個硬件中斷;
(3)如果CPU當前正在運行此進程的代碼,則該進程的用戶空間的代碼將暫停執行,CPU從用戶態切換至內核態處理中斷;
(4)終端驅動程序將Ctrl+c解釋爲一個SIGINT信號,記在該進程的PCB中;
(5)當某個時刻從內核返回至該進程的用戶空間代碼繼續執行之前,首先處理PCB中記錄的信號;SIGINT信號的默認處理動作爲終止信號,所以直接終止進程而不再返回到它的用戶空間代碼;
注:Ctrl+c所產生的信號只能發送給前臺進程,如果想讓該進程在後臺運行,需要在啓動該進程的時候,在後面加上&,這樣shell就不必等待進程結束就可以接受新的命令,啓動新的進程。
上圖中,S爲後臺進程,S+爲前臺進程;shell可以同時運行一個前臺進程和任意多個後臺進程,只有前臺進程才能接受諸如Ctrl+c這樣的信號,前臺進程在運行過程中用戶隨時按下Ctrl+c而產生一個信號,也就是說該進程的用戶空間代碼執行到任何地方都有可能接收到一個SIGINT信號而被終止,因此信號相對於進程的控制流程來說是異步的。
普通信號與實時信號
我們使用 kill -l 命令可以查看系統定義的信號列表,每個編號都有一個宏與之對應,可以在 /usr/include/asm/signal.h 中查看,下圖中 1~31號爲普通信號,34~36號信號爲實時信號。
那麼使用上述信號的目的是什麼呢?大致可以總結爲兩點。
(1)讓進程知道已經發生了一個特定的事件;
(2)強迫進程執行它代碼中的信號處理程序;
上述的兩個目的不是互斥的,因爲進程經常通過執行一個特定的例程來對某一個事件做出反應。
實時信號(real-time signal):編號爲34~46,它們通常與普通信號有很大的不同,因爲他們必須排序以便發送多個信號能被接收到。但是同種信號的普通信號並不排序,儘管在Linux內核並不使用實時信號,它還是通過幾個特定的系統調用完全實現了POSIX標準。
信號有很多,常見的有:
- SIGINT:在鍵盤按下<Ctrl+C>組合鍵後產生,默認動作爲終止進程;
- SIGQUIT:在鍵盤按下<Ctrl+\>組合鍵後產生,默認動作爲終止進程;
- SIGKILL:無條件終止進程。本信號不能被忽略、處理和阻塞。默認動作爲終止進程。它向系統管理員提供了一種可以殺死任何進程的方法;
- SIGALRM:定時器超時,超時的時間由系統調用alarm設置。默認動作爲終止進程;
- SIGCHLD:子進程結束時,父進程會收到這個信號。默認動作爲忽略該信號;
信號的存儲
內核給一個進程發送軟中斷信號的方法,是在進程所在的進程表項的信號域設置對應於該信號的位,而存儲這32位信號的空間恰巧需要4個字節,因此採用位圖存儲是最好不過的。bit位的位置表示對應信號的編號,用0來表示未接受到信號,1表示接收到信號。
產生信號的主要條件
(1)用戶在終端按下某些鍵時,終端驅動會發送信號給前臺進程,例如Ctrl+c產生的SIGINT信號、Ctrl+\產生SIGQUIT信號、Ctrl+z產生SIGSTOP信號;
(2)硬件異常產生的信號,這些條件由硬件檢測並通知內核,然後內核向當前進程發送合適的信號。比如當前進程訪問了非法地址,MMU(內存管理單元)會產生異常,內核將這個異常解釋爲SIGSEGV信號發送給進程;
(3)一個進程調用 kill 函數可以發送信號給另一個進程,也可以調用 kill 命令發送信號給某一個進程,kill 命令也是調用 kill 函數實現的,如果不明確指定信號,則發送SIGTERM信號,該信號的默認處理動作是終止進程,當內核檢測到軟件條件發生時可以通過信號通知進程。
如何處理信號
進程以三種方式對一個信號做出應答:
(1)顯示的忽略信號;
(2)執行與信號相關的默認操作;由內核預定義的默認操作取決於信號的類型,可以是以下類型之一:
Treminate:進程被終止(殺死)
Dump:進程被終止(殺死),如果可能,創建包含進程執行上下文的核心轉儲文件
Ignore:信號被忽略
Stop:進程被停止,即把進程置爲TASK_STOPPED狀態
Continue:如果進程被停止,就把它設置爲TASK_RUNNING狀態
(3)通過調用相應的信號處理函數捕捉信號(自定義類型)
信號捕捉函數:可以修改信號的默認處理操作,但某些信號是不能夠被捕捉的,比如9號信號,它存在的目的是防止惡意進程入侵而無法被終止,在一定程度上保護了操作系統。
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
參數signum:信號的編號
參數handler:是一個函數指針,表示接受此信號要執行的函數的地址
返回值:若成功則爲指向前次處理程序的指針,若出錯則爲SIG_ERR
我們做一個測試,對2號信號進行捕捉:
#include<stdio.h>
#include<signal.h>
void handler()
{
printf("handler\n");
}
int main()
{
signal(2,handler);
while(1);
return 0;
}
我們可以看到Ctrl+c是不能終止該程序的,無奈我們只能用9號信號來殺死該進程。
修改上面的程序:
#include<stdio.h>
#include<signal.h>
#include<unistd.h>
typedef void(*sighandler_t) (int);//函數指針
sighandler_t _handler = NULL;
void handler()
{
printf("handler\n");
signal(2,_handler);//恢復默認處理
}
int main()
{
_handler = signal(2,handler);//捕捉2號信號
while(1);
return 0;
}
對上述程序的解釋:首先我們用Ctrl+c捕捉2號信號,並用_handler函數指針對象接受,在handler函數內,再次用 Ctrl+c 捕捉2號信號,並指向_handler捕捉成功,返回之前的信號處理函數,即恢復了默認處理,程序得以終止。
信號產生的方法
(1)通過終端按鍵產生信號(Core dump)
(2)調用系統函數向進程發送信號
首先在後臺執行死循環程序,然後用 kill 命令給它發一個 SIGSEGV 信號。
我們將signal_a程序在後臺運行,之所以要按一次回車鍵才顯示段錯誤的原因在於,該進程終止之前已經回到了shell提示符等待用戶輸入下一條命令,shell不希望段錯誤的信息和用戶輸入的交錯在一起,所以等用戶輸入命令之後纔會顯示。
kill命令是調用 kill 函數實現的, kill 函數可以給一個特定的進程發送指定的信號;
raise函數可以給當前進程發送指定的信號(自己也可以給自己發送信號),原型如下:
#include <signal.h>
int kill(pid_t pid, int signum); //給任意進程發送任意信號
int raise(int signo); //給自己發送任意信號
參數pid:進程號
參數signum:信號的編號
返回值:兩者都是成功返回0,失敗返回-1
我們模擬一下raise函數,給自己發送2號信號:
#include<stdio.h>
#include<signal.h>
int count = 0;
void myhandler(int sig)
{
printf("count:%d, sig:%d\n",count++,sig);
}
int main()
{
signal(2,myhandler);
while(1)
{
raise(2);
sleep(1);
}
return 0;
}
運行結果如下:abort可以使當前進程接收到信號而異常終止,但是abort會認爲進程不安全。
#include <stdlib.h>
void abort(void);
類似於exit函數一樣,abort函數總是成功的,因此沒有返回值。
(3)由軟件條件產生信號
進程可以通過調用alarm向它自己發送SIGALRM信號,函數原型如下:
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
參數seconds:alarm函數安排內核在seconds秒內發送一個SIGALRM信號給調用進程,如果soconds等於0,那麼不會調度新的鬧鐘(alarm)
返回值:前一次鬧鐘剩餘的秒數,若以前沒有設定鬧鐘,則爲0
下面這個程序,我們讓SIGALRM信號在5秒內數數,當傳送第6個SIGALRM信號的時候程序會終止。
#include<stdio.h>
#include<unistd.h>
#include<signal.h>
#include<stdlib.h>
void handler(int sig)
{
static int count = 0;
printf("count=%d\n",count);
if(count++ < 5)
{
alarm(1);
}
else
{
printf("end...\n");
exit(0);
}
}
int main()
{
signal(SIGALRM,handler);
alarm(1);
while(1);
return 0;
}
運行結果如下:我們使用signal函數設置了一個信號處理函數,只有進程收到一個SIGALRM信號,就異步調用該函數,中斷main的while循環,當handler返回時,控制傳遞迴main函數,它就從最初被信號到達時中斷了的地方繼續執行。