Linux訊號處理編程

01. 學習目標

  • 了解訊號中的基本概念
  • 熟練使用訊號相關的函數
  • 了解內核中的阻塞訊號集和未決訊號集作用
  • 熟悉訊號集操作相關函數
  • 熟練使用訊號捕捉函數signal
  • 熟練使用訊號捕捉函數sigaction
  • 熟練掌握使用訊號完成子進程的回收

02. 訊號概述

訊號的概念

訊號是 Linux 進程間通訊的最古老的方式。訊號是軟體中斷,它是在軟體層次上對中斷機制的一種模擬,是一種非同步通訊的方式 。訊號可以導致一個正在運行的進程被另一個正在運行的非同步進程中斷,轉而處理某一個突發事件。

「中斷」在我們生活中經常遇到,譬如,我正在房間里打遊戲,突然送快遞的來了,把正在玩遊戲的我給「中斷」了,我去簽收快遞( 處理中斷 ),處理完成後,再繼續玩我的遊戲。

這裡我們學習的「訊號」就是屬於這麼一種「中斷」。我們在終端上敲「Ctrl+c」,就產生一個「中斷」,相當於產生一個訊號,接著就會處理這麼一個「中斷任務」(默認的處理方式為中斷當前進程)。

訊號的特點

  • 簡單
  • 不能攜帶大量資訊
  • 滿足某個特設條件才發送

訊號可以直接進行用戶空間進程和內核空間進程的交互,內核進程可以利用它來通知用戶空間進程發生了哪些系統事件。

一個完整的訊號周期包括三個部分:訊號的產生,訊號在進程中的註冊,訊號在進程中的註銷,執行訊號處理函數。如下圖所示:

image-20211024224531360

【PS】:這裡訊號的產生,註冊,註銷時訊號的內部機制,而不是訊號的函數實現。

03. 訊號的編號(了解)

訊號編號

Unix早期版本就提供了訊號機制,但不可靠,訊號可能丟失。Berkeley 和 AT&T都對訊號模型做了更改,增加了可靠訊號機制。但彼此不兼容。POSIX.1對可靠訊號常式進行了標準化。

Linux 可使用命令:kill -l("l" 為字母),查看相應的訊號。

1527928772368

不存在編號為0的訊號。其中1-31號訊號稱之為常規訊號(也叫普通訊號或標準訊號),34-64稱之為實時訊號,驅動編程與硬體相關。名字上區別不大。而前32個名字各不相同。

Linux常規訊號一覽表

編號 訊號 對應事件 默認動作
1 SIGHUP ①如果終端介面檢測到連接斷開,則會將SIGHUP訊號發送給與該終端關聯的控制進程(即會話首進程),接到該訊號的會話首進程可能在後台。②如果會話首進程終止,會將SIGHUP訊號發送給前台進程組的所有進程。 終止進程
2 SIGINT 當用戶按下了<Ctrl+C>組合鍵時,終端將SIGINT訊號發送給該終端的前台進程組中的每個進程。 終止進程
3 SIGQUIT 用戶按下<ctrl+\>組合鍵時產生該訊號,終端將SIGQUIT訊號發送給該終端的前台進程組中的每個進程。 終止進程
4 SIGILL CPU檢測到某進程執行了非法指令 終止進程併產生core文件
5 SIGTRAP 該訊號由斷點指令或其他 trap指令產生 終止進程併產生core文件
6 SIGABRT 調用abort函數時產生該訊號 終止進程併產生core文件
7 SIGBUS 非法訪問記憶體地址,包括記憶體對齊出錯 終止進程併產生core文件
8 SIGFPE 在發生致命的運算錯誤時發出。不僅包括浮點運算錯誤,還包括溢出及除數為0等所有的演算法錯誤 終止進程併產生core文件
9 SIGKILL 無條件終止進程。本訊號不能被忽略,處理和阻塞 終止進程,可以殺死任何進程
10 SIGUSE1 用戶定義的訊號。即程式設計師可以在程式中定義並使用該訊號 終止進程
11 SIGSEGV 指示進程進行了無效記憶體訪問(段錯誤) 終止進程併產生core文件
12 SIGUSR2 另外一個用戶自定義訊號,程式設計師可以在程式中定義並使用該訊號 終止進程
13 SIGPIPE Broken pipe向一個沒有讀端的管道寫數據 終止進程
14 SIGALRM 定時器超時,超時的時間 由系統調用alarm設置 終止進程
15 SIGTERM 程式結束訊號,與SIGKILL不同的是,該訊號可以被阻塞和終止。通常用來要示程式正常退出。執行shell命令Kill時,預設產生這個訊號 終止進程
16 SIGSTKFLT Linux早期版本出現的訊號,現仍保留向後兼容 終止進程
17 SIGCHLD 子進程結束時,父進程會收到這個訊號 忽略這個訊號
18 SIGCONT 如果進程已停止,則使其繼續運行 繼續/忽略
19 SIGSTOP 停止進程的執行。訊號不能被忽略,處理和阻塞 終止進程
20 SIGTSTP 交互停止訊號。按下<ctrl+z>組合鍵時發出這個訊號,該訊號發送至前台進程組中的每個進程。 暫停進程
21 SIGTTIN 後台進程讀終端控制台 暫停進程
22 SIGTTOU 該訊號類似於SIGTTIN,在後台進程要向終端輸出數據時發生 暫停進程
23 SIGURG 套接字上有緊急數據時,向當前正在運行的進程發出些訊號,報告有緊急數據到達。如網路帶外數據到達 忽略該訊號
24 SIGXCPU 進程執行時間超過了分配給該進程的CPU時間 ,系統產生該訊號並發送給該進程 終止進程
25 SIGXFSZ 超過文件的最大長度設置 終止進程
26 SIGVTALRM 虛擬時鐘超時時產生該訊號。類似於SIGALRM,但是該訊號只計算該進程佔用CPU的使用時間 終止進程
27 SGIPROF 類似於SIGVTALRM,它不公包括該進程佔用CPU時間還包括執行系統調用時間 終止進程
28 SIGWINCH 窗口變化大小時發出 忽略該訊號
29 SIGIO 此訊號向進程指示發出了一個非同步IO事件 忽略該訊號
30 SIGPWR 關機 終止進程
31 SIGSYS 無效的系統調用 終止進程併產生core文件
34~64 SIGRTMIN ~ SIGRTMAX LINUX的實時訊號,它們沒有固定的含義(可以由用戶自定義) 終止進程

早期不可靠訊號

在早期Unix版本中,訊號是不可靠的:

  • 訊號可能會丟失。
  • 進程對訊號的控制能力很差,無法阻塞訊號(阻塞訊號即要求內核不要忽略訊號,在訊號發生時記住他,然後在進程解除阻塞後再將該訊號通知給進程)。
  • 進程每次接到訊號對其處理時,隨即將該訊號處理動作重置為默認動作。

中斷的系統調用

早期Unix的一個特性是,如果進程在執行一個低速系統調用而阻塞期間捕捉到一個訊號,則該系統調用就被中斷而不再執行,該系統調用出錯返回,設置errno為EINTR。

【PS】:區分系統調用和函數,當捕捉到訊號時,被中斷的是內核中執行的系統調用。

將系統調用分為兩類:低速、其他系統調用。

低速系統調用指的是可能會使進程永遠阻塞的一類系統調用。

POSIX.1要求只有中斷訊號的SA_RESTART標誌有效時,實現才重啟系統調用。

04. 訊號四要素

每個訊號必備4要素,分別是:

1)編號 2)名稱 3)事件 4)默認處理動作

可通過man 7 signal查看幫助文檔獲取:

image-20211024224558471

在標準訊號中,有一些訊號是有三個「Value」,第一個值通常對alpha和sparc架構有效,中間值針對x86、arm和其他架構,最後一個應用於mips架構。一個『-』表示在對應架構上尚未定義該訊號。

不同的作業系統定義了不同的系統訊號。因此有些訊號出現在Unix系統內,也出現在Linux中,而有的訊號出現在FreeBSD或Mac OS中卻沒有出現在Linux下。這裡我們只研究Linux系統中的訊號。

Action為默認動作:

  • Term:終止進程
  • Ign: 忽略訊號 (默認即時對該種訊號忽略操作)
  • Core:終止進程,生成Core文件。(查驗死亡原因,用於gdb調試)
  • Stop:停止(暫停)進程
  • Cont:繼續運行進程

注意通過man 7 signal命令查看幫助文檔,其中可看到 : The signals SIGKILL and SIGSTOP cannot be caught, blocked, or ignored.

這裡特彆強調了9) SIGKILL 和19) SIGSTOP訊號,不允許忽略和捕捉,只能執行默認動作。甚至不能將其設置為阻塞。

另外需清楚,只有每個訊號所對應的事件發生了,該訊號才會被遞送(但不一定遞達),不應亂髮訊號!!

05. 訊號的狀態

1) 產生

a) 當用戶按某些終端鍵時,將產生訊號。

終端上按「Ctrl+c」組合鍵通常產生中斷訊號 SIGINT

終端上按「Ctrl+\」鍵通常產生中斷訊號 SIGQUIT

終端上按「Ctrl+z」鍵通常產生中斷訊號 SIGSTOP 等。

b) 硬體異常將產生訊號。

除數為 0,無效的記憶體訪問等。這些情況通常由硬體檢測到,並通知內核,然後內核產生適當的訊號發送給相應的進程。

c) 軟體異常將產生訊號。

當檢測到某種軟體條件已發生(如:定時器alarm),並將其通知有關進程時,產生訊號。

d) 調用系統函數(如:kill、raise、abort)將發送訊號。

注意:接收訊號進程和發送訊號進程的所有者必須相同,或發送訊號進程的所有者必須是超級用戶。

e) 運行 kill /killall命令將發送訊號。

此程式實際上是使用 kill 函數來發送訊號。也常用此命令終止一個失控的後台進程。

2) 未決狀態:在訊號產生和遞送之間的時間間隔內,稱訊號是未決的(pending)。

如果為進程產生了一個被該進程設置為阻塞的訊號,而且對該訊號的動作是默認或者捕捉該訊號,則內核為該進程將此訊號保持為未決狀態,直到該進程對此訊號解除阻塞,或者將對此訊號的動作改為忽略。

內核在遞送一個原來阻塞的訊號給進程時(而不是在產生訊號時),才決定對他的處理方式。於是,進程在訊號遞送給他之前仍可以改變該訊號的處理動作。

3) 遞送狀態:產生的訊號被通知給進程,訊號被處理

06. exec和fork時的訊號語義

6.1程式啟動

exec函數將原來要捕捉的訊號(指定了訊號處理函數的訊號)設置為默認動作(因為訊號捕捉函數的地址很可能在執行的心程式文件中已無意義),而將忽略和默認動作訊號保持原樣。

6.2進程創建

當進程調用fork時,其子進程繼承父進程的訊號處理方式,因為子進程複製了父進程記憶體映像,所以訊號捕獲函數的地址在子進程種有意義。

07. 阻塞訊號集和未決訊號集

訊號的實現手段導致訊號有很強的延時性,但對於用戶來說,時間非常短,不易察覺。

Linux內核的進程式控制制塊PCB是一個結構體,task_struct, 除了包含進程id,狀態,工作目錄,用戶id,組id,文件描述符表,還包含了訊號相關的資訊,主要指阻塞訊號集和未決訊號集。

7.1 阻塞訊號集(訊號屏蔽字)

將某些訊號加入集合,對他們設置屏蔽,當屏蔽x訊號後,再收到該訊號,該訊號的處理將推後(處理髮生在解除屏蔽後)。

7.2 未決訊號集

訊號產生,未決訊號集中描述該訊號的位立刻翻轉為1,表示訊號處於未決狀態。當訊號被處理對應位翻轉回為0。這一時刻往往非常短暫。

訊號產生後由於某些原因(主要是阻塞)不能抵達。這類訊號的集合稱之為未決訊號集。在屏蔽解除前,訊號一直處於未決狀態。

08. 訊號產生函數

8.1 kill函數

#include <sys/types.h>
#include <signal.h>

int kill(pid_t pid, int sig);
功能:給指定進程發送指定訊號(不一定殺死)

參數:
    pid : 取值有 4 種情況 :
        pid > 0:  將訊號傳送給進程 ID 為pid的進程。
        pid = 0 :  將訊號傳送給當前進程所在進程組中的所有進程。
        pid = -1 : 將訊號傳送給系統內所有的進程。
        pid < -1 : 將訊號傳給指定進程組的所有進程。這個進程組號等於 pid 的絕對值。
    sig : 訊號的編號,這裡可以填數字編號,也可以填訊號的宏定義,可以通過命令 kill - l("l" 為字母)進行相應查看。不推薦直接使用數字,應使用宏名,因為不同作業系統訊號編號可能不同,但名稱一致。

返回值:
    成功:0
    失敗:-1

super用戶(root)可以發送訊號給任意用戶,普通用戶是不能向系統用戶發送訊號的。

kill -9 (root用戶的pid) 是不可以的。同樣,普通用戶也不能向其他普通用戶發送訊號,終止其進程。 只能向自己創建的進程發送訊號。

普通用戶基本規則是:發送者實際或有效用戶ID == 接收者實際或有效用戶ID。

程式示例:

int main()
{
    pid_t pid = fork();
    if (pid == 0)
    {//子進程
        int i = 0;
        for (i = 0; i<5; i++)
        {
            printf("in son process\n");
            sleep(1);
        }
    }
    else
    {//父進程
        printf("in father process\n");
        sleep(2);
        printf("kill sub process now \n");
        kill(pid, SIGINT);
    }

    return 0;
}

8.2 raise函數


#include <signal.h>

int raise(int sig);
功能:給當前進程發送指定訊號(自己給自己發),等價於 kill(getpid(), sig)
參數:
    sig:訊號編號
返回值:
    成功:0
    失敗:非0值

8.3 abort函數

#include <stdlib.h>

void abort(void);
功能:給自己發送異常終止訊號 6) SIGABRT,併產生core文件,等價於kill(getpid(), SIGABRT);

參數:無

返回值:無

8.4 alarm函數(鬧鐘)

#include <unistd.h>

unsigned int alarm(unsigned int seconds);
功能:
    設置定時器(鬧鐘)。在指定seconds後,內核會給當前進程發送14)SIGALRM訊號。進程收到該訊號,默認動作終止。每個進程都有且只有唯一的一個定時器。
    取消定時器alarm(0),返回舊鬧鐘餘下秒數。
參數:
    seconds:指定的時間,以秒為單位
返回值:
    返回0或剩餘的秒數

定時,與進程狀態無關(自然定時法)!就緒、運行、掛起(阻塞、暫停)、終止、殭屍……無論進程處於何種狀態,alarm都計時。

測試程式:

int main()
{
    int seconds = 0;

    seconds = alarm(5);
    printf("seconds = %d\n", seconds);

    sleep(2);
    seconds = alarm(5);
    printf("seconds = %d\n", seconds);

    while (1);
    return 0;
}

8.5 setitimer函數(定時器)

#include <sys/time.h>

int setitimer(int which,  const struct itimerval *new_value, struct itimerval *old_value);
功能:
    設置定時器(鬧鐘)。 可代替alarm函數。精度微秒us,可以實現周期定時。
參數:
    which:指定定時方式
        a) 自然定時:ITIMER_REAL → 14)SIGALRM計算自然時間
        b) 虛擬空間計時(用戶空間):ITIMER_VIRTUAL → 26)SIGVTALRM  只計算進程佔用cpu的時間
        c) 運行時計時(用戶 + 內核):ITIMER_PROF → 27)SIGPROF計算佔用cpu及執行系統調用的時間
    new_value:struct itimerval, 負責設定timeout時間
        struct itimerval {
            struct timerval it_interval; // 鬧鐘觸發周期
            struct timerval it_value;    // 鬧鐘觸發時間
        };
        struct timeval {
            long tv_sec;            // 秒
            long tv_usec;           // 微秒
        }
        itimerval.it_value: 設定第一次執行function所延遲的秒數 
        itimerval.it_interval:  設定以後每幾秒執行function

    old_value: 存放舊的timeout值,一般指定為NULL
返回值:
    成功:0
    失敗:-1

示常式序:

void myfunc(int sig)
{
    printf("hello\n");
}

int main()
{
    struct itimerval new_value;

    //定時周期
    new_value.it_interval.tv_sec = 1;
    new_value.it_interval.tv_usec = 0;

    //第一次觸發的時間
    new_value.it_value.tv_sec = 2;
    new_value.it_value.tv_usec = 0;

    signal(SIGALRM, myfunc); //訊號處理
    setitimer(ITIMER_REAL, &new_value, NULL); //定時器設置

    while (1);

    return 0;
}

8.6 pause函數

pause函數使調用它的進程掛起,直到進程捕捉到一個訊號。

#include <unistd.h>
int pause(void);

只有執行了一個訊號處理程式,並從其返回時,pause函數才返回,此時,pause函數返回-1,設置errno為EINTR。

09. 訊號集

9.1 訊號集概述

在PCB中有兩個非常重要的訊號集。一個稱之為「阻塞訊號集」,另一個稱之為「未決訊號集」。

這兩個訊號集都是內核使用點陣圖機制來實現的。但作業系統不允許我們直接對其進行位操作。而需自定義另外一個集合,藉助訊號集操作函數來對PCB中的這兩個訊號集進行修改。

1527930344052

9.2 自定義訊號集函數

為了方便對多個訊號進行處理,一個用戶進程常常需要對多個訊號做出處理,在 Linux 系統中引入了訊號集(訊號的集合)。

這個訊號集有點類似於我們的 QQ 群,一個個的訊號相當於 QQ 群里的一個個好友。

訊號集是一個能表示多個訊號的數據類型,sigset_t set,set即一個訊號集。既然是一個集合,就需要對集合進行添加/刪除等操作。

相關函數說明如下:

#include <signal.h>  

int sigemptyset(sigset_t *set);       //將set集合置空
int sigfillset(sigset_t *set);          //將所有訊號加入set集合
int sigaddset(sigset_t *set, int signo);  //將signo訊號加入到set集合
int sigdelset(sigset_t *set, int signo);   //從set集合中移除signo訊號
int sigismember(const sigset_t *set, int signo); //判斷訊號是否存在

除sigismember外,其餘操作函數中的set均為傳出參數。sigset_t類型的本質是點陣圖。但不應該直接使用位操作,而應該使用上述函數,保證跨系統操作有效。sigset_t類型變數必須使用sigemptyset或sigfillset初始化,以防止該變數所在記憶體位置的原有數據對sigset_t的影響。

示常式序:

int main()
{
    sigset_t set;   // 定義一個訊號集變數
    int ret = 0;

    sigemptyset(&set); // 清空訊號集的內容

    // 判斷 SIGINT 是否在訊號集 set 里
    // 在返回 1, 不在返回 0
    ret = sigismember(&set, SIGINT);
    if (ret == 0)
    {
        printf("SIGINT is not a member of set \nret = %d\n", ret);
    }

    sigaddset(&set, SIGINT); // 把 SIGINT 添加到訊號集 set
    sigaddset(&set, SIGQUIT);// 把 SIGQUIT 添加到訊號集 set

    // 判斷 SIGINT 是否在訊號集 set 里
    // 在返回 1, 不在返回 0
    ret = sigismember(&set, SIGINT);
    if (ret == 1)
    {
        printf("SIGINT is a member of set \nret = %d\n", ret);
    }

    sigdelset(&set, SIGQUIT); // 把 SIGQUIT 從訊號集 set 移除

    // 判斷 SIGQUIT 是否在訊號集 set 里
    // 在返回 1, 不在返回 0
    ret = sigismember(&set, SIGQUIT);
    if (ret == 0)
    {
        printf("SIGQUIT is not a member of set \nret = %d\n", ret);
    }

    return 0;
}

9.3 訊號屏蔽集-sigprocmask函數

訊號阻塞集也稱訊號屏蔽集、訊號掩碼。每個進程都有一個阻塞集,創建子進程時子進程將繼承父進程的阻塞集。訊號阻塞集用來描述哪些訊號遞送到該進程的時候被阻塞(在訊號發生時記住它,直到進程準備好時再將訊號通知進程)。

所謂阻塞並不是禁止傳送訊號, 而是暫緩訊號的傳送。若將被阻塞的訊號從訊號阻塞集中刪除,且對應的訊號在被阻塞時發生了,進程將會收到相應的訊號。

另外,當捕捉到一個訊號時並進入訊號處理函數時,當前訊號會被自動加到訊號屏蔽字中,訊號處理函數返回後再解除屏蔽。

我們可以通過 sigprocmask() 修改當前的訊號掩碼來改變訊號的阻塞情況。

#include <signal.h>

int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
功能:
    檢查或修改訊號阻塞集,根據 how 指定的方法對進程的阻塞集合進行修改,新的訊號阻塞集由 set 指定,而原先的訊號阻塞集合由 oldset 保存。

參數:
    how : 訊號阻塞集合的修改方法,有 3 種情況:
        SIG_BLOCK:向訊號阻塞集合中添加 set 訊號集,新的訊號掩碼是set和舊訊號掩碼的並集。相當於 mask = mask|set。
        SIG_UNBLOCK:從訊號阻塞集合中刪除 set 訊號集,從當前訊號掩碼中去除 set 中的訊號。相當於 mask = mask & ~ set。
        SIG_SETMASK:將訊號阻塞集合設為 set 訊號集,相當於原來訊號阻塞集的內容清空,然後按照 set 中的訊號重新設置訊號阻塞集。相當於mask = set。
    set : 要操作的訊號集地址。
        若 set 為 NULL,則不改變訊號阻塞集合,函數只把當前訊號阻塞集合保存到 oldset 中。
    oldset : 保存原先訊號阻塞集地址

返回值:
    成功:0,
    失敗:-1,失敗時錯誤程式碼只可能是 EINVAL,表示參數 how 不合法。

示例:

image-20211024224620443

PS:屏蔽訊號時,若被屏蔽的訊號出現多次,接觸屏蔽該訊號後,訊號只會被觸發一次,即訊號不支援排隊,且有多個不同訊號觸發時,處理的順序取決於內核的調度。

9.4 未決訊號集-sigpending函數

#include <signal.h>

int sigpending(sigset_t *set);
功能:讀取當前進程的未決訊號集
參數:
    set:未決訊號集
返回值:
    成功:0
    失敗:-1

示例:

image-20211024224633586

10. 訊號捕捉

10.1 訊號處理方式

一個進程收到一個訊號的時候,可以用如下3種方法進行處理:

1)執行系統默認動作

對大多數訊號來說,系統默認動作是用來終止該進程。

2)忽略此訊號(丟棄)

接收到此訊號後沒有任何動作。

3)執行自定義訊號處理函數(捕獲)

用用戶定義的訊號處理函數處理該訊號。

【PS】:SIGKILL 和 SIGSTOP 不能更改訊號的處理方式,因為它們向用戶提供了一種使進程終止的可靠方法。

內核實現訊號捕捉過程:

image-20211024224645774

10.2 signal函數

#include <signal.h>

typedef void(*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
功能:
    註冊訊號處理函數(不可用於 SIGKILL、SIGSTOP 訊號),即確定收到訊號後處理函數的入口地址。此函數不會阻塞。

參數:
    signum:訊號的編號,這裡可以填數字編號,也可以填訊號的宏定義,可以通過命令 kill - l("l" 為字母)進行相應查看。
    handler : 取值有 3 種情況:
          SIG_IGN:忽略該訊號
          SIG_DFL:執行系統默認動作
          訊號處理函數名:自定義訊號處理函數,如:func
          回調函數的定義如下:
            void func(int signo)
            {
                // signo 為觸發的訊號,為 signal() 第一個參數的值
            }

返回值:
    成功:第一次返回 NULL,下一次返回此訊號上一次註冊的訊號處理函數的地址。如果需要使用此返回值,必須在前面先聲明此函數指針的類型。
    失敗:返回 SIG_ERR

【PS】:該函數由ANSI定義,由於歷史原因在不同版本的Unix和不同版本的Linux中可能有不同的行為。因此應該盡量避免使用它,取而代之使用sigaction函數。

示常式序:

// 訊號處理函數
void signal_handler(int signo)
{
    if (signo == SIGINT)
    {
        printf("recv SIGINT\n");
    }
    else if (signo == SIGQUIT)
    {
        printf("recv SIGQUIT\n");
    }
}

int main()
{
    printf("wait for SIGINT OR SIGQUIT\n");

    /* SIGINT: Ctrl+c ; SIGQUIT: Ctrl+\ */
    // 訊號註冊函數
    signal(SIGINT, signal_handler);
    signal(SIGQUIT, signal_handler);

    while (1); //不讓程式結束

    return 0;
}

10.3 sigaction函數

#include <signal.h>

int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
功能:
    檢查或修改指定訊號的設置(或同時執行這兩種操作)。

參數:
    signum:要操作的訊號。
    act:   要設置的對訊號的新處理方式(傳入參數)。
    oldact:原來對訊號的處理方式(傳出參數)。

    如果 act 指針非空,則要改變指定訊號的處理方式(設置),如果 oldact 指針非空,則系統將此前指定訊號的處理方式存入 oldact。

返回值:
    成功:0
    失敗:-1

struct 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、sa_sigaction:訊號處理函數指針,和 signal() 里的函數指針用法一樣,應根據情況給sa_sigaction、sa_handler 兩者之一賦值,有些體系上 struct sigaction使用 union 實現,所以不要同時設置sa_handle和sa_sigaction。其取值如下:
    • SIG_IGN:忽略該訊號
    • SIG_DFL:執行系統默認動作
    • 處理函數名:自定義訊號處理函數
  • sa_mask:訊號阻塞集,在訊號處理函數執行過程中,臨時屏蔽指定的訊號。
  • sa_flags:用於指定訊號處理的行為,通常設置為0,表使用默認屬性。它可以是一下值的「按位或」組合:
    • SA_RESTART:使被訊號打斷的系統調用自動重新發起
    • SA_NOCLDSTOP:使父進程在它的子進程暫停或繼續運行時不會收到 SIGCHLD 訊號。
    • SA_NOCLDWAIT:使父進程在它的子進程退出時不會收到 SIGCHLD 訊號,這時子進程如果退出也不會成為殭屍進程。
    • SA_NODEFER:使對訊號的屏蔽無效,即在訊號處理函數執行期間仍能發出這個訊號。
    • SA_RESETHAND:訊號處理之後重新設置為默認的處理方式。
    • SA_SIGINFO:使用 sa_sigaction 成員而不是 sa_handler 作為訊號處理函數。
  • sa_restorer:被廢棄不再使用

訊號處理函數:

void(*sa_sigaction)(int signum, siginfo_t *info, void *context);
參數說明:
    signum:訊號的編號。
    info:記錄訊號發送進程資訊的結構體。
    context:可以賦給指向 ucontext_t 類型的一個對象的指針,以引用在傳遞訊號時被中斷的接收進程或執行緒的上下文。

示常式序:


void myfunc(int sig)
{
    printf("hello signal: %d\n", sig);
    sleep(5);
    printf("wake up .....\n");
}

int main()
{
    // 註冊訊號捕捉函數
    struct sigaction act;
    act.sa_flags = 0;
    act.sa_handler = myfunc;
    // 設置臨時屏蔽的訊號
    sigemptyset(&act.sa_mask);  // 清空
    // ctrl + 反斜杠
    sigaddset(&act.sa_mask, SIGQUIT);

    sigaction(SIGINT, &act, NULL); //註冊訊號

    while (1);

    return 0;
}

10.4 sigqueue 函數(了解)

#include <signal.h>

int sigqueue(pid_t pid, int sig, const union sigval value);
功能:
    給指定進程發送訊號。
參數:
    pid : 進程號。
    sig : 訊號的編號。
    value : 通過訊號傳遞的參數。
        union sigval 類型如下:
            union sigval
            {
                int   sival_int;
                void *sival_ptr;
            };
返回值:
    成功:0
    失敗:-1

向指定進程發送指定訊號的同時,攜帶數據。但如傳地址,需注意,不同進程之間虛擬地址空間各自獨立,將當前進程地址傳遞給另一進程沒有實際意義。

下面我們做這麼一個例子,一個進程在發送訊號,一個進程在接收訊號的發送。

發送訊號示例程式碼如下:

/*******************************************************
*功能:     發 SIGINT 訊號及訊號攜帶的值給指定的進程
*參數:        argv[1]:進程號 argv[2]:待發送的值(默認為100)
*返回值:   0
********************************************************/
int main()
{
    if (argc >= 2)
    {
        pid_t pid, pid_self;
        union sigval tmp;

        pid = atoi(argv[1]); // 進程號
        if (argc >= 3)
        {
            tmp.sival_int = atoi(argv[2]);
        }
        else
        {
            tmp.sival_int = 100;
        }

        // 給進程 pid,發送 SIGINT 訊號,並把 tmp 傳遞過去
        sigqueue(pid, SIGINT, tmp);

        pid_self = getpid(); // 進程號
        printf("pid = %d, pid_self = %d\n", pid, pid_self);
    }

    return 0;
}

接收訊號示例程式碼如下:

// 訊號處理回調函數
void signal_handler(int signum, siginfo_t *info, void *ptr)
{
    printf("signum = %d\n", signum); // 訊號編號
    printf("info->si_pid = %d\n", info->si_pid); // 對方的進程號
    printf("info->si_sigval = %d\n", info->si_value.sival_int); // 對方傳遞過來的資訊
}

int main()
{
    struct sigaction act, oact;

    act.sa_sigaction = signal_handler; //指定訊號處理回調函數
    sigemptyset(&act.sa_mask); // 阻塞集為空
    act.sa_flags = SA_SIGINFO; // 指定調用 signal_handler

    // 註冊訊號 SIGINT
    sigaction(SIGINT, &act, &oact);

    while (1)
    {
        printf("pid is %d\n", getpid()); // 進程號

        pause(); // 捕獲訊號,此函數會阻塞
    }

    return 0;
}

兩個終端分別編譯程式碼,一個進程接收,一個進程發送,運行結果如下:

image-20211024224659326

10.5 sigsuspend函數

sigsuspend函數在原子操作中先恢復訊號屏蔽字,然後使進程休眠,直到捕捉到一個訊號:

#include <signal.h>
int sigsuspend(const sigset_t* sigmask);
返回值:
    返回-1,並設置errno為EINTR

如果捕捉到一個訊號並從訊號處理程式返回,則sigsuspend返回-1,設置errno為EINTR,並將該進程的訊號屏蔽字設置為調用sigsuspend之前的值。

11. 不可重入、可重入函數

如果有一個函數不幸被設計成為這樣:那麼不同任務調用這個函數時可能修改其他任務調用這個函數的數據,從而導致不可預料的後果。這樣的函數是不安全的函數,也叫不可重入函數。

滿足下列條件的函數多數是不可重入(不安全)的:

  • 函數體內使用了靜態的數據結構;
  • 函數體內調用了malloc() 或者 free() 函數(謹慎使用堆);
  • 函數體內調用了標準 I/O 函數。

相反,肯定有一個安全的函數,這個安全的函數又叫可重入函數。那麼什麼是可重入函數呢?所謂可重入是指一個可以被多個任務調用的過程,任務在調用時不必擔心數據是否會出錯。

保證函數的可重入性的方法:

  • 在寫函數時候盡量使用局部變數(例如暫存器、棧中的變數);
  • 對於要使用的全局變數要加以保護(如採取關中斷、訊號量等互斥方法),這樣構成的函數就一定是一個可重入的函數。

Linux常見的可重入函數:

image-20211024224712006

【PS】:訊號處理函數應該為可重入函數,並且,應該在調用開始時保存errno,在調用後恢復errno。

12. SIGCHLD訊號

12.1 SIGCHLD訊號產生的條件

  1. 子進程終止時

  2. 子進程接收到SIGSTOP訊號停止時

  3. 子進程處在停止態,接受到SIGCONT後喚醒時

12.2 如何避免殭屍進程

  1. 最簡單的方法,父進程通過 wait() 和 waitpid() 等函數等待子進程結束,但是,這會導致父進程掛起。

  2. 如果父進程要處理的事情很多,不能夠掛起,通過 signal() 函數人為處理訊號 SIGCHLD , 只要有子進程退出自動調用指定好的回調函數,因為子進程結束後, 父進程會收到該訊號 SIGCHLD ,可以在其回調函數里調用 wait() 或 waitpid() 回收。

示常式序:


void sig_child(int signo)
{
    pid_t  pid;

    //處理殭屍進程, -1 代表等待任意一個子進程, WNOHANG代表不阻塞
    while ((pid = waitpid(-1, NULL, WNOHANG)) > 0)
    {
        printf("child %d terminated.\n", pid);
    }
}

int main()
{
    pid_t pid;

    // 創建捕捉子進程退出訊號
    // 只要子進程退出,觸發SIGCHLD,自動調用sig_child()
    signal(SIGCHLD, sig_child);

    pid = fork();   // 創建進程
    if (pid < 0)
    { // 出錯
        perror("fork error:");
        exit(1);
    }
    else if (pid == 0)
    { // 子進程
        printf("I am child process,pid id %d.I am exiting.\n", getpid());
        exit(0);
    }
    else if (pid > 0)
    { // 父進程
        sleep(2);   // 保證子進程先運行
        printf("I am father, i am exited\n\n");
        system("ps -ef | grep defunct"); // 查看有沒有殭屍進程
    }

    return 0;
}
  1. 如果父進程不關心子進程什麼時候結束,那麼可以用signal(SIGCHLD, SIG_IGN)通知內核,自己對子進程的結束不感興趣,父進程忽略此訊號,那麼子進程結束後,內核會回收, 並不再給父進程發送訊號。

示常式序:

int main()
{
    pid_t pid;

    // 忽略子進程退出訊號的訊號
    // 那麼子進程結束後,內核會回收, 並不再給父進程發送訊號
    signal(SIGCHLD, SIG_IGN);

    pid = fork();   // 創建進程

    if (pid < 0)
    { // 出錯
        perror("fork error:");
        exit(1);
    }
    else if (pid == 0)
    { // 子進程
        printf("I am child process,pid id %d.I am exiting.\n", getpid());
        exit(0);

    }
    else if (pid > 0)
    { // 父進程
        sleep(2);   // 保證子進程先運行
        printf("I am father, i am exited\n\n");
        system("ps -ef | grep defunct"); // 查看有沒有殭屍進程
    }

    return 0;
}

13. 參考資料

  1. 《APUE》 3\e.