Ruby中线程内创建子进程为何会导致信号trap块内sleep行为异常
Ruby子线程内执行子进程时信号捕获异常原因分析
问题复现背景
正常运行的三个示例
示例1:基础信号捕获实现
signals = %w[INT TERM] signals.each do |signal| Signal.trap(signal) do puts "trapping the signal and sleeping for 5 seconds" sleep 5 puts "done, exiting" exit end end sleep 100
启动后按Ctrl+C,Ruby会等待5秒后退出。
示例2:子线程执行Ruby级别sleep
# ...复用示例1的信号捕获代码... t = Thread.new{ sleep 10 } t.join
启动后按Ctrl+C,Ruby会等待5秒后退出。
示例3:主线程执行shell命令sleep
# ...复用示例1的信号捕获代码... `sleep 10`
启动后按Ctrl+C,Ruby会等待5秒后退出。
异常的第四个示例
# ...复用示例1的信号捕获代码... t = Thread.new{ `sleep 10` } t.join
启动后按Ctrl+C,Ruby会直接退出:虽然会打印trapping the...和done...两行输出,但两者之间的sleep 5似乎被直接跳过。
底层技术原因
这个异常和Ruby对信号、子进程的处理规则直接相关,核心逻辑如下:
- Ruby信号处理基础规则
- 所有用户定义的信号处理器,默认都在主线程执行,和信号触发时正在运行的线程无关。
Kernel.sleep是可中断调用:被信号中断后,Ruby不会自动重启睡眠逻辑,会直接返回剩余的睡眠时间,继续执行后续代码,不会补足剩余的睡眠时长。
- 进程组信号传递规则
按下Ctrl+C触发的SIGINT信号,会发送给整个前台进程组的所有进程,包括Ruby主进程和它fork出来的所有子进程。 - 第四个示例的异常触发时序
- 子线程执行反引号
sleep 10时,会fork出独立的sleep子进程,子线程阻塞在waitpid系统调用,等待子进程返回。 - 按下Ctrl+C后:
- 独立的sleep子进程收到SIGINT信号,立刻终止退出,同时向Ruby主进程发送SIGCHLD信号(子进程退出时默认向父进程发送的通知信号)。
- Ruby主进程收到SIGINT,立刻切换到主线程执行自定义的信号处理函数,先打印
trapping the signal and sleeping for 5 seconds,随后调用sleep 5。 - 此时SIGCHLD信号递达Ruby主进程,直接中断了信号处理函数中正在执行的
sleep 5调用,按照Ruby的规则,sleep被中断后直接返回,不会继续睡剩下的时间。 - 信号处理函数继续执行,打印
done, exiting后调用exit退出,所以看起来sleep 5被直接跳过。
- 子线程执行反引号
- 前三个示例正常运行的原因
- 示例1:没有额外子进程,信号处理执行sleep 5期间没有其他信号中断,睡眠可以正常完成。
- 示例2:子线程的sleep是Ruby虚拟机级别的线程睡眠,没有fork独立子进程,不会产生SIGCHLD信号,sleep 5不会被中断。
- 示例3:子进程由主线程fork,主线程本身阻塞在反引号对应的
waitpid调用上,子进程退出时主线程立刻回收子进程,SIGCHLD信号被消费,不会中断后续信号处理函数里的sleep 5。
你可以通过在代码开头增加一行Signal.trap('CHLD', 'IGNORE')验证上述逻辑:忽略SIGCHLD信号后,第四个示例的sleep 5会正常执行,程序等待5秒后退出。
内容的提问来源于stack exchange,提问作者John Bachir
相关产品推荐
相关产品推荐

