1. 程式人生 > 其它 >Linux 系統程式設計 學習:3-程序間通訊1:Unix IPC(2)訊號

Linux 系統程式設計 學習:3-程序間通訊1:Unix IPC(2)訊號

背景

上一講我們介紹了Unix IPC中的2種管道。

回顧一下上一講的介紹,IPC的方式通常有:

  • Unix IPC包括:管道(pipe)、命名管道(FIFO)與訊號(Signal)
  • System V IPC:訊息佇列、訊號量、共享記憶體
  • Socket(支援不同主機上的兩個程序IPC)

我們在這一講介紹Unix IPC,中有關訊號(Signal)的處理。

訊號(Signal)

Signal :程序給作業系統或程序的某種資訊,讓作業系統或者其他程序做出某種反應。

訊號是程序間通訊機制中唯一的非同步通訊機制,(一個程序不必通過任何操作來等待訊號,然而,程序也不知道訊號到底何時到達。)

程序之間可以互相通過系統呼叫kill傳送軟中斷訊號。核心也可以因為內部事件而給程序傳送訊號,通知程序發生了某個事件。

訊號機制除了基本通知功能外,還可以傳遞附加資訊(呼叫 sigaction函式)。

程序檢查是否收到訊號的時機是:一個程序在即將從核心態返回到使用者態時;或者,在一個程序要進入或離開一個適當的低排程優先順序睡眠狀態時。

核心實現訊號捕捉的過程:
1.使用者態的程式,在執行流程中因為中斷、異常或者系統呼叫進入核心態
2.核心處理完異常準備返回使用者模式之前,先處理當前程序中可以遞送的訊號
3.如果訊號的處理動作是自定義的訊號處理函式,則回到使用者模式執行訊號處理函式(而不是返回使用者態程式的主執行流程)
4.訊號處理函式返回時,執行特殊的系統呼叫"sigreturn"再次進入核心
5.返回使用者態,從上次被打斷的地方繼續向下執行

Linux下當向一個程序發出訊號時,從訊號產生到程序接收該訊號並執行相應操作的過程稱為訊號的等待過程。如果某一個訊號沒有被程序遮蔽,則我們可以在程式中阻塞程序對該訊號所相應的操作。

例如一個程式當接收到SIGUSR1訊號時會進行一個操作,我們可以利用系統API阻塞(block)程式對該訊號的操作,直到我們解除阻止。
再舉個現實的例子:就好像一個同學讓我幫他帶飯,但是我現在有其他事要做,現在我先做我手頭上的事,直到我把手上的事都完成才去幫他帶飯。整個過程差不多就是這樣子。

訊號的特點:簡單、不能攜帶大量資訊、滿足某個特設條件才傳送。

利用訊號來處理子程序的回收是非常方便和高效的,因為所有的工作你都可以交給核心。

例如:當子程序終止時,會發出一個訊號,一旦你註冊的訊號捕捉器捕捉到了這個訊號,那麼就可以去回撥自己的函式(處理處理子程序的函式),去回收子程序了。

訊號的分類

Linux的signal.h中定義了很多訊號,使用命令kill -l可以檢視系統定義的訊號列表。

bash
1) SIGHUP       終端的控制程序結束,通知session內的各個作業,脫離關係 
2) SIGINT       程式終止訊號(Ctrl+c)
3) SIGQUIT      和2號訊號類似(Ctrl+\),產生core檔案
4) SIGILL       執行了非法指令,可執行檔案本身出現錯誤 
5) SIGTRAP      有斷點指令或其他trap指令產生,有debugger使用
6) SIGABRT      呼叫abort函式生成的訊號 
7) SIGBUS       非法地址(記憶體地址對齊出錯)
8) SIGFPE       致命的算術錯誤(浮點數運算,溢位,及除數為0 錯誤)
9) SIGKILL      用來立即結束程式的執行(不能為阻塞,處理,忽略)
10) SIGUSR1     使用者使用 

11) SIGSEGV     訪問記憶體錯誤
12) SIGUSR2     使用者使用
13) SIGPIPE     管道破裂
14) SIGALRM     時鐘定時訊號
15) SIGTERM     程式結束訊號(可被阻塞,處理)
16) SIGSTKFLT   協處理器棧堆錯誤
17) SIGCHLD     子程序結束,父程序收到這個訊號並進行處理,(wait也可以)否則殭屍程序
18) SIGCONT     讓一個停止的程序繼續執行(不能被阻塞)
19) SIGSTOP     讓一個程序停止執行(不能被阻塞,處理,忽略)
20) SIGTSTP     停止程序的執行(可以被處理和忽略)

21) SIGTTIN     當後臺作業要從使用者終端讀資料時, 該作業中的所有程序會收到SIGTTIN訊號. 預設時這些程序會停止執行.
22) SIGTTOU     類似SIGTTIN,但在寫終端時收到
23) SIGURG      有緊急資料或者out—of—band 資料到達socket時產生
24) SIGXCPU     超過CPU資源限定,這個限定可改變
25) SIGXFSZ     當程序企圖擴大檔案以至於超過檔案大小資源限制
26) SIGVTALRM   虛擬時鐘訊號(計算的是該程序佔用的CPU時間)
27) SIGPROF     時鐘訊號(程序用的CPU時間及系統呼叫時間)
28) SIGWINCH    視窗大小改變時發出
29) SIGIO       檔案描述符準備就緒,可以進行讀寫操作
30) SIGPWR      power failure
31) SIGSYS      非法的系統呼叫

(沒有32與33訊號)

34) SIGRTMIN    35) SIGRTMIN+1  36) SIGRTMIN+2  37) SIGRTMIN+3
38) SIGRTMIN+4  39) SIGRTMIN+5  40) SIGRTMIN+6  41) SIGRTMIN+7  42) SIGRTMIN+8
43) SIGRTMIN+9  44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9  56) SIGRTMAX-8  57) SIGRTMAX-7
58) SIGRTMAX-6  59) SIGRTMAX-5  60) SIGRTMAX-4  61) SIGRTMAX-3  62) SIGRTMAX-2
63) SIGRTMAX-1  64) SIGRTMAX

非實時訊號(1-31)

非實時訊號也叫不可靠訊號,有如下特點:
1)不可靠訊號(不可靠訊號 和 可靠訊號 的區別在於前者不支援排隊,可能會造成訊號丟失,而 可靠訊號 不會)
2)訊號是有可能丟失的
3)非實時訊號的產生都對應一個系統事件、每一個訊號都有一個預設的系統動作
4)巢狀執行(但是可能會造成丟失)
5)被阻塞訊號是沒有優先順序
6)在掛起的訊號中,訊號的執行時亂序的

在以上列出的訊號中,

  • 不可捕獲、阻塞或忽略的訊號有:SIGKILL,SIGSTOP
  • 不能恢復至預設動作的訊號有:SIGILL,SIGTRAP
  • 預設會導致程序流產的訊號有:SIGABRT,SIGBUS,SIGFPE,SIGILL,SIGIOT,SIGQUIT,SIGSEGV,SIGTRAP,SIGXCPU,SIGXFSZ
  • 預設會導致程序退出的訊號有:SIGALRM,SIGHUP,SIGINT,SIGKILL,SIGPIPE,SIGPOLL,SIGPROF,SIGSYS,SIGTERM,SIGUSR1,SIGUSR2,SIGVTALRM
  • 預設會導致程序停止的訊號有:SIGSTOP,SIGTSTP,SIGTTIN,SIGTTOU
  • 預設程序忽略的訊號有:SIGCHLD,SIGPWR,SIGURG,SIGWINCH

此外,SIGIO在SVR4是退出,在4.3BSD中是忽略;SIGCONT在程序掛起時是繼續,否則是忽略,不能被阻塞

實時訊號(34-64)

實時訊號也叫可靠訊號,有如下特點:
1)可靠的訊號(不管來幾個都會響應)
2)訊號是不會丟失的
3)巢狀執行
4)被掛起的訊號是有優先順序(值越大,優先順序越高)
5)被掛起時實時訊號訊號比非實時訊號優先順序要高
6)被掛起順序執行訊號的響應

使用訊號的注意事項

exec 與 訊號
exec函式執行後, 把該程序所有訊號設為預設動作。
exec函式執行後, 把原先要捕捉的訊號設為預設,其他不變。

子程序 與 訊號
1)訊號初始化的設定是會被子程序繼承的
2)訊號的阻塞設定是會被子程序繼承的
3)被掛起的訊號是不會被子程序繼承的

自定義訊號最好從 SIGRTMIN 開始,但最好保留前幾個。比如SIGRTMIN+10。

訊號的使用

步驟:
0)事先對一個訊號進行註冊,事先對一個訊號進行註冊,改變訊號的響應事件:執行預設動作、忽略(丟棄)、捕捉(呼叫戶處理函式)
1)改變訊號的阻塞方式
2)觸發某種條件產生訊號 或 使用者自己傳送一個訊號;等待訊號
3)處理訊號:執行預設動作、忽略(丟棄)、捕捉(呼叫戶處理函式)

在預設情況下,當一個事件觸發了對應的訊號時,會使程序發生對應的動作。我們也可以人為地改變程序對於某個訊號的處理,達到控制響應訊號的目的。

註冊訊號處理 函式

在系統中,提供了2個訊號的註冊處理函式:signal、sigaction;用於改變程序接收到特定訊號後的行為。

希望能用相同方式處理一種訊號的多次出現,最好用sigaction,訊號只出現並處理一次,可以用signal。
由於歷史原因,在不同版本核心中可能有不同的行為,使用signal呼叫會有相容性問題,所以推薦使用訊號註冊函式sigaction。

訊號處理函式裡面應該注意的地方(討論關於編寫安全的訊號處理函式):
1)區域性變數的相關處理,儘量只執行簡單的操作 (不應該在訊號處理函式裡面操作全域性變數)
2)“volatile sig_atomic_t”型別的全域性變數的相關操作
3)呼叫非同步訊號安全的相關函式(不可重入函式就不是非同步訊號安全函式)
4)errno 是執行緒安全,即每個執行緒有自己的 errno,但不是非同步訊號安全。如果訊號處理函式比較複雜,且呼叫了可能會改變 errno 值的庫函式,必須考慮在訊號處理函式開始時儲存、結束的時候恢復被中斷執行緒的 errno 值;

非同步訊號安全函式(async-signal-safe)是指:“在該函式內部即使因為訊號而正在被中斷,在其他的地方該函式再被呼叫了也沒有任何問題”。如果函式中存在更新靜態區域裡的資料的情況(例如,malloc),一般情況下都是不全的非同步訊號函式。但是,即使使用靜態資料,如果在這裡這個資料時候把訊號遮蔽了的話,它就會變成非同步訊號安全函數了。

signal 函式

c
#include<signal.h>

typedefvoid(*sighandler_t)(int);
sighandler_tsignal(int signum, sighandler_t handler);

// void (*signal(int signum, void (*hand)(int)))(int); // 如果將第二行和第三行拆分,展開以後就是這個樣子

引數解析:
signum :要處理的訊號值(不能是 9-SIGKILL,19-SIGSTOP;32、33不存在)
handler:處理方式

  • SIG_IGN :忽略該訊號,即無動作
  • SIG_DFL :重置設為預設方式
  • function:當訊號產生時,呼叫函式名為 函式名 的函式,該函式必須是void function(int signum)類似的一個帶有1個整形引數,無返回值的函式。

返回值:

  • 成功返回之前的handler;
  • 失敗返回 SIG_ERR,且設定errno。
c
#include<stdio.h>
#include<unistd.h>
#include<signal.h>

voidsignal_hander(int signum){
    printf("signum is %d\n", signum);
    sleep(1);
    return;
}

intmain(int argc, char *argv[]){
    signal(SIGINT, signal_hander); // 註冊 SIGINT 的訊號處理函式。
    while(1)
    {
        sleep(2);
        // printf("sleep loop\n");
    }
    return 0;
}

sigaction

sigaction是比signal更高階的訊號註冊響應函式,能夠提供更多功能;可以搭配 sigqueue 來使用。

c
#include<signal.h>

intsigaction(int signum, const struct sigaction *act,
              struct sigaction *oldact);

引數解析(如果把 act、oldact 引數都設為NULL,那麼該函式可用於檢查訊號的有效性。)

  • signum:註冊訊號
  • act:訊號活動結構體,其中包含了對指定訊號的處理、訊號所傳遞的資訊、訊號處理函式執行過程中應遮蔽掉哪些函式等等
c
/* 需要用到以下結構體 */
structsigaction {
    // sa_handler、sa_sigaction除了可以是使用者自定義的處理函式外,還可以為SIG_DFL(採用預設的處理方式),也可以為SIG_IGN(忽略訊號)。
   void     (*sa_handler)(int); //只有一個引數,即訊號值,所以訊號不能傳遞除訊號值之外的任何資訊
   void     (*sa_sigaction)(int, siginfo_t *, void *);
            // 帶參的訊號處理函式,如果你想要使能這個訊號處理函式,需要設定一下sa_flags為SA_SIGINFO
            // 帶有三個引數,是為實時訊號而設的(當然同樣支援非實時訊號),它指定一個3引數訊號處理函式。
             - 第一個引數為訊號值,第三個引數沒有使用(posix沒有規範該引數的標準),
             - 第二個引數是指向siginfo_t結構的指標,結構中包含訊號攜帶的資料值
             siginfo_t {
               int      si_signo;     /* Signal number */ //訊號編號
               int      si_errno;     /* An errno value */ //如果為非零值則錯誤程式碼與之關聯 
               int      si_code;      /* Signal code */   // //說明程序如何接收訊號以及從何處收到
                         SI_USER:通過 kill 函式收到
                         SI_KERNEL:來自kernel.
                         SI_QUEUE:來自 sigqueue 函式.
                         SI_TIMER:來自 POSIX 規範的 timer.
                         SI_MESGQ (since Linux 2.6.6): POSIX message queue state changed; see mq_notify(3).
                         SI_ASYNCIO: AIO completed.
                         SI_SIGIO:Queued  SIGIO(only in kernels up to Linux 2.2; from Linux 2.4 onward SIGIO/SIGPOLL fills in si_code as described below).
                         SI_TKILL(since Linux 2.4.19): 來自 tkill 函式 或 tgkill 函式.


               int      si_trapno;    /* Trap number that caused
                                         hardware-generated signal
                                         (unused on most architectures) */
               pid_t    si_pid;       /* Sending process ID *///適用於SIGCHLD,代表被終止程序的PID 
               uid_t    si_uid;       /* Real user ID of sending process *///適用於SIGCHLD,代表被終止程序所擁有程序的UID 
               int      si_status;    /* Exit value or signal *///適用於SIGCHLD,代表被終止程序的狀態 
               clock_t  si_utime;     /* User time consumed *///適用於SIGCHLD,代表被終止程序所消耗的使用者時間 
               clock_t  si_stime;     /* System time consumed *///適用於SIGCHLD,代表被終止程序所消耗系統的時間
               sigval_t si_value;     /* Signal value */
               int      si_int;       /* POSIX.1b signal */
               void    *si_ptr;       /* POSIX.1b signal */
               int      si_overrun;   /* Timer overrun count;
                                         POSIX.1b timers */
               int      si_timerid;   /* Timer ID; POSIX.1b timers */
               void    *si_addr;      /* Memory location which caused fault */
               long     si_band;      /* Band event (was int in
                                         glibc 2.3.2 and earlier) */
               int      si_fd;        /* File descriptor */
               short    si_addr_lsb;  /* Least significant bit of address
                                         (since Linux 2.6.32) */
               void    *si_call_addr; /* Address of system call instruction
                                         (since Linux 3.5) */
               int      si_syscall;   /* Number of attempted system call
                                         (since Linux 3.5) */
               unsigned int si_arch;  /* Architecture of attempted system call
                                         (since Linux 3.5) */
            }
                


   sigset_t   sa_mask;    //訊號阻塞設定,用來設定在處理該訊號時暫時將sa_mask 指定的訊號集擱置
                                    (在訊號處理函式執行的過程當中阻塞掉指定的訊號集,指定的訊號過來將會被掛起,等函式結束後再執行)

   int        sa_flags;   //訊號的操作標識,可以是以下的值(設定為0,代表使用預設屬性)
        SA_NOCLDSTOP:使父程序在它的子程序暫停或繼續執行時不會收到 SIGCHLD 訊號。(此標誌只有在設立SIGCHLD的處理程式時才有意義)
        SA_NOCLDWAIT:使父程序在它的子程序退出時不會收到 SIGCHLD 訊號,這時子程序如果退出也不會成為殭屍程序。(此標誌只有在設立SIGCHLD的處理程式時才有意義 或 設定回 SIG_DFL)
        SA_NODEFER :如果設定了 SA_NODEFER標記, 那麼在該訊號處理函式執行時,核心將不會阻塞該訊號。(一般情況下, 當訊號處理函式執行時,核心將阻塞該給定訊號)
        SA_ONSTACK:在sigaltstack提供的備用訊號堆疊上呼叫訊號處理程式。如果備用堆疊不可用,將使用預設堆疊。(此標誌只有在建立訊號處理程式時才有意義)
        SA_RESETHAND:當呼叫訊號處理函式時,將訊號的處理函式重置為預設值SIG_DFL(此標誌只有在建立訊號處理程式時才有意義)
        SA_RESTART:如果訊號中斷了程序的某個系統呼叫,則系統自動啟動該系統呼叫
        SA_RESTORER:不適用於程式使用。參考"sigreturn"
        SA_SIGINFO:使用 sa_sigaction 成員而不是 sa_handler 作為訊號處理函式。

   void     (*sa_restorer)(void); //被遺棄的設定
};

  • oldact:原本的設定會被儲存在這裡,如果是NULL則不儲存

返回值:

  • 成功返回 0;
  • 失敗返回 -1,且設定errno。

我們來看一個 sigaction 與 sigqueue 配合的例程。

c
#include<stdio.h>
#include<unistd.h>
#include<signal.h>
#include<stdlib.h>

voidmysa_handler(int signum){
    printf("sa_handler %d\n", signum);
    return ;
}

voidmysa_sigaction(int signo, siginfo_t *info,void *ctx){
    //以下兩種方式都能獲得sigqueue發來的資料
    printf("receive the data from siqueue by info->si_int is %d\n",info->si_int);
    printf("receive the data from siqueue by info->si_value.sival_int is %d\n",info->si_value.sival_int);
}

intmain(void){
    structsigactionact;
    // 下面的巨集 區分了2種 訊號響應方式
#if 1
    // act.sa_handler = SIG_DFL; 預設動作
    act.sa_handler = mysa_handler;
#else
    act.sa_sigaction = mysa_sigaction;
    act.sa_flags = SA_SIGINFO;//資訊傳遞開關
#endif
    sigemptyset(&act.sa_mask);
    if(sigaction(SIGINT,&act,NULL) == -1){
        perror("sigaction error");
        exit(EXIT_FAILURE);
    }
    sleep(2);
    union sigval mysigval;
    mysigval.sival_int = 100;
    if(sigqueue(getpid(),SIGINT,mysigval) == -1){
        perror("sigqueue error");
        exit(EXIT_FAILURE);
    }

    return 0;
}

訊號集 與 訊號阻塞 、未決

在PCB中有兩個非常重要的訊號集。一個稱之為“阻塞訊號集”,另一個稱之為“未決訊號集”。這兩個訊號集都是核心使用點陣圖機制來實現的。
作業系統不允許我們直接對其進行位操作。而需自定義另外一個集合,藉助訊號集操作函式來對PCB中的這兩個訊號集進行修改。

有關概念:

執⾏訊號的處理動作稱為訊號遞達(Delivery),訊號從產⽣到遞達之間的狀態,稱為訊號未決(Pending)。
程序可以選擇阻塞(Block)某個訊號。被阻塞的訊號產⽣時將保持在未決狀態,直到程序解除對此訊號的阻塞,才執⾏遞達的動作。
注意,阻塞和忽略是不同的,只要訊號被阻塞就不會遞達,⽽忽略是在遞達之後可選的⼀種處理動作。

每個程序都有一個用來描述哪些訊號遞送到程序時將被阻塞的訊號集,該訊號集中的所有訊號在遞送到程序後都將被阻塞。

  • block集(阻塞集、遮蔽集):一個程序所要遮蔽的訊號,在對應要遮蔽的訊號位置1
  • pending集(未決訊號集):如果某個訊號在程序的阻塞集中,則也在未決集中對應位置1,表示該訊號不能被遞達,不會被處理
  • handler(訊號處理函式集):表示每個訊號所對應的訊號處理函式,當訊號不在未決集中時,將被呼叫

阻塞訊號:對指定的訊號進行掛起,直到解除訊號的阻塞狀態以後,才去響應這個訊號。
忽略訊號:收到了訊號,但忽略對其的響應。(不執行任何動作)

訊號集有關的函式

以下是與訊號阻塞及未決相關的函式操作:

c
#include<signal.h>

intsigemptyset(sigset_t *set);                 // 清空宣告的訊號集
intsigfillset(sigset_t *set);                  // 將所有訊號登記進集合裡面

intsigaddset(sigset_t *set, int signum);       // 往集合集裡面新增signum訊號
intsigdelset(sigset_t *set, int signum);       // 往集合裡面刪除signum訊號

intsigismember(constsigset_t *set, int signum); // 測試訊號集合裡面有無signum訊號

intsigprocmask(int  how,  constsigset_t *set, sigset_t *oldset)); // 設定程序的訊號掩碼intsigpending(sigset_t *set));                // 獲得當前已遞送到程序,卻被阻塞的所有訊號,在set指向的訊號集中返回結果。
intsigsuspend(constsigset_t *mask));         // 用於在接收到某個訊號之前, 臨時用mask替換程序的訊號掩碼, 並暫停程序執行,直到收到訊號為止。 我們會在竟態中講到它

訊號集使用步驟
A. 宣告sigset_t *型別的訊號集變數#set
B. 清空宣告的訊號集(sigemptyset)
C. 向#set 中增刪訊號,可以使用以下有關函式進行操作

改變訊號的阻塞方式

更改程序的訊號遮蔽字可以阻塞所選擇的訊號,或解除對它們的阻塞。使用這種技術可以保護不希望由訊號中斷的程式碼臨界區。

c
#include<signal.h>

intsigprocmask(int how, constsigset_t *set, sigset_t *oldset);

引數解析:
how :阻塞模式

  • SIG_BLOCK:新增訊號集合裡面的訊號進行阻塞(原本的設定上新增設定),mask=mask|set
  • SIG_UNBLOCK:解除訊號集合裡面的訊號的阻塞,mask=mask|~set
  • SIG_SETMASK:直接阻塞訊號集合裡面的訊號,原本的設定直接被覆蓋,mask=set

set :設定的訊號集合
oldset :此前的設定

  • 填入:oldset,則將原本的設定儲存在這裡
  • 填入:NULL則不做任何操作

返回值:成功返回0;失敗返回-1,設定errno:

  • EFAULT :set或oldset引數指向非法地址。
  • EINVAL :how中指定的值無效。

使用阻塞訊號集阻塞訊號的例程:

c
/*
    說明:程式首先將SIGINT訊號加入程序阻塞集(遮蔽集)中,一開始並沒有傳送SIGINT訊號,所以程序未決集中沒有處於未決態的訊號。
    當我們連續按下ctrl+c時,向程序傳送SIGINT訊號;由於SIGINT訊號處於程序的阻塞集中,所以傳送的SIGINT訊號不能遞達,也是就是處於未決狀態,所以當我列印未決集合時發現SIGINT所對應的位為1。
    現在我們按下ctrl+\,傳送SIGQUIT訊號,由於此訊號並沒被程序阻塞,所以SIGQUIT訊號直接遞達,執行對應的處理函式,在該處理函式中解除程序對SIGINT訊號的阻塞。
    所以之前傳送的SIGINT訊號遞達了,執行對應的處理函式,但由於SIGINT訊號是不可靠訊號,不支援排隊,所以最終只有一個訊號遞達。
*/
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<errno.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<signal.h>

voidprintsigset(constsigset_t *pset){
    int i = 0;
    for (; i < 64; i++) //遍歷64個訊號,
    {
        //訊號從1開始   判斷哪些訊號在訊號未決狀態字中
        if (sigismember(pset, i + 1))
            putchar('1');
        else
            putchar('0');
    }
    printf("\n");
}

voidcatch_signal(int sign){
    switch (sign)
    {
    case SIGINT:
        printf("accept SIGINT!\n");
        exit(0);
        break;
    case SIGQUIT:
        printf("accept SIGQUIT!\n");
        //取消訊號阻塞
        
        sigset_t uset; //定義訊號集
        sigemptyset(&uset); //清空訊號集
        sigaddset(&uset,SIGINT); //將SIGINT訊號加入到訊號集中

        //進行位異或操作,將訊號集uset更新到程序控制塊PCB結構中,取消阻塞訊號SIGINT
        sigprocmask(SIG_UNBLOCK,&uset,NULL);
        break;
    }
}

intmain(int arg, char *args[]){
    //定義未決訊號集(pending)
    sigset_t pset;
    //定義阻塞訊號集(block)
    sigset_t bset;
    //清空訊號集
    sigemptyset(&bset);
    //將訊號SIGINT加入到訊號集中
    sigaddset(&bset, SIGINT);
    //註冊訊號
    if (signal(SIGINT, catch_signal) == SIG_ERR)
    {
        perror("signal error");
        return -1;
    }
    if (signal(SIGQUIT, catch_signal) == SIG_ERR)
    {
        perror("signal error");
        return -1;
    }
    //進行位或操作,將訊號集bset更新到程序控制塊PCB結構中,阻塞訊號SIGINT(即使使用者按下ctrl+c,訊號也不會遞達)
    sigprocmask(SIG_BLOCK, &bset, NULL);
    while (1)
    {
        /*
         * 獲取當前訊號未決資訊,即使在sigprocmask()函式中設定了訊號阻塞,
         * 但是如果沒有訊號的到來,訊號未決狀態字對應位依然是0
         * 只要有訊號到來,並且被阻塞了,訊號未決狀態字對應位才會是1
         * */
        sigpending(&pset);
        //列印訊號未決資訊
        printsigset(&pset);
        sleep(2);
    }
    return 0;
}

傳送一個訊號

手動傳送一個訊號,有下面這些函式:killraisesigqueuealarmualarmabort

kill 函式

描述:向 程序/程序組 傳送訊號。

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

intkill(pid_t pid, int sig);

引數解析:

  • pid

如果pid為正,則將訊號sig傳送到由pid指定ID的程序。
如果pid等於0,那麼sig將傳送到呼叫程序的程序組中的每個程序。用kill(0,sig)傳送自定義訊號時,本程序和所有子程序(通過exec等方式啟動的)都必須有對應的處理函式,否則所有程序都會退出。
如果pid等於-1,則sig被髮送到呼叫程序有權傳送訊號的每個程序,程序1(init)除外,要使程序具有傳送訊號的許可權,它必須具有特權(在Linux下:具有CAP_KILL功能),或者傳送程序的真實或有效使用者ID必須等於目標程序的真實或已儲存的設定使用者ID。在SIGCONT的情況下,當傳送和接收程序屬於同一會話時就足夠了。
如果pid小於-1,那麼sig將傳送到程序組中ID為-pid的每個程序。

  • sig :如果sig為0,則不傳送訊號,但仍會執行錯誤檢查;這可用於檢查是否存在程序ID或程序組ID。

**返回值說明: **
成功執行時,返回0。
失敗返回-1,errno被設為以下的某個值 :

  • EINVAL:指定的訊號碼無效(引數 sig 不合法)
  • EPERM;許可權不夠無法傳送訊號給指定程序
  • ESRCH:引數 pid 所指定的程序或程序組不存在

raise 函式

描述:對呼叫的程序/執行緒自身傳送一個訊號,相當於kill(getpid(), sig);pthread_kill(pthread_self(), sig);

c
#include<signal.h>

intraise(int sig);

alarm 函式

描述:設定訊號SIGALRM在經過引數seconds指定的秒數後傳送給目前的程序。每個程序都有且只有唯一的一個定時器。無論程序處於何種狀態,alarm都計時。

c
#include<unistd.h>

unsignedintalarm(unsignedint seconds);

返回值:返回值為上次定時呼叫到傳送之間剩餘的時間,或者因為沒有前一次定時呼叫而返回0。

注意:

  • 如果指定的引數seconds為0,則不再發送 SIGALRM訊號。後一次設定將取消前一次的設定。
  • 在使用時,alarm只設定為傳送一次訊號,如果要多次傳送,就要多次使用alarm呼叫。
  • SIGALRM訊號如果不處理(使用者捕獲 或者 忽略),會使程序exit
  • 在某些系統中,SIGALRM訊號會預設中斷系統呼叫(inturrupt),當然也有的系統,預設情況下是使系統呼叫被中斷後自動重新開始(restart automatically)

ualarm 函式

描述:將使當前程序在指定時間(第一個引數,以us位單位)內產生SIGALRM訊號,然後每隔指定時間(第2個引數,以us位單位)重複產生SIGALRM訊號,如果執行成功,將返回0。

c
#include<unistd.h>

useconds_tualarm(useconds_t usecs, useconds_t interval);

abort 函式

abort 函式:給自己傳送異常終止訊號 SIGABRT 訊號,終止併產生core檔案。該函式無返回

c
#include<stdlib.h>

voidabort(void);

注意:如果SIGABRT訊號被忽略,或被返回的處理程式捕獲,它會在被捕獲處理完成以後終止程序。(通過恢復SIGABRT的預設配置,然後再次傳送SIGABRT訊號來實現這一點。)

段坤我吃定了,誰也留不住他。

sigqueue 函式

描述:作為新的傳送訊號系統呼叫,主要是針對實時訊號提出的支援訊號帶有引數,與函式sigaction()配合使用。在傳送訊號同時,就可以讓訊號傳遞一些附加資訊。這對程式開發是非常有意義的。

c
#include<signal.h>

intsigqueue(pid_t pid, int sig, constunion sigval value);

union sigval {
        int   sival_int;
        void *sival_ptr;
};

引數解析:
pid : 給定的程序號(PID只有為正數的情況,不比 signal 那麼複雜)
sig :訊號值
value:(聯合體)可以是數值,也可以是指標;不同程序之間虛擬地址空間各自獨立,將當前程序地址傳遞給另一程序沒有實際意義。但是訊號可以回撥,在回撥的時候,自己給自己捕捉住,傳遞地址就有意義了。

原理:

  • 當呼叫sigqueue時,引數 value 就 拷貝到訊號處理函式的第二個引數sa_sigaction中。
  • 這樣,sigaction引數中的 act->siginfo_t.si_value與sigqueue中的第三個引數value關聯;
  • 所以通過siginfo_t.si_value可以獲得sigqueue(pid_t pid, int sig, const union sigval val)第三個引數傳遞過來的資料。
  • 如:sigaction引數中的 act->siginfo_t.si_value.sival_int或sigaction引數中的 act->siginfo_t.si_value.sival_ptr

對於父子程序接收訊號的處理方式

父子程序都會收到訊號,按各自的方式處理。

c
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<signal.h>

voidsignal_handler_son(int signum){
    printf("signal_handler_son%d\n", signum);
}
voidsignal_handler_father(int signum){
    printf("signal_handler_father%d\n", signum);
}

intmain(int argc, char *argv[]){
    pid_t pid = fork();
    if(pid == 0)
    {
        signal(SIGINT, signal_handler_son);
        while(1)
        {
            sleep(2);  printf("Son loop\n");
        }
        exit(123);
    }else if(pid > 0)
    {
        signal(SIGINT, signal_handler_father);
        while(1)
        {
            sleep(2);  printf("Father loop\n");
        }
        wait(NULL);
    }

    return 0;
}

藉助SIGCHLD訊號回收子程序

子程序結束執行,其父程序會收到SIGCHLD訊號。該訊號的預設處理動作是忽略。可以捕捉該訊號,在捕捉函式中完成子程序狀態的回收。
SIGCHLD的產生條件:

  • 子程序終止時
  • 子程序接收到SIGSTOP訊號停止時
  • 子程序處在停止態,接受到SIGCONT後喚醒時

下面的程式介紹瞭如何使用 SIGCHLD 回收子程序

c
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/wait.h>
#include<signal.h>
 
voidsys_err(char *str){
    perror(str);
    exit(1);
}
voiddo_sig_child(int signo){
    int status;    pid_t pid;
    while ((pid = waitpid(0, &status, WNOHANG)) > 0) {
        if (WIFEXITED(status))
            printf("child %d exit %d\n", pid, WEXITSTATUS(status));
        else if (WIFSIGNALED(status))
            printf("child %d cancel signal %d\n", pid, WTERMSIG(status));
    }
}
intmain(void){
    pid_t pid;    int i;
    for (i = 0; i < 10; i++) {
        if ((pid = fork()) == 0)
            break;
        else if (pid < 0)
            sys_err("fork");
    }
    if (pid == 0) {   
        int n = 1;
        while (n--) {
            printf("child ID %d\n", getpid());
            sleep(1);
        }
        return i+1;
    } else if (pid > 0) {
        struct sigaction act;
        act.sa_handler = do_sig_child;
        sigemptyset(&act.sa_mask);
        act.sa_flags = 0;
        sigaction(SIGCHLD, &act, NULL);
       
        while (1) {
            printf("Parent ID %d\n", getpid());
            sleep(1);
        }
    }
    return 0;
}

SIGCHLD訊號注意問題:

  • 子程序繼承了父程序的訊號遮蔽字和訊號處理動作,但子程序沒有繼承未決訊號集spending。
  • 注意註冊訊號捕捉函式的位置。
  • 應該在fork之前,阻塞SIGCHLD訊號。註冊完捕捉函式後解除阻塞。

訊號引起的時序競態

競態條件,跟系統負載有很緊密的關係,體現出訊號的不可靠性。系統負載越嚴重,訊號不可靠性越強。

不可靠由其實現原理所致。訊號是通過軟體方式實現(跟核心排程高度依賴,延時性強),每次系統呼叫結束後,或中斷處理處理結束後,需通過掃描PCB中的未決訊號集,來判斷是否應處理某個訊號。當系統負載過重時,會出現時序混亂。

這種意外情況只能在編寫程式過程中,提早預見,主動規避,而無法通過gdb程式除錯等其他手段彌補。且由於該錯誤不具規律性,後期捕捉和重現十分困難。

pause()

pause() 等待訊號來臨之前主動掛起:

c
#include<unistd.h>

intpause(void);

注意:

  • pause收到的訊號不能被遮蔽,如果被遮蔽,那麼pause就不能被喚醒。
  • 如果訊號的預設處理動作是終止程序,則程序終止,pause函式沒有機會返回。
  • 如果訊號的預設處理動作是忽略,程序繼續處於掛起狀態,pause函式不返回。
  • 如果訊號的處理動作是捕捉,則【呼叫完訊號處理函式之後,pause返回-1errno設定為EINTR,表示“被訊號中斷”。】

像這種返回值比較奇怪的 的函式還有execl一族

時序競態(競態條件)

設想如下場景:

欲睡覺,定鬧鐘10分鐘,希望10分鐘後鬧鈴將自己喚醒。
正常:定時,睡覺,10分鐘後被鬧鐘喚醒。
異常:鬧鐘定好後,被喚走,外出勞動,20分鐘後勞動結束。回來繼續睡覺計劃,但勞動期間鬧鐘已經響過,不會再將我喚醒。

更改程序的訊號遮蔽字可以保護不希望由訊號中斷的程式碼臨界區。如果希望對一個訊號解除阻塞,然後pause等待以前被阻塞的訊號發生,則又將如何呢?
假定訊號是SIGINT,實現這一點的一種不正確的方法是:

c
sigset_t    newmask, oldmask;

sigemptyset(&newmask);
sigaddset(&newmask, SIGINT);

/* block SIGINT and save current signal mask */
if(sigprocmask(SIG_BLOCK, &newmask, &oldmask) < 0)
    err_sys("SIG_BLOCK error");

/* critical region of code */
if(sigprocmask(SIG_SETMASK, &oldmask, NULL) < 0)
    err_sys("SIG_SETMASK error");

/* window is open */
pause();    /* wait for signal to occur */

/* continue processing */

如果在訊號阻塞時將其傳送給程序,那麼該訊號的傳遞就被推遲直到對它解除了阻塞。
對應用程式而言,該訊號好像發生在解除對SIGINT的阻塞和pause之間。
如果發生了這種情況,或者如果在解除阻塞時刻和pause之間確實發生了訊號,那麼就產生了問題。
因為我們可能不會再見到該訊號,所以從這種意義上而言,在此時間視窗(解除阻塞和pause之間)中發生的訊號丟失了,這樣就使pause永遠阻塞。

為了糾正此問題,需要在一個原子操作中先恢復訊號遮蔽字,然後使程序休眠。這種功能是由sigsuspend函式提供的。

如果在等待訊號發生時希望去休眠,在對時序要求嚴格的場合下都應該使用sigsuspend替換pause

c
#include<signal.h>

intsigsuspend(constsigset_t *mask);

將程序的訊號遮蔽字設定為由mask指向的值。在捕捉到一個訊號或發生了一個會終止該程序的訊號之前,該程序被掛起。如果捕捉到一個訊號而且從該訊號處理程式返回,則sigsuspend返回,並且將該程序的訊號遮蔽字設定為呼叫sigsuspend之前的值。
注意,sigsuspend 函式沒有成功返回值。如果它返回到呼叫者,則總是返回-1,並將errno設定為EINTR(表示一個被中斷的系統呼叫)。
下面的程式顯示了保護臨界區,使其不被特定訊號中斷的正確方法:

c
#include<stdio.h>
#include<stdlib.h>
#include<signal.h>

staticvoidsig_int(int signo){
    printf("in sig_int: %d\n", signo);
}

intmain(int argc, char *argv[]){
    sigset_t     newmask, oldmask, waitmask;

    printf("program start: \n");

    if(signal(SIGINT, sig_int) == SIG_ERR)
        perror("signal(SIGINT) error");
    sigemptyset(&waitmask);
    sigaddset(&waitmask, SIGQUIT);
    sigemptyset(&newmask);
    sigaddset(&newmask, SIGINT);

    /*
    * Block SIGINT and save current signal mask.
    */
    if(sigprocmask(SIG_BLOCK, &newmask, &oldmask) < 0)
        perror("SIG_BLOCK error: ");

    /*
    * Critical region of code.
    */
    printf("in critical region: \n");

    /*
    * Pause, allowing all signals except SIGUSR1.
    */
    if(sigsuspend(&waitmask) != -1)
        perror("sigsuspend error");

    printf("after return from sigsuspend: \n");

    /*
    * Reset signal mask which unblocks SIGINT.
    */
    if(sigprocmask(SIG_SETMASK, &oldmask, NULL) < 0)
        perror("SIG_SETMASK error");

    /*
    * And continue processing...
    */

    exit(0);
}

sigsuspend的另一種應用是等待一個訊號處理程式設定一個全域性變數。
下面的例程用於捕捉中斷訊號和退出訊號,但是希望僅當捕捉到退出訊號時,才喚醒主程式。

c
#include<stdio.h>
#include<signal.h>
#include<stdlib.h>

volatile sig_atomic_t    quitflag;    /* set nonzero by signal handler */

staticvoidsig_int(int signo)/* one signal handler for SIGINT and SIGQUIT */{
    // signal(SIGINT, sig_int);
    // signal(SIGQUIT, sig_int);
    if (signo == SIGINT)
        printf("\ninterrupt\n");
    else if (signo == SIGQUIT)
        quitflag = 1;    /* set flag for main loop */
}

intmain(int argc, char *argv[]){
    sigset_t    newmask, oldmask, zeromask;

    if(signal(SIGINT, sig_int) == SIG_ERR)
        perror("signal(SIGINT) error");
    if(signal(SIGQUIT, sig_int) == SIG_ERR)
        perror("signal(SIGQUIT) error");

    sigemptyset(&zeromask);
    sigemptyset(&newmask);
    sigaddset(&newmask, SIGQUIT);

    /*
    * Block SIGQUIT and save current signal mask.
    */
    if(sigprocmask(SIG_BLOCK, &newmask, &oldmask) < 0)
        perror("SIG_BLOCK error");

    while(!quitflag)
    {
        sigsuspend(&zeromask);
    }

    /*
    * SIGQUIT has been caught and is now blocked; do whatever.
    */
    quitflag = 0;

    /*
    * Reset signal mask which unblocks SIGQUIT.
    */
    if(sigprocmask(SIG_SETMASK, &oldmask, NULL) < 0)
        perror("SIG_SETMASK error");

    exit(0);
}

https://www.cnblogs.com/xiangtingshen/p/10885564.html

附錄:可重入函式

一個函式在被呼叫執行期間(尚未呼叫結束),由於某種時序又被重複呼叫,稱之為“重入”。
根據函式實現的方法可分為“可重入函式”和“不可重入函式”兩種。
可重入函式:指一個可以被多個任務呼叫的過程,任務在呼叫時不必擔心資料是否會出錯

為了增強程式的穩定性,在訊號處理函式中應使用可重入函式。

訊號處理程式中應當使用可再入(可重入)函式。
因為程序在收到訊號後,就將跳轉到訊號處理函式去接著執行。如果訊號處理函式中使用了不可重入函式,那麼訊號處理函式可能會修改原來程序中不應該被修改的資料,這樣程序從訊號處理函式中返回接著執行時,可能會出現不可預料的後果。
不可再入函式在訊號處理函式中被視為不安全函式。

滿足下列條件的函式多數是不可再入的:

  • 使用靜態的資料結構,如getlogin(),gmtime(),getgrgid(),getgrnam(),getpwuid()以及getpwnam()等等;
  • 函式實現時,呼叫了malloc()或者free()函式;(3)實現時使用了標準I/O函式的。

The Open Group視下列函式為可再入的:

c
_exit()、access()、alarm()、cfgetispeed()、cfgetospeed()、cfsetispeed()、cfsetospeed()、chdir()、chmod()、chown()  、close()、creat()、dup()、dup2()、execle()、execve()、fcntl()、fork()、fpathconf()、fstat()、fsync()、getegid()、  geteuid()、getgid()、getgroups()、getpgrp()、getpid()、getppid()、getuid()、kill()、link()、lseek()、mkdir()、mkfifo()、  open()、pathconf()、pause()、pipe()、raise()、read()、rename()、rmdir()、setgid()、setpgid()、setsid()、setuid()、  sigaction()、sigaddset()、sigdelset()、sigemptyset()、sigfillset()、sigismember()、signal()、sigpending()、sigprocmask()、sigsuspend()、sleep()、stat()、sysconf()、tcdrain()、tcflow()、tcflush()、tcgetattr()、tcgetpgrp()、tcsendbreak()、tcsetattr()、tcsetpgrp()、time()、times()、 umask()、uname()、unlink()、utime()、wait()、waitpid()、write()。

即使訊號處理函式使用的都是"安全函式",同樣要注意進入處理函式時,首先要儲存errno的值,結束時,再恢復原值。因為,訊號處理過程中,errno值隨時可能被改變。

另外,longjmp()以及siglongjmp()沒有被列為可再入函式,因為不能保證緊接著兩個函式的其它呼叫是安全的。

附錄:使用特殊的跳轉函式sigsetjmp()和siglongjmp() 實現 try-catch

linux中特殊的跳轉函式sigsetjmp()和siglongjmp(),在low-level subroutine中處理中斷和錯誤的時候特別有用。

c
#include<setjmp.h>

voidlongjmp(jmp_buf env, int val);
voidsiglongjmp(sigjmp_buf env, int val);

intsetjmp(jmp_buf env);
intsigsetjmp(sigjmp_buf env, int savesigs);

sigsetjmp會將當前的堆疊上下文儲存在變數env中,這個變數會在後面的siglongjmp中用到。但是當呼叫個sigsetjmp的函式返回的時候,env變數將會失效;
如果savesigs非零,阻塞的訊號集合也會儲存在env變數中,當呼叫siglongjmp的時候,阻塞的訊號集也會被恢復。
如果sigsetjmp本身直接返回,則返回值為0;若sigsetjmp在siglongjmp使用env之後返回,則返回值為非零。

c
#include<stdio.h>
#include<setjmp.h>
#include<signal.h>

static sigjmp_buf jmpbuf;

voidsig_fpe(int signo){
    siglongjmp(jmpbuf, 1);
}

intmain(int argc, char *argv[]){
    signal(SIGFPE, sig_fpe);
    if (sigsetjmp(jmpbuf, 1) == 0) // try
    {
        int ret = 10 / 0;

    }else // catch
    {
        printf("catch exception\n");
    }

    return 0;
}
/*
    分析:在第一次呼叫sigsetjmp的時候,由於之前沒有呼叫siglongjmp,所以sigsetjmp的返回值為0;
    故執行int ret = 10 / 0;的操作這時候產生了一個SIGFPE訊號,然後會進入SIGFPE訊號的handler中。
    在handler中呼叫了siglongjmp,恢復了env,這時候會回到儲存env之處,繼續重新執行if。
    由於在本次sigsetjmp呼叫之前已經有siglongjmp恢復了env,故返回值為非零。從而最終打印出捕捉到的異常資訊。
    這個功能其實相當於cpp中的異常捕捉try...catch塊。
*/

背景

上一講我們介紹了Unix IPC中的2種管道。

回顧一下上一講的介紹,IPC的方式通常有:

  • Unix IPC包括:管道(pipe)、命名管道(FIFO)與訊號(Signal)
  • System V IPC:訊息佇列、訊號量、共享記憶體
  • Socket(支援不同主機上的兩個程序IPC)

我們在這一講介紹Unix IPC,中有關訊號(Signal)的處理。

訊號(Signal)

Signal :程序給作業系統或程序的某種資訊,讓作業系統或者其他程序做出某種反應。

訊號是程序間通訊機制中唯一的非同步通訊機制,(一個程序不必通過任何操作來等待訊號,然而,程序也不知道訊號到底何時到達。)

程序之間可以互相通過系統呼叫kill傳送軟中斷訊號。核心也可以因為內部事件而給程序傳送訊號,通知程序發生了某個事件。

訊號機制除了基本通知功能外,還可以傳遞附加資訊(呼叫 sigaction函式)。

程序檢查是否收到訊號的時機是:一個程序在即將從核心態返回到使用者態時;或者,在一個程序要進入或離開一個適當的低排程優先順序睡眠狀態時。

核心實現訊號捕捉的過程:
1.使用者態的程式,在執行流程中因為中斷、異常或者系統呼叫進入核心態
2.核心處理完異常準備返回使用者模式之前,先處理當前程序中可以遞送的訊號
3.如果訊號的處理動作是自定義的訊號處理函式,則回到使用者模式執行訊號處理函式(而不是返回使用者態程式的主執行流程)
4.訊號處理函式返回時,執行特殊的系統呼叫"sigreturn"再次進入核心
5.返回使用者態,從上次被打斷的地方繼續向下執行

Linux下當向一個程序發出訊號時,從訊號產生到程序接收該訊號並執行相應操作的過程稱為訊號的等待過程。如果某一個訊號沒有被程序遮蔽,則我們可以在程式中阻塞程序對該訊號所相應的操作。

例如一個程式當接收到SIGUSR1訊號時會進行一個操作,我們可以利用系統API阻塞(block)程式對該訊號的操作,直到我們解除阻止。
再舉個現實的例子:就好像一個同學讓我幫他帶飯,但是我現在有其他事要做,現在我先做我手頭上的事,直到我把手上的事都完成才去幫他帶飯。整個過程差不多就是這樣子。

訊號的特點:簡單、不能攜帶大量資訊、滿足某個特設條件才傳送。

利用訊號來處理子程序的回收是非常方便和高效的,因為所有的工作你都可以交給核心。

例如:當子程序終止時,會發出一個訊號,一旦你註冊的訊號捕捉器捕捉到了這個訊號,那麼就可以去回撥自己的函式(處理處理子程序的函式),去回收子程序了。

訊號的分類

Linux的signal.h中定義了很多訊號,使用命令kill -l可以檢視系統定義的訊號列表。

bash
1) SIGHUP       終端的控制程序結束,通知session內的各個作業,脫離關係 
2) SIGINT       程式終止訊號(Ctrl+c)
3) SIGQUIT      和2號訊號類似(Ctrl+\),產生core檔案
4) SIGILL       執行了非法指令,可執行檔案本身出現錯誤 
5) SIGTRAP      有斷點指令或其他trap指令產生,有debugger使用
6) SIGABRT      呼叫abort函式生成的訊號 
7) SIGBUS       非法地址(記憶體地址對齊出錯)
8) SIGFPE       致命的算術錯誤(浮點數運算,溢位,及除數為0 錯誤)
9) SIGKILL      用來立即結束程式的執行(不能為阻塞,處理,忽略)
10) SIGUSR1     使用者使用 

11) SIGSEGV     訪問記憶體錯誤
12) SIGUSR2     使用者使用
13) SIGPIPE     管道破裂
14) SIGALRM     時鐘定時訊號
15) SIGTERM     程式結束訊號(可被阻塞,處理)
16) SIGSTKFLT   協處理器棧堆錯誤
17) SIGCHLD     子程序結束,父程序收到這個訊號並進行處理,(wait也可以)否則殭屍程序
18) SIGCONT     讓一個停止的程序繼續執行(不能被阻塞)
19) SIGSTOP     讓一個程序停止執行(不能被阻塞,處理,忽略)
20) SIGTSTP     停止程序的執行(可以被處理和忽略)

21) SIGTTIN     當後臺作業要從使用者終端讀資料時, 該作業中的所有程序會收到SIGTTIN訊號. 預設時這些程序會停止執行.
22) SIGTTOU     類似SIGTTIN,但在寫終端時收到
23) SIGURG      有緊急資料或者out—of—band 資料到達socket時產生
24) SIGXCPU     超過CPU資源限定,這個限定可改變
25) SIGXFSZ     當程序企圖擴大檔案以至於超過檔案大小資源限制
26) SIGVTALRM   虛擬時鐘訊號(計算的是該程序佔用的CPU時間)
27) SIGPROF     時鐘訊號(程序用的CPU時間及系統呼叫時間)
28) SIGWINCH    視窗大小改變時發出
29) SIGIO       檔案描述符準備就緒,可以進行讀寫操作
30) SIGPWR      power failure
31) SIGSYS      非法的系統呼叫

(沒有32與33訊號)

34) SIGRTMIN    35) SIGRTMIN+1  36) SIGRTMIN+2  37) SIGRTMIN+3
38) SIGRTMIN+4  39) SIGRTMIN+5  40) SIGRTMIN+6  41) SIGRTMIN+7  42) SIGRTMIN+8
43) SIGRTMIN+9  44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9  56) SIGRTMAX-8  57) SIGRTMAX-7
58) SIGRTMAX-6  59) SIGRTMAX-5  60) SIGRTMAX-4  61) SIGRTMAX-3  62) SIGRTMAX-2
63) SIGRTMAX-1  64) SIGRTMAX

非實時訊號(1-31)

非實時訊號也叫不可靠訊號,有如下特點:
1)不可靠訊號(不可靠訊號 和 可靠訊號 的區別在於前者不支援排隊,可能會造成訊號丟失,而 可靠訊號 不會)
2)訊號是有可能丟失的
3)非實時訊號的產生都對應一個系統事件、每一個訊號都有一個預設的系統動作
4)巢狀執行(但是可能會造成丟失)
5)被阻塞訊號是沒有優先順序
6)在掛起的訊號中,訊號的執行時亂序的

在以上列出的訊號中,

  • 不可捕獲、阻塞或忽略的訊號有:SIGKILL,SIGSTOP
  • 不能恢復至預設動作的訊號有:SIGILL,SIGTRAP
  • 預設會導致程序流產的訊號有:SIGABRT,SIGBUS,SIGFPE,SIGILL,SIGIOT,SIGQUIT,SIGSEGV,SIGTRAP,SIGXCPU,SIGXFSZ
  • 預設會導致程序退出的訊號有:SIGALRM,SIGHUP,SIGINT,SIGKILL,SIGPIPE,SIGPOLL,SIGPROF,SIGSYS,SIGTERM,SIGUSR1,SIGUSR2,SIGVTALRM
  • 預設會導致程序停止的訊號有:SIGSTOP,SIGTSTP,SIGTTIN,SIGTTOU
  • 預設程序忽略的訊號有:SIGCHLD,SIGPWR,SIGURG,SIGWINCH

此外,SIGIO在SVR4是退出,在4.3BSD中是忽略;SIGCONT在程序掛起時是繼續,否則是忽略,不能被阻塞

實時訊號(34-64)

實時訊號也叫可靠訊號,有如下特點:
1)可靠的訊號(不管來幾個都會響應)
2)訊號是不會丟失的
3)巢狀執行
4)被掛起的訊號是有優先順序(值越大,優先順序越高)
5)被掛起時實時訊號訊號比非實時訊號優先順序要高
6)被掛起順序執行訊號的響應

使用訊號的注意事項

exec 與 訊號
exec函式執行後, 把該程序所有訊號設為預設動作。
exec函式執行後, 把原先要捕捉的訊號設為預設,其他不變。

子程序 與 訊號
1)訊號初始化的設定是會被子程序繼承的
2)訊號的阻塞設定是會被子程序繼承的
3)被掛起的訊號是不會被子程序繼承的

自定義訊號最好從 SIGRTMIN 開始,但最好保留前幾個。比如SIGRTMIN+10。

訊號的使用

步驟:
0)事先對一個訊號進行註冊,事先對一個訊號進行註冊,改變訊號的響應事件:執行預設動作、忽略(丟棄)、捕捉(呼叫戶處理函式)
1)改變訊號的阻塞方式
2)觸發某種條件產生訊號 或 使用者自己傳送一個訊號;等待訊號
3)處理訊號:執行預設動作、忽略(丟棄)、捕捉(呼叫戶處理函式)

在預設情況下,當一個事件觸發了對應的訊號時,會使程序發生對應的動作。我們也可以人為地改變程序對於某個訊號的處理,達到控制響應訊號的目的。

註冊訊號處理 函式

在系統中,提供了2個訊號的註冊處理函式:signal、sigaction;用於改變程序接收到特定訊號後的行為。

希望能用相同方式處理一種訊號的多次出現,最好用sigaction,訊號只出現並處理一次,可以用signal。
由於歷史原因,在不同版本核心中可能有不同的行為,使用signal呼叫會有相容性問題,所以推薦使用訊號註冊函式sigaction。

訊號處理函式裡面應該注意的地方(討論關於編寫安全的訊號處理函式):
1)區域性變數的相關處理,儘量只執行簡單的操作 (不應該在訊號處理函式裡面操作全域性變數)
2)“volatile sig_atomic_t”型別的全域性變數的相關操作
3)呼叫非同步訊號安全的相關函式(不可重入函式就不是非同步訊號安全函式)
4)errno 是執行緒安全,即每個執行緒有自己的 errno,但不是非同步訊號安全。如果訊號處理函式比較複雜,且呼叫了可能會改變 errno 值的庫函式,必須考慮在訊號處理函式開始時儲存、結束的時候恢復被中斷執行緒的 errno 值;

非同步訊號安全函式(async-signal-safe)是指:“在該函式內部即使因為訊號而正在被中斷,在其他的地方該函式再被呼叫了也沒有任何問題”。如果函式中存在更新靜態區域裡的資料的情況(例如,malloc),一般情況下都是不全的非同步訊號函式。但是,即使使用靜態資料,如果在這裡這個資料時候把訊號遮蔽了的話,它就會變成非同步訊號安全函數了。

signal 函式

c
#include<signal.h>

typedefvoid(*sighandler_t)(int);
sighandler_tsignal(int signum, sighandler_t handler);

// void (*signal(int signum, void (*hand)(int)))(int); // 如果將第二行和第三行拆分,展開以後就是這個樣子

引數解析:
signum :要處理的訊號值(不能是 9-SIGKILL,19-SIGSTOP;32、33不存在)
handler:處理方式

  • SIG_IGN :忽略該訊號,即無動作
  • SIG_DFL :重置設為預設方式
  • function:當訊號產生時,呼叫函式名為 函式名 的函式,該函式必須是void function(int signum)類似的一個帶有1個整形引數,無返回值的函式。

返回值:

  • 成功返回之前的handler;
  • 失敗返回 SIG_ERR,且設定errno。
c
#include<stdio.h>
#include<unistd.h>
#include<signal.h>

voidsignal_hander(int signum){
    printf("signum is %d\n", signum);
    sleep(1);
    return;
}

intmain(int argc, char *argv[]){
    signal(SIGINT, signal_hander); // 註冊 SIGINT 的訊號處理函式。
    while(1)
    {
        sleep(2);
        // printf("sleep loop\n");
    }
    return 0;
}

sigaction

sigaction是比signal更高階的訊號註冊響應函式,能夠提供更多功能;可以搭配 sigqueue 來使用。

c
#include<signal.h>

intsigaction(int signum, const struct sigaction *act,
              struct sigaction *oldact);

引數解析(如果把 act、oldact 引數都設為NULL,那麼該函式可用於檢查訊號的有效性。)

  • signum:註冊訊號
  • act:訊號活動結構體,其中包含了對指定訊號的處理、訊號所傳遞的資訊、訊號處理函式執行過程中應遮蔽掉哪些函式等等
c
/* 需要用到以下結構體 */
structsigaction {
    // sa_handler、sa_sigaction除了可以是使用者自定義的處理函式外,還可以為SIG_DFL(採用預設的處理方式),也可以為SIG_IGN(忽略訊號)。
   void     (*sa_handler)(int); //只有一個引數,即訊號值,所以訊號不能傳遞除訊號值之外的任何資訊
   void     (*sa_sigaction)(int, siginfo_t *, void *);
            // 帶參的訊號處理函式,如果你想要使能這個訊號處理函式,需要設定一下sa_flags為SA_SIGINFO
            // 帶有三個引數,是為實時訊號而設的(當然同樣支援非實時訊號),它指定一個3引數訊號處理函式。
             - 第一個引數為訊號值,第三個引數沒有使用(posix沒有規範該引數的標準),
             - 第二個引數是指向siginfo_t結構的指標,結構中包含訊號攜帶的資料值
             siginfo_t {
               int      si_signo;     /* Signal number */ //訊號編號
               int      si_errno;     /* An errno value */ //如果為非零值則錯誤程式碼與之關聯 
               int      si_code;      /* Signal code */   // //說明程序如何接收訊號以及從何處收到
                         SI_USER:通過 kill 函式收到
                         SI_KERNEL:來自kernel.
                         SI_QUEUE:來自 sigqueue 函式.
                         SI_TIMER:來自 POSIX 規範的 timer.
                         SI_MESGQ (since Linux 2.6.6): POSIX message queue state changed; see mq_notify(3).
                         SI_ASYNCIO: AIO completed.
                         SI_SIGIO:Queued  SIGIO(only in kernels up to Linux 2.2; from Linux 2.4 onward SIGIO/SIGPOLL fills in si_code as described below).
                         SI_TKILL(since Linux 2.4.19): 來自 tkill 函式 或 tgkill 函式.


               int      si_trapno;    /* Trap number that caused
                                         hardware-generated signal
                                         (unused on most architectures) */
               pid_t    si_pid;       /* Sending process ID *///適用於SIGCHLD,代表被終止程序的PID 
               uid_t    si_uid;       /* Real user ID of sending process *///適用於SIGCHLD,代表被終止程序所擁有程序的UID 
               int      si_status;    /* Exit value or signal *///適用於SIGCHLD,代表被終止程序的狀態 
               clock_t  si_utime;     /* User time consumed *///適用於SIGCHLD,代表被終止程序所消耗的使用者時間 
               clock_t  si_stime;     /* System time consumed *///適用於SIGCHLD,代表被終止程序所消耗系統的時間
               sigval_t si_value;     /* Signal value */
               int      si_int;       /* POSIX.1b signal */
               void    *si_ptr;       /* POSIX.1b signal */
               int      si_overrun;   /* Timer overrun count;
                                         POSIX.1b timers */
               int      si_timerid;   /* Timer ID; POSIX.1b timers */
               void    *si_addr;      /* Memory location which caused fault */
               long     si_band;      /* Band event (was int in
                                         glibc 2.3.2 and earlier) */
               int      si_fd;        /* File descriptor */
               short    si_addr_lsb;  /* Least significant bit of address
                                         (since Linux 2.6.32) */
               void    *si_call_addr; /* Address of system call instruction
                                         (since Linux 3.5) */
               int      si_syscall;   /* Number of attempted system call
                                         (since Linux 3.5) */
               unsigned int si_arch;  /* Architecture of attempted system call
                                         (since Linux 3.5) */
            }
                


   sigset_t   sa_mask;    //訊號阻塞設定,用來設定在處理該訊號時暫時將sa_mask 指定的訊號集擱置
                                    (在訊號處理函式執行的過程當中阻塞掉指定的訊號集,指定的訊號過來將會被掛起,等函式結束後再執行)

   int        sa_flags;   //訊號的操作標識,可以是以下的值(設定為0,代表使用預設屬性)
        SA_NOCLDSTOP:使父程序在它的子程序暫停或繼續執行時不會收到 SIGCHLD 訊號。(此標誌只有在設立SIGCHLD的處理程式時才有意義)
        SA_NOCLDWAIT:使父程序在它的子程序退出時不會收到 SIGCHLD 訊號,這時子程序如果退出也不會成為殭屍程序。(此標誌只有在設立SIGCHLD的處理程式時才有意義 或 設定回 SIG_DFL)
        SA_NODEFER :如果設定了 SA_NODEFER標記, 那麼在該訊號處理函式執行時,核心將不會阻塞該訊號。(一般情況下, 當訊號處理函式執行時,核心將阻塞該給定訊號)
        SA_ONSTACK:在sigaltstack提供的備用訊號堆疊上呼叫訊號處理程式。如果備用堆疊不可用,將使用預設堆疊。(此標誌只有在建立訊號處理程式時才有意義)
        SA_RESETHAND:當呼叫訊號處理函式時,將訊號的處理函式重置為預設值SIG_DFL(此標誌只有在建立訊號處理程式時才有意義)
        SA_RESTART:如果訊號中斷了程序的某個系統呼叫,則系統自動啟動該系統呼叫
        SA_RESTORER:不適用於程式使用。參考"sigreturn"
        SA_SIGINFO:使用 sa_sigaction 成員而不是 sa_handler 作為訊號處理函式。

   void     (*sa_restorer)(void); //被遺棄的設定
};

  • oldact:原本的設定會被儲存在這裡,如果是NULL則不儲存

返回值:

  • 成功返回 0;
  • 失敗返回 -1,且設定errno。

我們來看一個 sigaction 與 sigqueue 配合的例程。

c
#include<stdio.h>
#include<unistd.h>
#include<signal.h>
#include<stdlib.h>

voidmysa_handler(int signum){
    printf("sa_handler %d\n", signum);
    return ;
}

voidmysa_sigaction(int signo, siginfo_t *info,void *ctx){
    //以下兩種方式都能獲得sigqueue發來的資料
    printf("receive the data from siqueue by info->si_int is %d\n",info->si_int);
    printf("receive the data from siqueue by info->si_value.sival_int is %d\n",info->si_value.sival_int);
}

intmain(void){
    structsigactionact;
    // 下面的巨集 區分了2種 訊號響應方式
#if 1
    // act.sa_handler = SIG_DFL; 預設動作
    act.sa_handler = mysa_handler;
#else
    act.sa_sigaction = mysa_sigaction;
    act.sa_flags = SA_SIGINFO;//資訊傳遞開關
#endif
    sigemptyset(&act.sa_mask);
    if(sigaction(SIGINT,&act,NULL) == -1){
        perror("sigaction error");
        exit(EXIT_FAILURE);
    }
    sleep(2);
    union sigval mysigval;
    mysigval.sival_int = 100;
    if(sigqueue(getpid(),SIGINT,mysigval) == -1){
        perror("sigqueue error");
        exit(EXIT_FAILURE);
    }

    return 0;
}

訊號集 與 訊號阻塞 、未決

在PCB中有兩個非常重要的訊號集。一個稱之為“阻塞訊號集”,另一個稱之為“未決訊號集”。這兩個訊號集都是核心使用點陣圖機制來實現的。
作業系統不允許我們直接對其進行位操作。而需自定義另外一個集合,藉助訊號集操作函式來對PCB中的這兩個訊號集進行修改。

有關概念:

執⾏訊號的處理動作稱為訊號遞達(Delivery),訊號從產⽣到遞達之間的狀態,稱為訊號未決(Pending)。
程序可以選擇阻塞(Block)某個訊號。被阻塞的訊號產⽣時將保持在未決狀態,直到程序解除對此訊號的阻塞,才執⾏遞達的動作。
注意,阻塞和忽略是不同的,只要訊號被阻塞就不會遞達,⽽忽略是在遞達之後可選的⼀種處理動作。

每個程序都有一個用來描述哪些訊號遞送到程序時將被阻塞的訊號集,該訊號集中的所有訊號在遞送到程序後都將被阻塞。

  • block集(阻塞集、遮蔽集):一個程序所要遮蔽的訊號,在對應要遮蔽的訊號位置1
  • pending集(未決訊號集):如果某個訊號在程序的阻塞集中,則也在未決集中對應位置1,表示該訊號不能被遞達,不會被處理
  • handler(訊號處理函式集):表示每個訊號所對應的訊號處理函式,當訊號不在未決集中時,將被呼叫

阻塞訊號:對指定的訊號進行掛起,直到解除訊號的阻塞狀態以後,才去響應這個訊號。
忽略訊號:收到了訊號,但忽略對其的響應。(不執行任何動作)

訊號集有關的函式

以下是與訊號阻塞及未決相關的函式操作:

c
#include<signal.h>

intsigemptyset(sigset_t *set);                 // 清空宣告的訊號集
intsigfillset(sigset_t *set);                  // 將所有訊號登記進集合裡面

intsigaddset(sigset_t *set, int signum);       // 往集合集裡面新增signum訊號
intsigdelset(sigset_t *set, int signum);       // 往集合裡面刪除signum訊號

intsigismember(constsigset_t *set, int signum); // 測試訊號集合裡面有無signum訊號

intsigprocmask(int  how,  constsigset_t *set, sigset_t *oldset)); // 設定程序的訊號掩碼intsigpending(sigset_t *set));                // 獲得當前已遞送到程序,卻被阻塞的所有訊號,在set指向的訊號集中返回結果。
intsigsuspend(constsigset_t *mask));         // 用於在接收到某個訊號之前, 臨時用mask替換程序的訊號掩碼, 並暫停程序執行,直到收到訊號為止。 我們會在竟態中講到它

訊號集使用步驟
A. 宣告sigset_t *型別的訊號集變數#set
B. 清空宣告的訊號集(sigemptyset)
C. 向#set 中增刪訊號,可以使用以下有關函式進行操作

改變訊號的阻塞方式

更改程序的訊號遮蔽字可以阻塞所選擇的訊號,或解除對它們的阻塞。使用這種技術可以保護不希望由訊號中斷的程式碼臨界區。

c
#include<signal.h>

intsigprocmask(int how, constsigset_t *set, sigset_t *oldset);

引數解析:
how :阻塞模式

  • SIG_BLOCK:新增訊號集合裡面的訊號進行阻塞(原本的設定上新增設定),mask=mask|set
  • SIG_UNBLOCK:解除訊號集合裡面的訊號的阻塞,mask=mask|~set
  • SIG_SETMASK:直接阻塞訊號集合裡面的訊號,原本的設定直接被覆蓋,mask=set

set :設定的訊號集合
oldset :此前的設定

  • 填入:oldset,則將原本的設定儲存在這裡
  • 填入:NULL則不做任何操作

返回值:成功返回0;失敗返回-1,設定errno:

  • EFAULT :set或oldset引數指向非法地址。
  • EINVAL :how中指定的值無效。

使用阻塞訊號集阻塞訊號的例程:

c
/*
    說明:程式首先將SIGINT訊號加入程序阻塞集(遮蔽集)中,一開始並沒有傳送SIGINT訊號,所以程序未決集中沒有處於未決態的訊號。
    當我們連續按下ctrl+c時,向程序傳送SIGINT訊號;由於SIGINT訊號處於程序的阻塞集中,所以傳送的SIGINT訊號不能遞達,也是就是處於未決狀態,所以當我列印未決集合時發現SIGINT所對應的位為1。
    現在我們按下ctrl+\,傳送SIGQUIT訊號,由於此訊號並沒被程序阻塞,所以SIGQUIT訊號直接遞達,執行對應的處理函式,在該處理函式中解除程序對SIGINT訊號的阻塞。
    所以之前傳送的SIGINT訊號遞達了,執行對應的處理函式,但由於SIGINT訊號是不可靠訊號,不支援排隊,所以最終只有一個訊號遞達。
*/
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<errno.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<signal.h>

voidprintsigset(constsigset_t *pset){
    int i = 0;
    for (; i < 64; i++) //遍歷64個訊號,
    {
        //訊號從1開始   判斷哪些訊號在訊號未決狀態字中
        if (sigismember(pset, i + 1))
            putchar('1');
        else
            putchar('0');
    }
    printf("\n");
}

voidcatch_signal(int sign){
    switch (sign)
    {
    case SIGINT:
        printf("accept SIGINT!\n");
        exit(0);
        break;
    case SIGQUIT:
        printf("accept SIGQUIT!\n");
        //取消訊號阻塞
        
        sigset_t uset; //定義訊號集
        sigemptyset(&uset); //清空訊號集
        sigaddset(&uset,SIGINT); //將SIGINT訊號加入到訊號集中

        //進行位異或操作,將訊號集uset更新到程序控制塊PCB結構中,取消阻塞訊號SIGINT
        sigprocmask(SIG_UNBLOCK,&uset,NULL);
        break;
    }
}

intmain(int arg, char *args[]){
    //定義未決訊號集(pending)
    sigset_t pset;
    //定義阻塞訊號集(block)
    sigset_t bset;
    //清空訊號集
    sigemptyset(&bset);
    //將訊號SIGINT加入到訊號集中
    sigaddset(&bset, SIGINT);
    //註冊訊號
    if (signal(SIGINT, catch_signal) == SIG_ERR)
    {
        perror("signal error");
        return -1;
    }
    if (signal(SIGQUIT, catch_signal) == SIG_ERR)
    {
        perror("signal error");
        return -1;
    }
    //進行位或操作,將訊號集bset更新到程序控制塊PCB結構中,阻塞訊號SIGINT(即使使用者按下ctrl+c,訊號也不會遞達)
    sigprocmask(SIG_BLOCK, &bset, NULL);
    while (1)
    {
        /*
         * 獲取當前訊號未決資訊,即使在sigprocmask()函式中設定了訊號阻塞,
         * 但是如果沒有訊號的到來,訊號未決狀態字對應位依然是0
         * 只要有訊號到來,並且被阻塞了,訊號未決狀態字對應位才會是1
         * */
        sigpending(&pset);
        //列印訊號未決資訊
        printsigset(&pset);
        sleep(2);
    }
    return 0;
}

傳送一個訊號

手動傳送一個訊號,有下面這些函式:killraisesigqueuealarmualarmabort

kill 函式

描述:向 程序/程序組 傳送訊號。

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

intkill(pid_t pid, int sig);

引數解析:

  • pid

如果pid為正,則將訊號sig傳送到由pid指定ID的程序。
如果pid等於0,那麼sig將傳送到呼叫程序的程序組中的每個程序。用kill(0,sig)傳送自定義訊號時,本程序和所有子程序(通過exec等方式啟動的)都必須有對應的處理函式,否則所有程序都會退出。
如果pid等於-1,則sig被髮送到呼叫程序有權傳送訊號的每個程序,程序1(init)除外,要使程序具有傳送訊號的許可權,它必須具有特權(在Linux下:具有CAP_KILL功能),或者傳送程序的真實或有效使用者ID必須等於目標程序的真實或已儲存的設定使用者ID。在SIGCONT的情況下,當傳送和接收程序屬於同一會話時就足夠了。
如果pid小於-1,那麼sig將傳送到程序組中ID為-pid的每個程序。

  • sig :如果sig為0,則不傳送訊號,但仍會執行錯誤檢查;這可用於檢查是否存在程序ID或程序組ID。

**返回值說明: **
成功執行時,返回0。
失敗返回-1,errno被設為以下的某個值 :

  • EINVAL:指定的訊號碼無效(引數 sig 不合法)
  • EPERM;許可權不夠無法傳送訊號給指定程序
  • ESRCH:引數 pid 所指定的程序或程序組不存在

raise 函式

描述:對呼叫的程序/執行緒自身傳送一個訊號,相當於kill(getpid(), sig);pthread_kill(pthread_self(), sig);

c
#include<signal.h>

intraise(int sig);

alarm 函式

描述:設定訊號SIGALRM在經過引數seconds指定的秒數後傳送給目前的程序。每個程序都有且只有唯一的一個定時器。無論程序處於何種狀態,alarm都計時。

c
#include<unistd.h>

unsignedintalarm(unsignedint seconds);

返回值:返回值為上次定時呼叫到傳送之間剩餘的時間,或者因為沒有前一次定時呼叫而返回0。

注意:

  • 如果指定的引數seconds為0,則不再發送 SIGALRM訊號。後一次設定將取消前一次的設定。
  • 在使用時,alarm只設定為傳送一次訊號,如果要多次傳送,就要多次使用alarm呼叫。
  • SIGALRM訊號如果不處理(使用者捕獲 或者 忽略),會使程序exit
  • 在某些系統中,SIGALRM訊號會預設中斷系統呼叫(inturrupt),當然也有的系統,預設情況下是使系統呼叫被中斷後自動重新開始(restart automatically)

ualarm 函式

描述:將使當前程序在指定時間(第一個引數,以us位單位)內產生SIGALRM訊號,然後每隔指定時間(第2個引數,以us位單位)重複產生SIGALRM訊號,如果執行成功,將返回0。

c
#include<unistd.h>

useconds_tualarm(useconds_t usecs, useconds_t interval);

abort 函式

abort 函式:給自己傳送異常終止訊號 SIGABRT 訊號,終止併產生core檔案。該函式無返回

c
#include<stdlib.h>

voidabort(void);

注意:如果SIGABRT訊號被忽略,或被返回的處理程式捕獲,它會在被捕獲處理完成以後終止程序。(通過恢復SIGABRT的預設配置,然後再次傳送SIGABRT訊號來實現這一點。)

段坤我吃定了,誰也留不住他。

sigqueue 函式

描述:作為新的傳送訊號系統呼叫,主要是針對實時訊號提出的支援訊號帶有引數,與函式sigaction()配合使用。在傳送訊號同時,就可以讓訊號傳遞一些附加資訊。這對程式開發是非常有意義的。

c
#include<signal.h>

intsigqueue(pid_t pid, int sig, constunion sigval value);

union sigval {
        int   sival_int;
        void *sival_ptr;
};

引數解析:
pid : 給定的程序號(PID只有為正數的情況,不比 signal 那麼複雜)
sig :訊號值
value:(聯合體)可以是數值,也可以是指標;不同程序之間虛擬地址空間各自獨立,將當前程序地址傳遞給另一程序沒有實際意義。但是訊號可以回撥,在回撥的時候,自己給自己捕捉住,傳遞地址就有意義了。

原理:

  • 當呼叫sigqueue時,引數 value 就 拷貝到訊號處理函式的第二個引數sa_sigaction中。
  • 這樣,sigaction引數中的 act->siginfo_t.si_value與sigqueue中的第三個引數value關聯;
  • 所以通過siginfo_t.si_value可以獲得sigqueue(pid_t pid, int sig, const union sigval val)第三個引數傳遞過來的資料。
  • 如:sigaction引數中的 act->siginfo_t.si_value.sival_int或sigaction引數中的 act->siginfo_t.si_value.sival_ptr

對於父子程序接收訊號的處理方式

父子程序都會收到訊號,按各自的方式處理。

c
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<signal.h>

voidsignal_handler_son(int signum){
    printf("signal_handler_son%d\n", signum);
}
voidsignal_handler_father(int signum){
    printf("signal_handler_father%d\n", signum);
}

intmain(int argc, char *argv[]){
    pid_t pid = fork();
    if(pid == 0)
    {
        signal(SIGINT, signal_handler_son);
        while(1)
        {
            sleep(2);  printf("Son loop\n");
        }
        exit(123);
    }else if(pid > 0)
    {
        signal(SIGINT, signal_handler_father);
        while(1)
        {
            sleep(2);  printf("Father loop\n");
        }
        wait(NULL);
    }

    return 0;
}

藉助SIGCHLD訊號回收子程序

子程序結束執行,其父程序會收到SIGCHLD訊號。該訊號的預設處理動作是忽略。可以捕捉該訊號,在捕捉函式中完成子程序狀態的回收。
SIGCHLD的產生條件:

  • 子程序終止時
  • 子程序接收到SIGSTOP訊號停止時
  • 子程序處在停止態,接受到SIGCONT後喚醒時

下面的程式介紹瞭如何使用 SIGCHLD 回收子程序

c
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/wait.h>
#include<signal.h>
 
voidsys_err(char *str){
    perror(str);
    exit(1);
}
voiddo_sig_child(int signo){
    int status;    pid_t pid;
    while ((pid = waitpid(0, &status, WNOHANG)) > 0) {
        if (WIFEXITED(status))
            printf("child %d exit %d\n", pid, WEXITSTATUS(status));
        else if (WIFSIGNALED(status))
            printf("child %d cancel signal %d\n", pid, WTERMSIG(status));
    }
}
intmain(void){
    pid_t pid;    int i;
    for (i = 0; i < 10; i++) {
        if ((pid = fork()) == 0)
            break;
        else if (pid < 0)
            sys_err("fork");
    }
    if (pid == 0) {   
        int n = 1;
        while (n--) {
            printf("child ID %d\n", getpid());
            sleep(1);
        }
        return i+1;
    } else if (pid > 0) {
        struct sigaction act;
        act.sa_handler = do_sig_child;
        sigemptyset(&act.sa_mask);
        act.sa_flags = 0;
        sigaction(SIGCHLD, &act, NULL);
       
        while (1) {
            printf("Parent ID %d\n", getpid());
            sleep(1);
        }
    }
    return 0;
}

SIGCHLD訊號注意問題:

  • 子程序繼承了父程序的訊號遮蔽字和訊號處理動作,但子程序沒有繼承未決訊號集spending。
  • 注意註冊訊號捕捉函式的位置。
  • 應該在fork之前,阻塞SIGCHLD訊號。註冊完捕捉函式後解除阻塞。

訊號引起的時序競態

競態條件,跟系統負載有很緊密的關係,體現出訊號的不可靠性。系統負載越嚴重,訊號不可靠性越強。

不可靠由其實現原理所致。訊號是通過軟體方式實現(跟核心排程高度依賴,延時性強),每次系統呼叫結束後,或中斷處理處理結束後,需通過掃描PCB中的未決訊號集,來判斷是否應處理某個訊號。當系統負載過重時,會出現時序混亂。

這種意外情況只能在編寫程式過程中,提早預見,主動規避,而無法通過gdb程式除錯等其他手段彌補。且由於該錯誤不具規律性,後期捕捉和重現十分困難。

pause()

pause() 等待訊號來臨之前主動掛起:

c
#include<unistd.h>

intpause(void);

注意:

  • pause收到的訊號不能被遮蔽,如果被遮蔽,那麼pause就不能被喚醒。
  • 如果訊號的預設處理動作是終止程序,則程序終止,pause函式沒有機會返回。
  • 如果訊號的預設處理動作是忽略,程序繼續處於掛起狀態,pause函式不返回。
  • 如果訊號的處理動作是捕捉,則【呼叫完訊號處理函式之後,pause返回-1errno設定為EINTR,表示“被訊號中斷”。】

像這種返回值比較奇怪的 的函式還有execl一族

時序競態(競態條件)

設想如下場景:

欲睡覺,定鬧鐘10分鐘,希望10分鐘後鬧鈴將自己喚醒。
正常:定時,睡覺,10分鐘後被鬧鐘喚醒。
異常:鬧鐘定好後,被喚走,外出勞動,20分鐘後勞動結束。回來繼續睡覺計劃,但勞動期間鬧鐘已經響過,不會再將我喚醒。

更改程序的訊號遮蔽字可以保護不希望由訊號中斷的程式碼臨界區。如果希望對一個訊號解除阻塞,然後pause等待以前被阻塞的訊號發生,則又將如何呢?
假定訊號是SIGINT,實現這一點的一種不正確的方法是:

c
sigset_t    newmask, oldmask;

sigemptyset(&newmask);
sigaddset(&newmask, SIGINT);

/* block SIGINT and save current signal mask */
if(sigprocmask(SIG_BLOCK, &newmask, &oldmask) < 0)
    err_sys("SIG_BLOCK error");

/* critical region of code */
if(sigprocmask(SIG_SETMASK, &oldmask, NULL) < 0)
    err_sys("SIG_SETMASK error");

/* window is open */
pause();    /* wait for signal to occur */

/* continue processing */

如果在訊號阻塞時將其傳送給程序,那麼該訊號的傳遞就被推遲直到對它解除了阻塞。
對應用程式而言,該訊號好像發生在解除對SIGINT的阻塞和pause之間。
如果發生了這種情況,或者如果在解除阻塞時刻和pause之間確實發生了訊號,那麼就產生了問題。
因為我們可能不會再見到該訊號,所以從這種意義上而言,在此時間視窗(解除阻塞和pause之間)中發生的訊號丟失了,這樣就使pause永遠阻塞。

為了糾正此問題,需要在一個原子操作中先恢復訊號遮蔽字,然後使程序休眠。這種功能是由sigsuspend函式提供的。

如果在等待訊號發生時希望去休眠,在對時序要求嚴格的場合下都應該使用sigsuspend替換pause

c
#include<signal.h>

intsigsuspend(constsigset_t *mask);

將程序的訊號遮蔽字設定為由mask指向的值。在捕捉到一個訊號或發生了一個會終止該程序的訊號之前,該程序被掛起。如果捕捉到一個訊號而且從該訊號處理程式返回,則sigsuspend返回,並且將該程序的訊號遮蔽字設定為呼叫sigsuspend之前的值。
注意,sigsuspend 函式沒有成功返回值。如果它返回到呼叫者,則總是返回-1,並將errno設定為EINTR(表示一個被中斷的系統呼叫)。
下面的程式顯示了保護臨界區,使其不被特定訊號中斷的正確方法:

c
#include<stdio.h>
#include<stdlib.h>
#include<signal.h>

staticvoidsig_int(int signo){
    printf("in sig_int: %d\n", signo);
}

intmain(int argc, char *argv[]){
    sigset_t     newmask, oldmask, waitmask;

    printf("program start: \n");

    if(signal(SIGINT, sig_int) == SIG_ERR)
        perror("signal(SIGINT) error");
    sigemptyset(&waitmask);
    sigaddset(&waitmask, SIGQUIT);
    sigemptyset(&newmask);
    sigaddset(&newmask, SIGINT);

    /*
    * Block SIGINT and save current signal mask.
    */
    if(sigprocmask(SIG_BLOCK, &newmask, &oldmask) < 0)
        perror("SIG_BLOCK error: ");

    /*
    * Critical region of code.
    */
    printf("in critical region: \n");

    /*
    * Pause, allowing all signals except SIGUSR1.
    */
    if(sigsuspend(&waitmask) != -1)
        perror("sigsuspend error");

    printf("after return from sigsuspend: \n");

    /*
    * Reset signal mask which unblocks SIGINT.
    */
    if(sigprocmask(SIG_SETMASK, &oldmask, NULL) < 0)
        perror("SIG_SETMASK error");

    /*
    * And continue processing...
    */

    exit(0);
}

sigsuspend的另一種應用是等待一個訊號處理程式設定一個全域性變數。
下面的例程用於捕捉中斷訊號和退出訊號,但是希望僅當捕捉到退出訊號時,才喚醒主程式。

c
#include<stdio.h>
#include<signal.h>
#include<stdlib.h>

volatile sig_atomic_t    quitflag;    /* set nonzero by signal handler */

staticvoidsig_int(int signo)/* one signal handler for SIGINT and SIGQUIT */{
    // signal(SIGINT, sig_int);
    // signal(SIGQUIT, sig_int);
    if (signo == SIGINT)
        printf("\ninterrupt\n");
    else if (signo == SIGQUIT)
        quitflag = 1;    /* set flag for main loop */
}

intmain(int argc, char *argv[]){
    sigset_t    newmask, oldmask, zeromask;

    if(signal(SIGINT, sig_int) == SIG_ERR)
        perror("signal(SIGINT) error");
    if(signal(SIGQUIT, sig_int) == SIG_ERR)
        perror("signal(SIGQUIT) error");

    sigemptyset(&zeromask);
    sigemptyset(&newmask);
    sigaddset(&newmask, SIGQUIT);

    /*
    * Block SIGQUIT and save current signal mask.
    */
    if(sigprocmask(SIG_BLOCK, &newmask, &oldmask) < 0)
        perror("SIG_BLOCK error");

    while(!quitflag)
    {
        sigsuspend(&zeromask);
    }

    /*
    * SIGQUIT has been caught and is now blocked; do whatever.
    */
    quitflag = 0;

    /*
    * Reset signal mask which unblocks SIGQUIT.
    */
    if(sigprocmask(SIG_SETMASK, &oldmask, NULL) < 0)
        perror("SIG_SETMASK error");

    exit(0);
}

https://www.cnblogs.com/xiangtingshen/p/10885564.html

附錄:可重入函式

一個函式在被呼叫執行期間(尚未呼叫結束),由於某種時序又被重複呼叫,稱之為“重入”。
根據函式實現的方法可分為“可重入函式”和“不可重入函式”兩種。
可重入函式:指一個可以被多個任務呼叫的過程,任務在呼叫時不必擔心資料是否會出錯

為了增強程式的穩定性,在訊號處理函式中應使用可重入函式。

訊號處理程式中應當使用可再入(可重入)函式。
因為程序在收到訊號後,就將跳轉到訊號處理函式去接著執行。如果訊號處理函式中使用了不可重入函式,那麼訊號處理函式可能會修改原來程序中不應該被修改的資料,這樣程序從訊號處理函式中返回接著執行時,可能會出現不可預料的後果。
不可再入函式在訊號處理函式中被視為不安全函式。

滿足下列條件的函式多數是不可再入的:

  • 使用靜態的資料結構,如getlogin(),gmtime(),getgrgid(),getgrnam(),getpwuid()以及getpwnam()等等;
  • 函式實現時,呼叫了malloc()或者free()函式;(3)實現時使用了標準I/O函式的。

The Open Group視下列函式為可再入的:

c
_exit()、access()、alarm()、cfgetispeed()、cfgetospeed()、cfsetispeed()、cfsetospeed()、chdir()、chmod()、chown()  、close()、creat()、dup()、dup2()、execle()、execve()、fcntl()、fork()、fpathconf()、fstat()、fsync()、getegid()、  geteuid()、getgid()、getgroups()、getpgrp()、getpid()、getppid()、getuid()、kill()、link()、lseek()、mkdir()、mkfifo()、  open()、pathconf()、pause()、pipe()、raise()、read()、rename()、rmdir()、setgid()、setpgid()、setsid()、setuid()、  sigaction()、sigaddset()、sigdelset()、sigemptyset()、sigfillset()、sigismember()、signal()、sigpending()、sigprocmask()、sigsuspend()、sleep()、stat()、sysconf()、tcdrain()、tcflow()、tcflush()、tcgetattr()、tcgetpgrp()、tcsendbreak()、tcsetattr()、tcsetpgrp()、time()、times()、 umask()、uname()、unlink()、utime()、wait()、waitpid()、write()。

即使訊號處理函式使用的都是"安全函式",同樣要注意進入處理函式時,首先要儲存errno的值,結束時,再恢復原值。因為,訊號處理過程中,errno值隨時可能被改變。

另外,longjmp()以及siglongjmp()沒有被列為可再入函式,因為不能保證緊接著兩個函式的其它呼叫是安全的。

附錄:使用特殊的跳轉函式sigsetjmp()和siglongjmp() 實現 try-catch

linux中特殊的跳轉函式sigsetjmp()和siglongjmp(),在low-level subroutine中處理中斷和錯誤的時候特別有用。

c
#include<setjmp.h>

voidlongjmp(jmp_buf env, int val);
voidsiglongjmp(sigjmp_buf env, int val);

intsetjmp(jmp_buf env);
intsigsetjmp(sigjmp_buf env, int savesigs);

sigsetjmp會將當前的堆疊上下文儲存在變數env中,這個變數會在後面的siglongjmp中用到。但是當呼叫個sigsetjmp的函式返回的時候,env變數將會失效;
如果savesigs非零,阻塞的訊號集合也會儲存在env變數中,當呼叫siglongjmp的時候,阻塞的訊號集也會被恢復。
如果sigsetjmp本身直接返回,則返回值為0;若sigsetjmp在siglongjmp使用env之後返回,則返回值為非零。

c
#include<stdio.h>
#include<setjmp.h>
#include<signal.h>

static sigjmp_buf jmpbuf;

voidsig_fpe(int signo){
    siglongjmp(jmpbuf, 1);
}

intmain(int argc, char *argv[]){
    signal(SIGFPE, sig_fpe);
    if (sigsetjmp(jmpbuf, 1) == 0) // try
    {
        int ret = 10 / 0;

    }else // catch
    {
        printf("catch exception\n");
    }

    return 0;
}
/*
    分析:在第一次呼叫sigsetjmp的時候,由於之前沒有呼叫siglongjmp,所以sigsetjmp的返回值為0;
    故執行int ret = 10 / 0;的操作這時候產生了一個SIGFPE訊號,然後會進入SIGFPE訊號的handler中。
    在handler中呼叫了siglongjmp,恢復了env,這時候會回到儲存env之處,繼續重新執行if。
    由於在本次sigsetjmp呼叫之前已經有siglongjmp恢復了env,故返回值為非零。從而最終打印出捕捉到的異常資訊。
    這個功能其實相當於cpp中的異常捕捉try...catch塊。
*/