对应标题, 本节讨论的是信号和信号IO.首先介绍信号
Signal
在Linux操作系统中, 产生信号有这样的方式:
- 用户输入特殊的终端字符来给它发送信号,
SIGINT
- 系统异常, 如
SIGKILL
,SIGTERM
- 系统状态变化, 如基于
alarm
的SIGALRM
信号 - 运行
kill(2)
或者调用kill(1)
信号产生的方式多样, 种类各异, 从不同的Unix变种
到POSIX标准
增加了很多信号, 具体查看man page
我们对于信号的处理方式有这么两种:
- 安装信号处理函数, 顾名思义: 对于指定信号使用专门的处理函数处理
- 使用默认行为, 有
结束进程
,忽略信号
,结束进程并生成核心转储文件
,暂停进程
,继续进程
我们首先来说一个问题: 中断系统调用 对于, 阻塞状态的系统调用, 收到某些信号, 会导致从阻塞状态结束, 并且返回EINTR
. 对于这种行为, 我们应该这样处理:
- 忽略
EINTR
错误 - 同时在
sigaction
处理函数中,sa.flags
字段设置SA_RESTART
选项, 以重启被中断的系统调用, 同之前所讲,connect
是不能被重启的
信号处理函数
我们一般使用这样两种信号处理函数
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler); (1)
int sigaction(int signum, const struct sigaction *act, (2)
struct sigaction *oldact);
- 使用较为简单
- 可以定制更多的行为细节
signal
: 用新的信号处理函数, 处理指定信号, 并且返回原来的该信号行为处理函数 sigaction
: 用指定的方式act
处理指定信号signum
, 并且返回原来的方式oldact
重点介绍一下sigaction
:
struct sigaction {
void (*sa_handler)(int); // 信号处理函数
void (*sa_sigaction)(int, siginfo_t *, void *); // 同上, 某些OS实现时联合体
sigset_t sa_mask; // 信号掩码, 用于屏蔽信号
int sa_flags; // 设置标志
void (*sa_restorer)(void); // 不是为应用准备的,见sigreturn(2)
};
结构是挺清晰地. 那么我们着重分析的就是sa_flags
字段的内容:
SA_NOCLDSTOP
: 表示对于SIGCHLD
信号, 不会产生SIGCHLD
信号SA_NOCLDWAIT
: 对于SIGCHLD
信号, 结束时不产生僵尸进程SA_SIGINFO
: 使用sa_sigaction
字段的处理函数SA_RESTART
: 重启被中断的系统调用SA_RESETHAND
: 信号处理函数结束之后, 重新恢复默认处理函数- ….
默认行为
要对信号使用默认行为, 我们需要signal(2)
进行操作 一般的默认行为支持: SIG_IGN
, SIG_TERM
等几种
如: ::signal(SIGPIPE, SIG_IGN);
即可忽略SIGPIPE
信号
统一事件源
我们之前曾经提到过: 我们进行网络编程的主流是事件驱动模型, 使用IO多路复用 + 非阻塞IO 但是, 信号是异步事件, 我们不知道何时完成, 因此, 如何和主循环融合就是一个很重要的问题
之前的解法: 手动模拟通知
思路是:
- 创建管道
- 指定信号的默认处理行为是向管道写端写入的数据
- 使用IO多路复用检测管道读端
- 读端就绪, 则说明信号产生, 可以执行指定的行为
在此我们就不展示具体的实现了.
我们建议的方式是使用signalfd(2)
进行事件源统一
#include <sys/signalfd.h>
int signalfd(int fd, const sigset_t *mask, int flags);
参数含义如下:
fd
: 可以为-1
(表示由系统指定signalfd
), 或者指定已经创建的signalfd
mask
: 设置信号掩码, 是signalfd
所关注的信号flags
: 同各种和文件描述有关的操作SFD_NONBLOCK
,SFD_CLOEXEC
使用signalfd
的好处在于: 可以将信号事件抽象成为一个文件描述符, 融入多路复用中, 有信号产生, 会导致文件描述符就绪. 这个时候, 我们进行相关的信号处理即可.
eventfd
, signalfd
, timerfd的产生已经证明事件驱动模型的确是Linux网络编程的主流
最最重要的是: signalfd
支持线程语义 即: 在多线程程序里的signalfd
文件描述符语义对应信号标准语义。换句话说,当一个线程读一个 signalfd
文件描述符时,它将读取直接发送给其自身的信号或直接发送进程的信号(也就是整个线程组)。 一言以蔽之: 线程只能读到自己的信号, 不会读到其他线程产生的信号
网络编程相关的信号
在网络编程中 我们着重需要关注这样几个信号: SIGHUP
, SIGPIPE
, SIGURG
SIGHUP
: 当进程挂起控制终端时, 产生该信号. 一般用户后台服务器强制重新读取配置文件SIGPIPE
: TCP连接时全双工的, 在对端关闭后, 继续写会产生[RST]
, 下一次写就是SIGPIPE
该信号会导致进程关闭, 一般通过read == 0
或者EPOLLRDHUP
判断, 忽略该信号SIGURG
: 与带外数据相关
Signal-driven IO
信号驱动IO是基于信号的, 我们先介绍信号驱动IO的行为, 之后分析其必要性与可行性.
信号驱动IO的思路是: 注册信号处理函数
->
产生指定信号
->
信号处理函数执行行为
一般实现是这样的:
- 为通知信号安装信号处理例程, 一般是
SIGIO
- 设置文件描述符的属主, 即可以指定接受到信号的进程或进程组
- 使用非阻塞IO,
O_NONBLOCK
- 使用信号驱动IO,
O_ASYNC
- 进程继续执行其他任务
- 信号产生, 执行信号处理例程
**切记: **信号触发是便于触发模式, 所以也类似与Epoll(ET)
, 要使用非阻塞IO, 同时读到EAGAIN
上面的向为看上去还有点帅, 因为可以使IO与计算重叠 但是, 我们再来分析一下信号驱动IO的可行性:
SIGIO
信号的局限性 使用SIGIO
基本是不可行的, TCP通信时, 各种情况和状态都会触发同一个信号SIGIO
, 如何进行事件处理的选择? 没办法了吧, 这一点就是信号驱动IO最严重的问题. 对于UDP呢? 勉强可用, 极其有限. 一般UDP通信也不是用信号驱动IOSIGIO
信号本身的属性SIGIO
是非排队信号, 我们应该使用fcntl(F_SETSIG, sig)
指定一个其他的实时信号, 如果不这么做, 一旦一个IO阻塞, 后续信号全部丢失- 一般需要设置
SA_SIGINFO
一般建议设置SA_SIGINFO
标识,siginfo_t
结构体会被传入信号处理例程, 其中包含了信号产生的例程, 即可以得知信号产生进程. 避免出现不知道处理的信号产生自进程.2
,3
一般必须同时设置, 否则仍然是默认的SIGIO
信号 - 信号溢出队列 使用
ulimit -i
即可查看可等待的信号数, 超过此上限之后, 所有的信号优惠恢复默认的行为SIGIO
, 一般的操作是溢出时: 立即使用sigwaitinfo(2)
将信号全部捕获, 临时切换多路复用继续运行
因此, 结合上述的论点, 我们对于signal
在单线程中勉强可以使用, 多线程中, 最好不要使用signal
, 一方面是信号会打断控制流, 另一方面是信号有的是发送给进程的, 有的是进程中某一线程 从最顶层考虑, 还是因为标准最早没有考虑到多线程需求的.