SIGCHLD为何不中断read等系统调用?如何配置使其中断?
问题回顾
当进程收到SIGCHLD信号时,read这类阻塞系统调用不会返回EINTR错误;但收到其他信号时,未设置SA_RESTART的情况下会正常触发EINTR。哪怕把SIGCHLD的处理函数设为自定义逻辑或SIG_DFL,甚至显式调用siginterrupt(SIGCHLD, TRUE),都没法改变这个行为。
一、SIGCHLD特殊行为的原因
这是内核层面的硬编码设计,根源在于SIGCHLD的定位:它是用来通知父进程子进程状态变化(终止、暂停、继续)的"状态通知类信号",而非需要立即打断进程执行的"紧急信号"(比如SIGINT、SIGTERM)。
早期UNIX设计时,考虑到父进程可能长期处于阻塞IO状态(比如等待用户输入、网络数据),如果每次子进程退出都打断这些核心操作,会导致程序逻辑变得异常繁琐——开发者不得不频繁处理EINTR,再手动重启系统调用,反而违背了SIGCHLD"悄悄通知"的设计初衷。因此内核默认给SIGCHLD加了特殊逻辑:无论是否设置SA_RESTART,只要触发SIGCHLD,内核都会自动重启被打断的阻塞系统调用,不会返回EINTR。
siginterrupt本质是用来切换信号是否触发EINTR的,但它对SIGCHLD无效,因为内核直接忽略了这个配置,强制重启系统调用。
二、让系统调用被SIGCHLD中断的可行方案
要绕开内核的默认行为,有两种可靠的思路:
1. 用signalfd将信号转为IO事件(推荐)
这是最通用且可靠的方法,把SIGCHLD的触发转化为文件描述符可读事件,和原有IO操作统一处理:
- 第一步:调用
signalfd(-1, &sigset, SFD_NONBLOCK | SFD_CLOEXEC)创建一个监听SIGCHLD的文件描述符,其中sigset要包含SIGCHLD; - 第二步:把这个
signalfd和你要监听的IO文件描述符(比如read用的套接字、终端fd)一起加入select/poll/epoll的监听集合; - 第三步:当SIGCHLD触发时,
signalfd会变为可读,此时select/poll/epoll会立即返回,你可以读取signalfd中的信号信息,同时原有的阻塞read会被唤醒(如果还没完成IO的话),实现类似"中断"的效果。
这种方式相当于把信号事件融入IO多路复用模型,完全规避了内核的自动重启逻辑。
2. 手动标记事件,在系统调用返回后处理
如果不想用IO多路复用,可以在SIGCHLD的处理函数中设置一个全局标志位,同时在处理函数中调用waitpid(-1, NULL, WNOHANG)清理子进程僵尸状态:
volatile sig_atomic_t has_child_exit = 0; void sigchld_handler(int sig) { // 清理僵尸进程,避免信号堆积 while (waitpid(-1, NULL, WNOHANG) > 0); has_child_exit = 1; }
之后在主循环中,每次read返回后检查has_child_exit标志位,如果为1则处理子进程退出逻辑。这种方式不是真正的"中断"系统调用,但能让你及时响应SIGCHLD事件,同时不依赖内核的重启行为。
3. Linux特定:使用prctl结合信号掩码
在Linux系统中,你可以先调用prctl(PR_SET_CHILD_SUBREAPER, 1)将当前进程设为子收割者,避免子进程变成僵尸,然后结合sigprocmask临时阻塞SIGCHLD,在需要中断系统调用的时间段内解除阻塞。不过这种方式的可靠性不如前两种,因为内核依然可能在某些场景下重启系统调用。
内容的提问来源于stack exchange,提问作者Caulder

