基本概念
信號在Linux中是一個比較常見的概念,例如我們按Ctrl+C中斷前台進程,通過Kill命令結束進程都是通過信號實現的。下面就以Ctrl+C為例簡單的說明信號的處理流程:
- 用戶按下Ctrl-C,這個鍵盤輸入產生一個硬件中斷。
- 該進程的用戶空間代碼暫停執行,CPU從用戶態切換到內核態處理硬件中斷。
- 終端驅動程序將Ctrl-C解釋成一個SIGINT信號,記在該進程的PCB中(也可以說發送了一個SIGINT信號給該進程)。
- 當內核返回到該進程的用戶空間代碼繼續執行之前,首先處理PCB中記錄的信號,發現有一個SIGINT信號待處理,而這個信號的默認處理動作是終止進程,所以直接終止進程而不再返回它的用戶空間代碼執行。
用kill -l命令可以察看系統定義的信號列表:
$ kill –l
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL
5) SIGTRAP 6) SIGABRT 7) SIGBUS 8) SIGFPE
9) SIGKILL 10) SIGUSR1 11) SIGSEGV 12) SIGUSR2
13) SIGPIPE 14) SIGALRM 15) SIGTERM 16) SIGSTKFLT
17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU
25) SIGXFSZ 26) SIGVTALRM 27) SIGPROF 28) SIGWINCH
29) SIGIO 30) SIGPWR 31) SIGSYS 34) SIGRTMIN
35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3 38) SIGRTMIN+4
39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8
43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12
47) SIGRTMIN+13 48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14
51) SIGRTMAX-13 52) SIGRTMAX-12 53) SIGRTMAX-11 54) SIGRTMAX-10
55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7 58) SIGRTMAX-6
59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2
63) SIGRTMAX-1 64) SIGRTMAX
每個信號都有一個編號和一個宏定義名稱,這些宏定義可以在signal.h中找到,可以通過man signal(7)查看詳細說明:
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
...
產生信號的條件主要有:
用戶在終端按下某些鍵時,終端驅動程序會發送信號給前台進程。
例如常見的Ctrl-C產生SIGINT信號,Ctrl-\產生SIGQUIT信號,Ctrl-Z產生SIGTSTP信號。
硬件異常產生信號,這些條件由硬件檢測到並通知內核,然后內核向當前進程發送適當的信號。例如當前進程執行了除以0的指令,CPU的運算單元會產生異常,內核將這個異常解釋為SIGFPE信號發送給進程。
一個進程調用kill函數可以發送信號給另一個進程。
當內核檢測到某種軟件條件發生時也可以通過信號通知進程,例如鬧鍾超時產生SIGALRM信號,向讀端已關閉的管道寫數據時產生SIGPIPE信號。
捕捉信號
如果不想按默認動作處理信號,用戶程序可以調用sigaction函數接管該信號的處理流程。由於信號處理函數的代碼是在用戶空間的,處理過程比較復雜,舉例如下:
- 用戶程序注冊了SIGQUIT信號的處理函數sighandler。
- 當前正在執行main函數,這時發生中斷或異常切換到內核態。
- 在中斷處理完畢后要返回用戶態的main函數之前檢查到有信號SIGQUIT遞達。
- 內核決定返回用戶態后不是恢復main函數的上下文繼續執行,而是執行sighandler函數,sighandler和main函數使用不同的堆棧空間,它們之間不存在調用和被調用的關系,是兩個獨立的控制流程。
- ighandler函數返回后自動執行特殊的系統調用sigreturn再次進入內核態。
- 如果沒有新的信號要遞達,這次再返回用戶態就是恢復main函數的上下文繼續執行了。
sigaction函數可以讀取和修改與指定信號相關聯的處理動作,它的聲明如下:
#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
其中關鍵就是第二個參數act,他是一個sigaction類型,結構如下:
struct sigaction {
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
};
其中關鍵的是sa_handler這個參數,它通常可以有如下幾種賦值:
- 常數SIG_IGN表示忽略信號
- 常數SIG_DFL表示執行系統默認動作,一般用於恢復信號處理
- 賦值為一個函數指針表示用自定義函數捕捉信號
另外,如果在調用信號處理函數時,除了當前信號被自動屏蔽之外,還希望自動屏蔽另外一些信號,則用sa_mask字段說明這些需要額外屏蔽的信號,當信號處理函數返回時自動恢復原來的信號屏蔽字。
下面就以一個簡單的例子演示下如何實現捕捉信號的過程,該函數的功能比較簡單,就是在Ctrl+C的時候並不直接退出,而是先輸出一條華麗的分割線后才退出。
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
void show_and_exit(int sig)
{
printf("\n----------------------------\n");
exit(0);
}
int main(void)
{
struct sigaction act = {0}, oldact = {0};
act.sa_handler = show_and_exit;
//act.sa_flags = SA_RESETHAND | SA_NODEFER;
//sigaddset(&act.sa_mask, SIGQUIT);
sigaction(SIGINT, &act, &oldact);
int count = 0;
while(1)
{
sleep(1);
printf("sleeping %d\n", count++);
}
}
執行該函數結果如下:
$./sign
sleeping 0
sleeping 1
sleeping 2
sleeping 3
sleeping 4
----------------------------
$