Linux信號處理編程


01. 學習目標

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

02. 信號概述

信號的概念

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

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

這里我們學習的“信號”就是屬於這么一種“中斷”。我們在終端上敲“Ctrl+c”,就產生一個“中斷”,相當於產生一個信號,接着就會處理這么一個“中斷任務”(默認的處理方式為中斷當前進程)。

信號的特點

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

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

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

image-20211024224531360

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

03. 信號的編號(了解)

信號編號

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

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

image-20211028125856411

不存在編號為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的一個特性是,如果進程在執行一個低速系統調用而阻塞期間捕捉到一個信號,則該系統調用就被中斷而不再執行,該系統調用出錯返回,設置errnoEINTR

【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,設置errnoEINTR

09. 信號集

9.1 信號集概述

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

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

image-20211028125924013

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_handlesa_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.


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM