使用 sigprocmask 和 sigpending 在程序正文中捕獲和處理信號

最近在嘗試使用 epoll 寫一個相似 libevent 的庫。那麼,如何像 libevent 同樣,在 event loop 里加入對信號事件的觀測呢?
我查了一下資料,一個可行的方法,就是使用 sigprocmask() 及其相關功能來實現啦。html

可是請注意,這個方法是存在缺陷的,請看官留心。
我的在繼續研究以後,暫時是不打算使用此種方法來實現信號事件,而改用另外一個方法linux

Reference

《UNIX 環境高級編程》
sigprocmask , sigpending 和 sigsuspend函數
errno多線程安全
Linux 多線程應用中編寫安全的信號處理函數git

UNIX 系統主要信號

如下就只列出主要的信號了:github

名稱 說明 FreeBSD Linux macOS Solaris 默認動做
SIGABRT 調用了abort() Y Y Y Y 終止 + core
SIGALRM alarm()產生的 Y Y Y Y 終止
SIGBUS 硬件故障 Y Y Y Y 終止 + core
SIGCHLD 子進程狀態改變 Y Y Y Y 忽略
SIGHUP 鏈接斷開 Y Y Y Y 終止
SIGINT Ctrl + C Y Y Y Y 終止
SIGKILL 終止;不可捕獲 Y Y Y Y 終止
SIGPIPE 向關閉的管道寫 Y Y Y Y 終止
SIGQUIT Ctrl + \ Y Y Y Y 終止 + core
SIGSEGV 段錯誤 Y Y Y Y 終止 + core
SIGSTOP 中止 Y Y Y Y 暫停進程
SIGTERM kill(1) Y Y Y Y 終止
SIGUSR1 用戶自定義1 Y Y Y Y 終止
SIGUSR2 用戶自定義2 Y Y Y Y 終止
SIGPOLL 可輪訓的設備發生事件 . Y . Y 終止
SIGPWR 主電源失效,電池電量不足 . Y . Y 終止或忽略

若是要在 C 裏面發送一個信號的話,那麼能夠用 kill()raise()。其中後者是想當前進程發信號,而前者能夠向任意進程發信號。kill()pid 參數能夠有如下可能值:編程

  • pid > 0:發給指定進程
  • pid == 0:發給與當前進程屬於同一進程組的全部進程,但須要權限容許
  • pid < 0:發給進程組 ID 等於 (0 - pid) 的全部進程,但須要權限容許
  • pid == -1:發給全部進程,但須要權限容許

信號集操做

#include <signal.h>

    int sigemptyset(sigset_t *set);
    int sigfillset(sigset_t *set);
    int sigaddset(sigset_t *set, int signum);
    int sigdelset(sigset_t *set, int signum);
    int sigismember(const sigset_t *set, int signum);

上面的幾個函數語義都很清楚了,就是在一個集合裏面配置多個信號。
除了 sigismenber() 實際上返回的是 BOOL 類型以外,其餘的函數均返回 0 表明成功,-1 表明失敗。segmentfault

sigprocmask 和 sigpending

#include <signal.h>

    int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
    int sigpending(sigset_t *set);

sigprocmask() 返回的是 0 或者 -1 的狀態值,而 sigpending() 返回 BOOL
其中 how 能夠有如下值:安全

  • SIG_BLOCK:屏蔽信號(注意,不是「忽略」信號)
  • SIG_UNBLOCK:解屏蔽
  • SIG_SETMASK:將整個表配置設置進去。這適用於 sigprocmask() 恢復階段。後續有說明

「屏蔽」 信號的含義

sigprocmask()的做用,主要就是屏蔽指定的信號。這個 「屏蔽」 的含義須要說明清楚。
首先咱們大體數一下信號在內核裏的處理流程吧(不是準確的流程,只是便於說明):多線程

  1. 內核等待信號中斷
  2. 信號產生,觸發內核中斷
  3. 內核將信號存下來,或者說設置信號標誌
  4. 內核根據用戶空間的配置,處理信號。若是用戶空間沒有特別配置,則按照默認行爲處理
  5. 處理完成後,清除信號標誌
  6. 回到 1,繼續等待

sigprocmask()所作的 「屏蔽」,其實就是將上述的信號處理流程,卡在了 3 和 4 之間,讓內核可以將信號標誌設置好,可是卻到不了判斷並處理的那一步。
換句話說,即使進程調用 signal() 函數,設置了 SIG_IGN 標誌,但若是指定的信號被 sigprocmask() 屏蔽了的話,內核也不會去判斷是否該忽略這個信號,而只是把信號標誌卡在那兒,直到調用sigprocmask()執行SIG_UNBLOCK爲止,才能讓內核繼續走到第 4 步。函數

在程序正文處理信號

這裏所說的 「正文」,指的是:
  不在 signal()sigaction() 中指定的 handler 中處理信號事件,而是在普通的程序流程可以中捕捉信號,而且處理信號。oop

這麼作有不少好處:

  • 中斷處理函數有不少限制,只能調用某些系統調用,不然可能致使上下文異常。但在正文中就不會有這個問題
  • 中斷處理函數和正文之間能夠視爲兩個不一樣的線程,二者之間的同步比較麻煩
  • 在正文中處理,能夠實現相似於 libeventEV_SIGNAL 功能——而這也是筆者正在研究的。

基本軟件流程以下:

  1. 使用 signal()sigaction() 將須要捕獲的信號設置爲 SIG_IGN
  2. 使用 sigprocmask() 屏蔽須要捕獲的信號,同時注意將屏蔽以前的信號集保存下來(oset參數)
  3. 進行相應操做(好比 epoll()
  4. 若是發現 errnoEINTR,那麼就能夠用 sigpending() 獲取被屏蔽的信號集,判斷須要捕獲的信號是否在信號集中
  5. 使用 sigprocmask() 執行一次 SIG_UNBLOCK 操做,讓內核清除信號集標誌
  6. 回到 2,從新屏蔽信號

缺陷

不過這個流程有一個 bug,就是信號有可能在 4 和 6 之間產生,這樣的話,就捕獲不到了——這還須要想一想怎麼處理。

sigaction 函數

這裏順便記一下 sigaction() 吧,POSIX 是建議不要再使用 signal() 了。
簡單狀況下,只須要使用 struct sigcation 裏的 sa_handlersa_mask 就能夠替代 signal() 調用了。

#include <signal.h>

    struct sigaction {
        void     (*sa_handler)(int);
        void     (*sa_sigaction)(int, siginfo_t *, void *);
        sigset_t   sa_mask;
        int        sa_flags;
        void     (*sa_restorer)(void);
    };

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

errno 的線程安全問題

前文說起 「若是發現 errno 爲 EINTR ...」。有同窗可能會問了:「errno 是一個全局變量啊,這安全不?」
實際上,errno線程安全的……呃,這個優勢,其實筆者本身也是才知道……看了一下 errno 的原理,以爲實在是很厲害啊!

可是,使用 errno 只有一點要注意,就是雖然在程序正文中,errno 是線程安全的,可是在中斷處理函數中卻並非這樣。其餘位置的話,隨意。

這裏參考的資料是這個還有這個

相關文章
相關標籤/搜索