2

最近在尝试使用 epoll 写一个类似 libevent 的库。那么,如何像 libevent 一样,在 event loop 里加入对信号事件的观测呢?
我查了一下资料,一个可行的方法,就是使用 sigprocmask() 及其相关功能来实现啦。

但是请注意,这个方法是存在缺陷的,请看官留心。
个人在继续研究之后,暂时是不打算使用此种方法来实现信号事件,而改用另一个方法

Reference

《UNIX 环境高级编程》
sigprocmask , sigpending 和 sigsuspend函数
errno多线程安全
Linux 多线程应用中编写安全的信号处理函数

UNIX 系统主要信号

以下就只列出主要的信号了:

名称 说明 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 代表失败。

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 中处理信号事件,而是在普通的程序流程能够中捕捉信号,并且处理信号。

这么做有很多好处:

  • 中断处理函数有很多限制,只能调用某些系统调用,否则可能导致上下文异常。但在正文中就不会有这个问题
  • 中断处理函数和正文之间可以视为两个不同的线程,两者之间的同步比较麻烦
  • 在正文中处理,可以实现类似于 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 是线程安全的,但是在中断处理函数中却并不是这样。其他位置的话,随意。

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


amc
927 声望228 粉丝

电子和互联网深耕多年,拥有丰富的嵌入式和服务器开发经验。现负责腾讯心悦俱乐部后台开发