最近在尝试使用 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,继续等待
sigprocmask()
所做的 “屏蔽”,其实就是将上述的信号处理流程,卡在了 3 和 4 之间,让内核能够将信号标志设置好,但是却到不了判断并处理的那一步。
换句话说,即便进程调用 signal()
函数,设置了 SIG_IGN
标志,但如果指定的信号被 sigprocmask()
屏蔽了的话,内核也不会去判断是否该忽略这个信号,而只是把信号标志卡在那儿,直到调用sigprocmask()
执行SIG_UNBLOCK
为止,才能让内核继续走到第 4 步。
在程序正文处理信号
这里所说的 “正文”,指的是:
不在 signal()
或 sigaction()
中指定的 handler 中处理信号事件,而是在普通的程序流程能够中捕捉信号,并且处理信号。
这么做有很多好处:
- 中断处理函数有很多限制,只能调用某些系统调用,否则可能导致上下文异常。但在正文中就不会有这个问题
- 中断处理函数和正文之间可以视为两个不同的线程,两者之间的同步比较麻烦
- 在正文中处理,可以实现类似于
libevent
中EV_SIGNAL
功能——而这也是笔者正在研究的。
基本软件流程如下:
- 使用
signal()
或sigaction()
将需要捕获的信号设置为SIG_IGN
- 使用
sigprocmask()
屏蔽需要捕获的信号,同时注意将屏蔽之前的信号集保存下来(oset
参数) - 进行相应操作(比如
epoll()
) - 如果发现
errno
为EINTR
,那么就可以用sigpending()
获取被屏蔽的信号集,判断需要捕获的信号是否在信号集中 - 使用
sigprocmask()
执行一次SIG_UNBLOCK
操作,让内核清除信号集标志 - 回到 2,重新屏蔽信号
缺陷
不过这个流程有一个 bug,就是信号有可能在 4 和 6 之间产生,这样的话,就捕获不到了——这还需要想想怎么处理。
sigaction 函数
这里顺便记一下 sigaction()
吧,POSIX 是建议不要再使用 signal()
了。
简单情况下,只需要使用 struct sigcation
里的 sa_handler
和 sa_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
是线程安全的,但是在中断处理函数中却并不是这样。其他位置的话,随意。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。