Linux訊號處理機制

劍西樓發表於2017-02-14

在Linux中,訊號是程式間通訊的一種方式,它採用的是非同步機制。當訊號傳送到某個程式中時,作業系統會中斷該程式的正常流程,並進入相應的訊號處理函式執行操作,完成後再回到中斷的地方繼續執行。

需要說明的是,訊號只是用於通知程式發生了某個事件,除了訊號本身的資訊之外,並不具備傳遞使用者資料的功能。

1 訊號的響應動作

每個訊號都有自己的響應動作,當接收到訊號時,程式會根據訊號的響應動作執行相應的操作,訊號的響應動作有以下幾種:

  • 中止程式(Term)
  • 忽略訊號(Ign)
  • 中止程式並儲存記憶體資訊(Core)
  • 停止程式(Stop)
  • 繼續執行程式(Cont)

使用者可以通過signalsigaction函式修改訊號的響應動作(也就是常說的“註冊訊號”,在文章的後面會舉例說明)。另外,在多執行緒中,各執行緒的訊號響應動作都是相同的,不能對某個執行緒設定獨立的響應動作。

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 後臺程式向終端中寫資料時觸發

:其中SIGKILLSIGSTOP訊號不能被捕獲、阻塞或忽略。

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版本之前,SIGSYSSIGXCPUSIGXFSZ以及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 傳送訊號

用於傳送訊號的函式有raisekillkillpgpthread_killtgkillsigqueue,這幾個函式的含義和用法都大同小異,這裡主要介紹一下常用的raisekill函式。

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 等待訊號被捕獲

等待訊號的過程,其實就是將當前程式(執行緒)暫停,直到有訊號發到當前程式(執行緒)上並被捕獲,函式有pausesigsuspend

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 修改訊號的響應動作

使用者可以自己重新定義某個訊號的處理方式,即前面提到的修改訊號的預設響應動作,也可以理解為對訊號的註冊,可以通過signalsigaction函式進行,這裡以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."列印出來,直至程式正常結束。


相關文章