使用 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 是线程安全的,可是在中断处理函数中却并非这样。其余位置的话,随意。

这里参考的资料是这个还有这个

相关文章
相关标签/搜索