I'm going to consider 2 cases:
-
No signal handler was registered:
ruby -e sleep
-
A signal handler was registered:
ruby -e 'Signal.trap("TERM") do puts "SIGTERM" end; sleep'
On initialization ruby
sets a SIGTERM
handler:
main()
->rb_main()
rb_main()
->ruby_init()
ruby_init()
->ruby_setup()
ruby_setup()
->rb_call_inits()
rb_call_inits()
->Init_signal()
Init_signal()
->install_sighandler(SIGTERM, sighandler)
On receiving SIGTERM
it puts it into a queue:
sighandler()
->signal_enque()
signal_enque()
->ATOMIC_INC(signal_buff.cnt[sig])
And when it has time it tries to execute the user handler (if it was registered):
rb_f_sleep()
->rb_thread_sleep_forever()
rb_thread_sleep_forever()
->sleep_forever()
sleep_forever()
->vm_check_ints_blocking()
vm_check_ints_blocking()
->rb_threadptr_execute_interrupts()
rb_threadptr_execute_interrupts()
->rb_get_next_signal()
rb_get_next_signal()
->ATOMIC_DEC(signal_buff.cnt[i])
rb_threadptr_execute_interrupts()
->rb_signal_exec()
If there's no signal handler, it raises an exception. If there is, it invokes it.
Another way to confirm this:
$ docker run --rm ruby:3.3.5-alpine3.20 \
sh -euc 'apk add strace && strace -e trace=rt_sigaction ruby --disable gems -e "" 2>&1 | grep TERM'
rt_sigaction(SIGTERM, {sa_handler=0x7aa469668bb0, sa_mask=[], sa_flags=SA_RESTORER|SA_SIGINFO, sa_restorer=0x7aa469acefeb}, {sa_handler=SIG_DFL, sa_mask=[], sa_flags=0}, 8) = 0
Apparently ruby
does this to delay execution of signal handlers to a moment where it's safe to execute them. It's probably the reason similar to the one explained in the perl
docs:
Prior to Perl 5.8.0 it was necessary to do as little as you possibly could in your handler; notice how all we do is set a global variable and then raise an exception. That's because on most systems, libraries are not re-entrant; particularly, memory allocation and I/O routines are not. That meant that doing nearly anything in your handler could in theory trigger a memory fault and subsequent core dump - see "Deferred Signals (Safe Signals)" below.