Ruby信号处理详解
Ruby中Process.kill方法发送信号,signal参数可为整数或符号名指定信号类型,pid参数为正数时发送给指定进程,为0时发送给同组所有进程,为负数时发送给对应进程组(绝对值)。
Ruby使用Process.kill发送信号
Process.kill(signal, pid, ...) → integer
先来看Process.kill的作用:发送指定的信号给一个或多个进程或进程组。它的行为取决于pid参数的取值——这几个规则其实很直白:

- 如果目标
pid>0,那就发送信号给指定PID的那个进程。 - 如果目标
pid=0,信号会发给调用kill的进程所在进程组里的所有进程。 - 如果目标
pid<0,情况稍微复杂一点,得看操作系统。以Linux为例: - 当
pid=-1时,信号会发给除了init进程(PID=1)以外的所有进程——当然,没有权限的进程会被跳过。 - 当
pid<-1时,信号发给-pid这个进程所在的进程组里的所有进程。比如-3000,就是把信号发给PID为3000的进程所在的整个进程组。
那么Process.kill的第一个参数——信号——怎么指定?
- 信号可以是字符串格式的名字,也可以是数值。比如
INT、SIGINT、1都是有效的。 - 如果信号带个负号(比如
-2或-INT),意思是发送给进程所在的进程组,而不是具体的进程。不过Linux不支持这种写法。 - 如果信号是0,那就不是真的发送信号,而是探测能不能把信号发过去——可以用来检查目标进程是否存在,或者当前进程是否有权限管理它。
pid = fork do
sleep 300
end
# ...
Process.kill("HUP", pid)
Process.wait
Ruby使用trap()设置信号处理程序
在Ruby里,捕获信号并定义处理逻辑,用的是Kernel.trap或Signal.trap,这俩完全等价。可以同时为多个信号设置不同的trap。
Signal.trap(0, proc { puts "Terminating: #{$$}" })
Signal.trap("CLD") { puts "Child died" }
fork && Process.wait
=begin
Terminating: 27461
Child died
Terminating: 27460
=end
trap的第一个参数是要监控的信号,同样可以是字符串名(比如SIGINT)、省略SIG前缀的名字(比如INT),或者数值(比如2)。Ruby还支持一个特殊的信号0,对应的字符串名字是EXIT或SIGEXIT——它会在进程退出时触发。
第二个参数或者代码块,就是捕获信号后要执行的内容。第二个参数有几种特殊的字符串值:
IGNORE或SIG_IGN:忽略这个信号。DEFAULT或SIG_DFL:按Ruby的默认处理规则来。EXIT:以状态码0退出当前进程。SYSTEM_DEFAULT:按系统的默认信号处理规则,一般是以状态码141退出。
避免信号覆盖
用第三方库的时候,经常不确定它有没有给某个信号注册处理程序——就算知道它注册了,自己也想追加逻辑,但不想覆盖人家的处理。怎么办?巧用trap的返回值就行。每次调用trap,它都会返回之前已经注册的处理程序(一个Proc对象)。不过要注意:有些信号的初始处理程序是字符串DEFAULT,而不是Proc对象,所以得做类型判断:
# 第一次定义INT的信号处理程序
first_trap = trap('INT') {
first_trap.call if first_trap.is_a? Proc
puts "first_trap"
}
# 第二次定义INT的信号处理程序
old_trap = trap('INT') {
old_trap.call if old_trap.is_a? Proc # 调用第一次定义的信号处理程序
puts "old trap" # 本次trap时执行的逻辑
}
# 定义好之后,old_trap为第一次定义的信号处理程序
# 之后按下CTRL+C触发INT信号的信号处理程序
多线程信号注册问题
如果是在多线程环境里注册信号处理程序,有一个重要规则:信号处理程序总是注册在所在进程的main线程中——哪怕你是在其他线程里调用的trap()。来看个例子:
pid = fork do
puts "main Thread: #{Thread.current}"
Thread.new {
puts "new Thread: #{Thread.current}"
trap("TERM", proc { puts "Signal: #{Thread.current}" })
sleep 2
}
sleep 2
end
sleep 1
Process.kill 'SIGTERM', pid
=begin
main Thread: #
new Thread: #
Signal: #
=end
输出很说明问题:信号处理实际是在main线程里执行的,而不是在那个新线程里。
子进程继承信号处理程序
子进程会从父进程那里继承信号处理程序。这一点可能出乎意料,但确实如此:
trap 'TERM', proc { puts "Signal: #{Process.pid}" }
puts "Parent: #{Process.pid}"
pid = fork do
sleep 30
end
puts "Child: #{pid}"
Process.kill 'TERM', pid
=begin
Parent: 2872
Child: 2901
Signal: 2901
=end
可以看到,子进程的TERM信号处理程序直接继承自父进程——这在设计多进程架构时是个需要留意的细节。
Postman是一套面向API全生命周期的开发与测试平台,可在Mac上发送和调试多种协议请求,管理集合、环境及认证信息,并完成脚本测试、模拟服务、监控、文档发布和团队协作。它适合开发、测试及运维人员使用,提供个人免费方案。
Docker Desktop 是 Docker 官方为 Mac 提供的容器开发环境,集成 Docker Engine、命令行工具、Compose、Build、Kubernetes 与图形化管理界面。
Android Studio 是 Google 面向安卓应用开发提供的官方集成开发环境,基于 IntelliJ IDEA 构建。它整合智能代码编辑、Gradle 构建、界面预览、设备模拟、性能分析、调试测试及 APK 分析等工具。
Postman 是面向 API 全生命周期的开发与协作平台。Windows 桌面版可用于构建和发送 HTTP、GraphQL、gRPC、WebSocket、MQTT 等请求,并提供环境变量、集合、脚本测试、模拟服务、监控、文档及团队工作区等
Docker Desktop 为 Windows 开发者提供集成式容器环境,内置 Docker Engine、命令行工具、Compose、Build 与 Kubernetes 等组件,可在本机完成镜像构建、容器运行、日志查看和资源管理。
Android Studio 是 Google 面向 Android 应用开发提供的官方集成开发环境,以 IntelliJ IDEA 为基础,集成智能代码编辑、Gradle 构建、设备模拟、界面预览、调试测试及性能分析工具,













