Linux信號處理機制

在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 後臺程序向終端中寫數據時觸發

注:其中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 等待信號被捕獲

等待信號的過程,其實就是將當前進程(線程)暫停,直到有信號發到當前進程(線程)上並被捕獲,函數有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 修改信號的響應動作

用戶可以自己重新定義某個信號的處理方式,即前面提到的修改信號的默認響應動作,也可以理解為對信號的註冊,可以通過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."打印出來,直至程序正常結束。


书籍推荐