在Linux中,訊號是程式間通訊的一種方式,它採用的是非同步機制。當訊號傳送到某個程式中時,作業系統會中斷該程式的正常流程,並進入相應的訊號處理函式執行操作,完成後再回到中斷的地方繼續執行。
需要說明的是,訊號只是用於通知程式發生了某個事件,除了訊號本身的資訊之外,並不具備傳遞使用者資料的功能。
1 訊號的響應動作
每個訊號都有自己的響應動作,當接收到訊號時,程式會根據訊號的響應動作執行相應的操作,訊號的響應動作有以下幾種:
- 中止程式(Term)
- 忽略訊號(Ign)
- 中止程式並儲存記憶體資訊(Core)
- 停止程式(Stop)
- 繼續執行程式(Cont)
使用者可以通過signal
或sigaction
函式修改訊號的響應動作(也就是常說的“註冊訊號”,在文章的後面會舉例說明)。另外,在多執行緒中,各執行緒的訊號響應動作都是相同的,不能對某個執行緒設定獨立的響應動作。
2 訊號型別
Linux支援的訊號型別可以參考下面給出的列表。
2.1 在POSIX.1-1990標準中的訊號列表
訊號 | 值 | 動作 | 說明 |
---|---|---|---|
SIGHUP | 1 | Term | 終端控制程式結束(終端連線斷開) |
SIGINT | 2 | Term | 使用者傳送INTR字元(Ctrl+C)觸發 |
SIGQUIT | 3 | Core | 使用者傳送QUIT字元(Ctrl+/)觸發 |
SIGILL | 4 | Core | 非法指令(程式錯誤、試圖執行資料段、棧溢位等) |
SIGABRT | 6 | Core | 呼叫abort函式觸發 |
SIGFPE | 8 | Core | 算術執行錯誤(浮點運算錯誤、除數為零等) |
SIGKILL | 9 | Term | 無條件結束程式(不能被捕獲、阻塞或忽略) |
SIGSEGV | 11 | Core | 無效記憶體引用(試圖訪問不屬於自己的記憶體空間、對只讀記憶體空間進行寫操作) |
SIGPIPE | 13 | Term | 訊息管道損壞(FIFO/Socket通訊時,管道未開啟而進行寫操作) |
SIGALRM | 14 | Term | 時鐘定時訊號 |
SIGTERM | 15 | Term | 結束程式(可以被捕獲、阻塞或忽略) |
SIGUSR1 | 30,10,16 | Term | 使用者保留 |
SIGUSR2 | 31,12,17 | Term | 使用者保留 |
SIGCHLD | 20,17,18 | Ign | 子程式結束(由父程式接收) |
SIGCONT | 19,18,25 | Cont | 繼續執行已經停止的程式(不能被阻塞) |
SIGSTOP | 17,19,23 | Stop | 停止程式(不能被捕獲、阻塞或忽略) |
SIGTSTP | 18,20,24 | Stop | 停止程式(可以被捕獲、阻塞或忽略) |
SIGTTIN | 21,21,26 | Stop | 後臺程式從終端中讀取資料時觸發 |
SIGTTOU | 22,22,27 | Stop | 後臺程式向終端中寫資料時觸發 |
注:其中SIGKILL
和SIGSTOP
訊號不能被捕獲、阻塞或忽略。
2.2 在SUSv2和POSIX.1-2001標準中的訊號列表
訊號 | 值 | 動作 | 說明 |
---|---|---|---|
SIGTRAP | 5 | Core | Trap指令觸發(如斷點,在偵錯程式中使用) |
SIGBUS | 0,7,10 | Core | 非法地址(記憶體地址對齊錯誤) |
SIGPOLL | Term | Pollable event (Sys V). Synonym for SIGIO | |
SIGPROF | 27,27,29 | Term | 效能時鐘訊號(包含系統呼叫時間和程式佔用CPU的時間) |
SIGSYS | 12,31,12 | Core | 無效的系統呼叫(SVr4) |
SIGURG | 16,23,21 | Ign | 有緊急資料到達Socket(4.2BSD) |
SIGVTALRM | 26,26,28 | Term | 虛擬時鐘訊號(程式佔用CPU的時間)(4.2BSD) |
SIGXCPU | 24,24,30 | Core | 超過CPU時間資源限制(4.2BSD) |
SIGXFSZ | 25,25,31 | Core | 超過檔案大小資源限制(4.2BSD) |
注:在Linux 2.2版本之前,SIGSYS
、SIGXCPU
、SIGXFSZ
以及SIGBUS
的預設響應動作為Term,Linux
2.4版本之後這三個訊號的預設響應動作改為Core。
2.3 其它訊號
訊號 | 值 | 動作 | 說明 |
---|---|---|---|
SIGIOT | 6 | Core | IOT捕獲訊號(同SIGABRT訊號) |
SIGEMT | 7,-,7 | Term | 實時硬體發生錯誤 |
SIGSTKFLT | -,16,- | Term | 協同處理器棧錯誤(未使用) |
SIGIO | 23,29,22 | Term | 檔案描述符準備就緒(可以開始進行輸入/輸出操作)(4.2BSD) |
SIGCLD | -,-,18 | Ign | 子程式結束(由父程式接收)(同SIGCHLD訊號) |
SIGPWR | 29,30,19 | Term | 電源錯誤(System V) |
SIGINFO | 29,-,- | 電源錯誤(同SIGPWR訊號) | |
SIGLOST | -,-,- | Term | 檔案鎖丟失(未使用) |
SIGWINCH | 28,28,20 | Ign | 視窗大小改變時觸發(4.3BSD, Sun) |
SIGUNUSED | -,31,- | Core | 無效的系統呼叫(同SIGSYS訊號) |
注意:列表中有的訊號有三個值,這是因為部分訊號的值和CPU架構有關,這些訊號的值在不同架構的CPU中是不同的,三個值的排列順序為:1,Alpha/Sparc;2,x86/ARM/Others;3,MIPS。
例如SIGSTOP
這個訊號,它有三種可能的值,分別是17、19、23,其中第一個值(17)是用在Alpha和Sparc架構中,第二個值(19)用在x86、ARM等其它架構中,第三個值(23)則是用在MIPS架構中的。
3 訊號機制
文章的前面提到過,訊號是非同步的,這就涉及訊號何時接收、何時處理的問題。
我們知道,函式執行在使用者態,當遇到系統呼叫、中斷或是異常的情況時,程式會進入核心態。訊號涉及到了這兩種狀態之間的轉換,過程可以先看一下下面的示意圖:
接下來圍繞示意圖,將訊號分成接收、檢測和處理三個部分,逐一講解每一步的處理流程。
3.1 訊號的接收
接收訊號的任務是由核心代理的,當核心接收到訊號後,會將其放到對應程式的訊號佇列中,同時向程式傳送一箇中斷,使其陷入核心態。
注意,此時訊號還只是在佇列中,對程式來說暫時是不知道有訊號到來的。
3.2 訊號的檢測
程式陷入核心態後,有兩種場景會對訊號進行檢測:
- 程式從核心態返回到使用者態前進行訊號檢測
- 程式在核心態中,從睡眠狀態被喚醒的時候進行訊號檢測
當發現有新訊號時,便會進入下一步,訊號的處理。
3.3 訊號的處理
訊號處理函式是執行在使用者態的,呼叫處理函式前,核心會將當前核心棧的內容備份拷貝到使用者棧上,並且修改指令暫存器(eip)將其指向訊號處理函式。
接下來程式返回到使用者態中,執行相應的訊號處理函式。
訊號處理函式執行完成後,還需要返回核心態,檢查是否還有其它訊號未處理。如果所有訊號都處理完成,就會將核心棧恢復(從使用者棧的備份拷貝回來),同時恢復指令暫存器(eip)將其指向中斷前的執行位置,最後回到使用者態繼續執行程式。
至此,一個完整的訊號處理流程便結束了,如果同時有多個訊號到達,上面的處理流程會在第2步和第3步驟間重複進行。
4 訊號的使用
4.1 傳送訊號
用於傳送訊號的函式有raise
、kill
、killpg
、pthread_kill
、tgkill
、sigqueue
,這幾個函式的含義和用法都大同小異,這裡主要介紹一下常用的raise
和kill
函式。
raise函式:向程式本身傳送訊號
函式宣告如下:
#include <signal.h>
int raise(int sig);
函式功能是向當前程式(自身)傳送訊號,其中引數sig
為訊號值。
kill函式:向指定程式傳送訊號
函式宣告如下:
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
函式功能是向特定的程式傳送訊號,其中引數pid
為程式號,sig
為訊號值。
在這裡的引數pid
,根據取值範圍不同,含義也不同,具體說明如下:
- pid > 0 :向程式號為pid的程式傳送訊號
- pid = 0 :向當前程式所在的程式組傳送訊號
- pid = -1 :向所有程式(除PID=1外)傳送訊號(許可權範圍內)
- pid < -1 :向程式組號為-pid的所有程式傳送訊號
另外,當sig
值為零時,實際不傳送任何訊號,但函式返回值依然有效,可以用於檢查程式是否存在。
4.2 等待訊號被捕獲
等待訊號的過程,其實就是將當前程式(執行緒)暫停,直到有訊號發到當前程式(執行緒)上並被捕獲,函式有pause
和sigsuspend
。
pause函式:將程式(或執行緒)轉入睡眠狀態,直到接收到訊號
函式宣告如下:
#include <unistd.h>
int pause(void);
該函式呼叫後,呼叫者(程式或執行緒)會進入睡眠(Sleep)狀態,直到捕獲到(任意)訊號為止。該函式的返回值始終為-1,並且呼叫結束後,錯誤程式碼(errno)會被置為EINTR。
sigsuspend函式:將程式(或執行緒)轉入睡眠狀態,直到接收到特定訊號
函式宣告如下:
#include <signal.h>
int sigsuspend(const sigset_t *mask);
該函式呼叫後,會將程式的訊號掩碼臨時修改(引數mask
),然後暫停程式,直到收到符合條件的訊號為止,函式返回前會將呼叫前的訊號掩碼恢復。該函式的返回值始終為-1,並且呼叫結束後,錯誤程式碼(errno)會被置為EINTR。
4.3 修改訊號的響應動作
使用者可以自己重新定義某個訊號的處理方式,即前面提到的修改訊號的預設響應動作,也可以理解為對訊號的註冊,可以通過signal
或sigaction
函式進行,這裡以signal
函式舉例說明。
首先看一下函式宣告:
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
第一個引數signum
是訊號值,可以從前面的訊號列表中查到,第二個引數handler
為處理函式,通過回撥方式在訊號觸發時呼叫。
下面為示例程式碼:
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
/* 訊號處理函式 */
void sig_callback(int signum) {
switch (signum) {
case SIGINT:
/* SIGINT: Ctrl+C 按下時觸發 */
printf("Get signal SIGINT. \r\n");
break;
/* 多個訊號可以放到同一個函式中進行 通過訊號值來區分 */
default:
/* 其它訊號 */
printf("Unknown signal %d. \r\n", signum);
break;
}
return;
}
/* 主函式 */
int main(int argc, char *argv[]) {
printf("Register SIGINT(%u) Signal Action. \r\n", SIGINT);
/* 註冊SIGINT訊號的處理函式 */
signal(SIGINT, sig_callback);
printf("Waitting for Signal ... \r\n");
/* 等待訊號觸發 */
pause();
printf("Process Continue. \r\n");
return 0;
}
原始檔下載:連結
例子中,將SIGINT
訊號(Ctrl+C
觸發)的動作接管(列印提示資訊),程式執行後,按下Ctrl+C
,命令列輸出如下:
./linux_signal_example
Register SIGINT(2) Signal Action.
Waitting for Signal ...
^CGet signal SIGINT.
Process Continue.
程式收到SIGINT
訊號後,觸發響應動作,將提示資訊列印出來,然後從暫停的地方繼續執行。這裡需要注意的是,因為我們修改了SIGINT
訊號的響應動作(只列印資訊,不做程式退出處理),所以我們按下Ctrl+C
後,程式並沒有直接退出,而是繼續執行並將"Process
Continue."列印出來,直至程式正常結束。