Overview
Signal trapping in Ruby provides a mechanism for handling operating system signals sent to running processes. Ruby's Signal module offers methods to register handlers that execute when specific signals arrive, enabling applications to respond to external events like user interrupts, system shutdowns, or custom process communication.
The Signal module operates by registering trap handlers that execute when the Ruby interpreter receives designated signals. These handlers run asynchronously, interrupting normal program flow when signals arrive. Ruby supports most POSIX signals and provides platform-specific signal handling capabilities.
# Basic signal handler registration
Signal.trap("INT") do |signo|
puts "Received signal #{signo}"
exit(0)
end
# Alternative syntax using signal numbers
Signal.trap(2) do |signo|
puts "SIGINT received: #{signo}"
end
# Check current trap handler
current_handler = Signal.trap("INT", "DEFAULT")
puts current_handler.class # => String or Proc
Ruby processes inherit signal dispositions from their parent process, but applications can override these settings using Signal.trap. The module handles signal delivery across threads and manages the execution context for trap handlers.
Signal handlers execute in the main thread regardless of which thread receives the signal. This design choice simplifies signal handling but requires careful coordination when multiple threads access shared resources. Ruby queues signals that arrive during handler execution, processing them after the current handler completes.
Basic Usage
Signal trapping begins with registering handlers using Signal.trap, which accepts signal names or numbers and handler objects. Ruby supports string signal names, integer signal numbers, and symbolic signal names.
# String-based signal names
Signal.trap("TERM") { puts "Termination requested" }
Signal.trap("USR1") { puts "User signal 1" }
Signal.trap("HUP") { puts "Hangup signal" }
# Numeric signal identifiers
Signal.trap(15) { puts "SIGTERM received" }
Signal.trap(10) { puts "SIGUSR1 received" }
# Symbolic references using Signal constants
Signal.trap(Signal::SIGINT) { puts "Interrupt signal" }
Handler blocks receive the signal number as their first argument, enabling generic signal processing logic. Ruby passes the signal number even when handlers are registered using string names.
# Generic handler processing multiple signals
handler = proc do |signal|
case signal
when 2, 15 # SIGINT or SIGTERM
puts "Shutdown signal #{signal} received"
cleanup_resources
exit(0)
when 10 # SIGUSR1
puts "Reloading configuration due to signal #{signal}"
reload_config
else
puts "Unknown signal: #{signal}"
end
end
Signal.trap("INT", &handler)
Signal.trap("TERM", &handler)
Signal.trap("USR1", &handler)
Ruby provides predefined handler strings for common signal processing patterns. The "DEFAULT" string restores the system's default signal handling behavior, while "IGNORE" prevents the signal from affecting the process.
# Restore default signal handling
Signal.trap("INT", "DEFAULT")
# Ignore specific signals
Signal.trap("PIPE", "IGNORE")
# Save and restore previous handlers
old_int_handler = Signal.trap("INT") { puts "Custom handler" }
Signal.trap("INT", old_int_handler) # Restore previous handler
Signal.list returns a hash mapping signal names to their numeric values, providing a comprehensive reference for available signals on the current platform.
# Enumerate available signals
Signal.list.each do |name, number|
puts "#{name}: #{number}"
end
# Check for platform-specific signals
if Signal.list.key?("WINCH")
Signal.trap("WINCH") { handle_window_resize }
end
Thread Safety & Concurrency
Signal handlers execute in the main thread regardless of the thread that receives the signal, creating specific concurrency considerations for multithreaded Ruby applications. Ruby's signal handling mechanism queues signals during handler execution and processes them sequentially after each handler completes.
Thread safety becomes critical when signal handlers access shared data structures or modify global state. Ruby's Global Interpreter Lock (GIL) provides some protection, but signal handlers can still interrupt critical sections and create race conditions.
require 'thread'
class SafeSignalHandler
def initialize
@mutex = Mutex.new
@shutdown_requested = false
@worker_threads = []
end
def setup_signals
Signal.trap("INT") do |signal|
@mutex.synchronize do
puts "Shutdown requested via signal #{signal}"
@shutdown_requested = true
end
wake_workers
end
Signal.trap("USR1") do |signal|
@mutex.synchronize do
puts "Status requested via signal #{signal}"
report_status
end
end
end
def shutdown_requested?
@mutex.synchronize { @shutdown_requested }
end
private
def wake_workers
@worker_threads.each(&:wakeup)
end
def report_status
puts "Active threads: #{@worker_threads.size}"
puts "Memory usage: #{GC.stat[:heap_live_slots]} objects"
end
end
Signal delivery timing creates challenges when handlers modify data structures accessed by other threads. Atomic operations and careful synchronization prevent data corruption in multithreaded signal handling scenarios.
require 'atomic'
class AtomicSignalCounter
def initialize
@signal_count = Atomic.new(0)
@last_signal = Atomic.new(0)
@processing = Atomic.new(false)
end
def setup_counting_handler
Signal.trap("USR1") do |signal|
return if @processing.compare_and_swap(false, true)
begin
current_count = @signal_count.update { |v| v + 1 }
@last_signal.update { |_| Time.now.to_f }
puts "Signal #{signal} count: #{current_count}"
ensure
@processing.update { |_| false }
end
end
end
def stats
{
count: @signal_count.value,
last_signal: Time.at(@last_signal.value),
processing: @processing.value
}
end
end
Thread coordination requires careful attention to blocking operations in signal handlers. Ruby signal handlers should avoid operations that might block indefinitely, as this can prevent signal processing and create deadlocks in multithreaded applications.
class NonBlockingSignalHandler
def initialize
@signal_queue = Queue.new
@shutdown = false
setup_signal_processor
end
def setup_signals
["INT", "TERM", "USR1", "USR2"].each do |sig|
Signal.trap(sig) do |signal_num|
# Non-blocking signal queuing
@signal_queue.push([sig, signal_num, Time.now]) rescue nil
end
end
end
private
def setup_signal_processor
Thread.new do
until @shutdown
begin
signal_data = @signal_queue.pop(non_block: true)
process_queued_signal(*signal_data)
rescue ThreadError
sleep(0.1) # Queue empty, brief pause
end
end
end
end
def process_queued_signal(signal_name, signal_num, timestamp)
case signal_name
when "INT", "TERM"
puts "Shutdown signal #{signal_name} (#{signal_num}) at #{timestamp}"
@shutdown = true
when "USR1"
puts "Status request via #{signal_name} at #{timestamp}"
print_status
when "USR2"
puts "Config reload via #{signal_name} at #{timestamp}"
reload_configuration
end
end
end
Error Handling & Debugging
Signal trap registration can fail when attempting to handle non-trappable signals or when platform restrictions prevent handler installation. Ruby raises ArgumentError for invalid signal specifications and Errno exceptions for system-level signal handling failures.
class RobustSignalHandler
RESTRICTED_SIGNALS = %w[KILL STOP SEGV].freeze
def register_handler(signal, &block)
return false if RESTRICTED_SIGNALS.include?(signal.to_s.upcase)
begin
old_handler = Signal.trap(signal, &block)
puts "Registered handler for #{signal}"
old_handler
rescue ArgumentError => e
warn "Invalid signal specification: #{signal} - #{e.message}"
nil
rescue SystemCallError => e
warn "System error registering #{signal} handler: #{e.message}"
nil
end
end
def safe_signal_list
available_signals = {}
Signal.list.each do |name, number|
begin
# Test if signal can be trapped by temporarily setting default
old_handler = Signal.trap(name, "DEFAULT")
Signal.trap(name, old_handler)
available_signals[name] = number
rescue ArgumentError, SystemCallError
# Signal cannot be trapped, skip it
end
end
available_signals
end
end
Debugging signal handler execution requires special techniques because handlers run asynchronously and can interrupt normal program flow at arbitrary points. Signal delivery timing issues often manifest as intermittent bugs that are difficult to reproduce.
class DebuggableSignalHandler
def initialize(debug: false)
@debug = debug
@signal_log = []
@handler_stack = []
end
def debug_trap(signal, &handler)
wrapped_handler = proc do |signo|
entry_time = Time.now
if @debug
@signal_log << {
signal: signal,
number: signo,
timestamp: entry_time,
thread: Thread.current.object_id,
call_stack: caller[0..5]
}
end
@handler_stack.push(signal)
begin
result = handler.call(signo)
if @debug
@signal_log.last[:duration] = Time.now - entry_time
@signal_log.last[:result] = result.class.name
end
result
rescue => e
if @debug
@signal_log.last[:error] = e.message
@signal_log.last[:error_class] = e.class.name
end
warn "Error in #{signal} handler: #{e.message}"
warn e.backtrace.first(3).join("\n")
raise
ensure
@handler_stack.pop
end
end
Signal.trap(signal, &wrapped_handler)
end
def signal_report
return "Debug mode disabled" unless @debug
report = ["Signal Handler Debug Report"]
report << "=" * 30
@signal_log.each_with_index do |entry, idx|
report << "#{idx + 1}. Signal: #{entry[:signal]} (#{entry[:number]})"
report << " Time: #{entry[:timestamp]}"
report << " Thread: #{entry[:thread]}"
report << " Duration: #{entry[:duration]&.round(4)}s"
report << " Error: #{entry[:error]}" if entry[:error]
report << ""
end
report.join("\n")
end
end
Signal handler exceptions require careful management because uncaught exceptions in handlers can terminate the entire process. Ruby's exception handling mechanisms work within signal handlers, but exception propagation follows different rules than normal method execution.
module SignalErrorHandling
def self.safe_handler(&block)
proc do |signal|
begin
block.call(signal)
rescue => e
error_info = {
signal: signal,
error: e.class.name,
message: e.message,
backtrace: e.backtrace&.first(5),
timestamp: Time.now
}
log_signal_error(error_info)
# Attempt graceful degradation
case e
when SystemCallError
warn "System error in signal handler: #{e.message}"
when StandardError
warn "Application error in signal handler: #{e.message}"
else
warn "Unexpected error in signal handler: #{e.class} - #{e.message}"
raise # Re-raise serious errors
end
end
end
end
def self.log_signal_error(info)
File.open("signal_errors.log", "a") do |f|
f.puts "[#{info[:timestamp]}] Signal #{info[:signal]} Error:"
f.puts " #{info[:error]}: #{info[:message]}"
info[:backtrace]&.each { |line| f.puts " #{line}" }
f.puts
end
rescue
# Fallback if logging fails
warn "Failed to log signal error: #{info[:error]}"
end
end
# Usage with error handling
Signal.trap("USR1", &SignalErrorHandling.safe_handler do |signal|
risky_operation_that_might_fail
puts "Signal #{signal} processed successfully"
end)
Production Patterns
Production Ruby applications commonly use signal trapping for graceful shutdown sequences, configuration reloading, and runtime diagnostics. These patterns require robust signal handling that coordinates with application lifecycle management and external monitoring systems.
Graceful shutdown implementation involves capturing termination signals and performing cleanup operations before process termination. This pattern prevents data loss and ensures proper resource cleanup in production environments.
class GracefulShutdownManager
def initialize
@shutdown_callbacks = []
@shutdown_timeout = 30
@shutdown_initiated = false
@shutdown_mutex = Mutex.new
end
def register_shutdown_callback(name, timeout: 10, &block)
@shutdown_callbacks << {
name: name,
callback: block,
timeout: timeout
}
end
def setup_signal_handlers
["INT", "TERM"].each do |signal|
Signal.trap(signal) do |signo|
initiate_shutdown(signal, signo)
end
end
Signal.trap("USR2") do |signo|
dump_shutdown_status
end
end
private
def initiate_shutdown(signal_name, signal_number)
@shutdown_mutex.synchronize do
return if @shutdown_initiated
@shutdown_initiated = true
end
puts "Graceful shutdown initiated by #{signal_name} (#{signal_number})"
shutdown_start = Time.now
@shutdown_callbacks.each do |callback_info|
callback_start = Time.now
begin
Timeout.timeout(callback_info[:timeout]) do
puts "Executing shutdown callback: #{callback_info[:name]}"
callback_info[:callback].call
end
elapsed = Time.now - callback_start
puts "Shutdown callback #{callback_info[:name]} completed in #{elapsed.round(2)}s"
rescue Timeout::Error
warn "Shutdown callback #{callback_info[:name]} timed out after #{callback_info[:timeout]}s"
rescue => e
warn "Error in shutdown callback #{callback_info[:name]}: #{e.message}"
end
end
total_elapsed = Time.now - shutdown_start
puts "Graceful shutdown completed in #{total_elapsed.round(2)}s"
exit(0)
end
def dump_shutdown_status
puts "Shutdown Status:"
puts " Initiated: #{@shutdown_initiated}"
puts " Callbacks registered: #{@shutdown_callbacks.size}"
puts " Process uptime: #{Time.now - $PROGRAM_START_TIME}s"
end
end
# Production usage example
shutdown_manager = GracefulShutdownManager.new
shutdown_manager.register_shutdown_callback("database_cleanup") do
ActiveRecord::Base.connection_pool.disconnect!
end
shutdown_manager.register_shutdown_callback("worker_shutdown", timeout: 15) do
WorkerPool.shutdown_gracefully
end
shutdown_manager.register_shutdown_callback("metrics_flush") do
MetricsCollector.flush_pending_metrics
end
shutdown_manager.setup_signal_handlers
Configuration reloading using signals enables production applications to update settings without process restart. This pattern requires thread-safe configuration management and atomic updates to prevent inconsistent application state.
class ConfigurationReloader
def initialize(config_paths)
@config_paths = Array(config_paths)
@current_config = load_configuration
@config_mutex = Mutex.new
@reload_callbacks = []
@last_reload = Time.now
end
def setup_reload_signals
Signal.trap("HUP") do |signo|
reload_configuration("HUP", signo)
end
Signal.trap("USR1") do |signo|
validate_configuration_files
end
end
def register_reload_callback(name, &block)
@reload_callbacks << { name: name, callback: block }
end
def current_config
@config_mutex.synchronize { @current_config.dup }
end
private
def reload_configuration(signal_name, signal_number)
puts "Configuration reload triggered by #{signal_name} (#{signal_number})"
reload_start = Time.now
begin
new_config = load_configuration
@config_mutex.synchronize do
old_config = @current_config
@current_config = new_config
@last_reload = reload_start
notify_reload_callbacks(old_config, new_config)
end
elapsed = Time.now - reload_start
puts "Configuration reloaded successfully in #{elapsed.round(3)}s"
rescue => e
warn "Configuration reload failed: #{e.message}"
warn "Continuing with previous configuration"
end
end
def load_configuration
config = {}
@config_paths.each do |path|
if File.exist?(path)
case File.extname(path)
when '.json'
config.merge!(JSON.parse(File.read(path)))
when '.yml', '.yaml'
config.merge!(YAML.load_file(path))
else
warn "Unknown configuration file format: #{path}"
end
else
warn "Configuration file not found: #{path}"
end
end
config.freeze
end
def notify_reload_callbacks(old_config, new_config)
@reload_callbacks.each do |callback_info|
begin
callback_info[:callback].call(old_config, new_config)
rescue => e
warn "Error in reload callback #{callback_info[:name]}: #{e.message}"
end
end
end
def validate_configuration_files
puts "Configuration File Validation:"
@config_paths.each do |path|
if File.exist?(path)
begin
load_configuration_file(path)
puts " ✓ #{path} - Valid"
rescue => e
puts " ✗ #{path} - Error: #{e.message}"
end
else
puts " ✗ #{path} - File not found"
end
end
puts "Last successful reload: #{@last_reload}"
end
end
Runtime diagnostics through signal handlers provide production visibility into application state without external dependencies. This pattern enables debugging and monitoring through signal-triggered reporting.
class RuntimeDiagnostics
def initialize
@diagnostic_handlers = {}
@report_history = []
@max_history = 50
end
def register_diagnostic(signal, name, &block)
@diagnostic_handlers[signal] = {
name: name,
handler: block,
call_count: 0,
last_called: nil
}
Signal.trap(signal) do |signo|
generate_diagnostic_report(signal, signo)
end
end
private
def generate_diagnostic_report(signal, signal_number)
handler_info = @diagnostic_handlers[signal]
return unless handler_info
report_start = Time.now
handler_info[:call_count] += 1
handler_info[:last_called] = report_start
puts "=" * 50
puts "DIAGNOSTIC REPORT: #{handler_info[:name]}"
puts "Signal: #{signal} (#{signal_number})"
puts "Timestamp: #{report_start}"
puts "Report ##{handler_info[:call_count]}"
puts "=" * 50
begin
report_data = handler_info[:handler].call
if report_data.is_a?(Hash)
report_data.each { |key, value| puts "#{key}: #{value}" }
else
puts report_data
end
rescue => e
puts "Error generating report: #{e.message}"
end
elapsed = Time.now - report_start
puts "-" * 50
puts "Report generated in #{elapsed.round(3)}s"
puts
# Store report in history
store_report_history(signal, handler_info[:name], report_start, elapsed)
end
def store_report_history(signal, name, timestamp, duration)
@report_history << {
signal: signal,
name: name,
timestamp: timestamp,
duration: duration
}
@report_history.shift if @report_history.size > @max_history
end
end
# Production diagnostic setup
diagnostics = RuntimeDiagnostics.new
diagnostics.register_diagnostic("USR1", "Memory and Thread Status") do
{
"Ruby Version" => RUBY_VERSION,
"Process PID" => Process.pid,
"Memory Usage" => "#{GC.stat[:heap_live_slots]} live objects",
"GC Stats" => "#{GC.stat[:count]} collections, #{GC.stat[:total_time]}ms total",
"Active Threads" => Thread.list.size,
"Load Average" => File.read("/proc/loadavg").strip rescue "N/A",
"Uptime" => "#{(Time.now - $PROGRAM_START_TIME).round(2)}s"
}
end
diagnostics.register_diagnostic("USR2", "Application State") do
{
"Active Connections" => ConnectionPool.active_count,
"Queue Sizes" => JobQueue.size,
"Cache Hit Rate" => CacheMetrics.hit_rate,
"Request Rate" => RequestCounter.requests_per_second,
"Error Rate" => ErrorTracker.error_rate
}
end
Common Pitfalls
Signal handler execution creates numerous opportunities for subtle bugs and unexpected behavior. These issues often manifest intermittently in production environments, making them particularly challenging to diagnose and resolve.
Platform-specific signal behavior differences cause compatibility issues when Ruby applications run across different operating systems. Signal numbers, available signals, and default behaviors vary between platforms, requiring defensive programming techniques.
class PlatformAwareSignalHandler
COMMON_SIGNALS = {
'INT' => 2, # Interrupt (Ctrl+C)
'TERM' => 15, # Termination request
'HUP' => 1, # Hangup (Unix only)
'QUIT' => 3 # Quit (Unix only)
}.freeze
def self.setup_cross_platform_handlers
# Handle signals that exist on current platform
available_signals = Signal.list
COMMON_SIGNALS.each do |name, expected_number|
if available_signals.key?(name)
actual_number = available_signals[name]
if actual_number != expected_number
warn "Signal #{name} number differs: expected #{expected_number}, got #{actual_number}"
end
Signal.trap(name) do |signo|
handle_termination_signal(name, signo)
end
else
warn "Signal #{name} not available on this platform"
end
end
# Platform-specific signal handling
case RUBY_PLATFORM
when /linux/
setup_linux_specific_signals
when /darwin/
setup_macos_specific_signals
when /mswin|mingw/
setup_windows_specific_signals
end
end
private
def self.setup_linux_specific_signals
Signal.trap("USR1") { handle_user_signal(1) } if Signal.list.key?("USR1")
Signal.trap("USR2") { handle_user_signal(2) } if Signal.list.key?("USR2")
end
def self.setup_macos_specific_signals
Signal.trap("INFO") { handle_info_signal } if Signal.list.key?("INFO")
end
def self.setup_windows_specific_signals
# Windows has limited signal support
warn "Running on Windows: limited signal support available"
end
end
Signal handler reentrancy issues occur when signal handlers are interrupted by additional signals before completing execution. Ruby queues signals during handler execution, but complex handlers can create race conditions and inconsistent state.
class ReentrantSignalProblem
def initialize
@counter = 0
@processing = false
end
def setup_problematic_handler
# PROBLEMATIC: Not reentrant-safe
Signal.trap("USR1") do |signal|
puts "Handler entry, processing: #{@processing}"
@processing = true
# Simulate work that could be interrupted
5.times do |i|
@counter += 1
puts "Counter: #{@counter}, iteration: #{i}"
sleep(0.1) # Dangerous: allows signal interruption
end
@processing = false
puts "Handler exit"
end
end
def setup_safe_handler
# BETTER: Reentrant-safe with proper guards
Signal.trap("USR1") do |signal|
# Quick check without blocking
if @processing
puts "Handler busy, signal #{signal} ignored"
next
end
@processing = true
begin
# Atomic operations only
current_value = @counter
new_value = current_value + 1
@counter = new_value
puts "Signal #{signal}: #{current_value} -> #{new_value}"
ensure
@processing = false
end
end
end
end
Signal delivery timing creates race conditions when handlers modify shared state accessed by other parts of the application. These timing-dependent bugs are particularly difficult to reproduce and debug.
class SignalRaceCondition
def initialize
@shared_resource = []
@access_count = 0
end
def problematic_signal_handling
# PROBLEMATIC: Race condition between signal handler and main thread
Signal.trap("USR1") do
# Signal handler modifies shared state
@shared_resource << Time.now
@access_count += 1
puts "Signal handler: #{@shared_resource.size} items"
end
# Main thread also accesses shared state
Thread.new do
loop do
# Race condition: size might change during iteration
@shared_resource.each_with_index do |item, index|
puts "Processing item #{index}: #{item}"
sleep(0.01) # Simulation of work
end
sleep(1)
end
end
end
def safe_signal_handling
require 'thread'
@mutex = Mutex.new
# BETTER: Synchronized access to shared state
Signal.trap("USR1") do
@mutex.synchronize do
@shared_resource << Time.now
@access_count += 1
puts "Signal handler: #{@shared_resource.size} items"
end
end
Thread.new do
loop do
items_to_process = @mutex.synchronize { @shared_resource.dup }
items_to_process.each_with_index do |item, index|
puts "Processing item #{index}: #{item}"
sleep(0.01)
end
sleep(1)
end
end
end
end
Signal handler complexity creates maintenance and debugging challenges when handlers perform extensive processing or interact with multiple subsystems. Simple, focused handlers reduce the likelihood of bugs and improve reliability.
# PROBLEMATIC: Complex signal handler
class ComplexSignalHandler
def setup_complex_handler
Signal.trap("USR1") do |signal|
begin
# Too much processing in signal handler
update_configuration_from_file
recalculate_worker_pool_sizes
refresh_database_connections
send_metrics_to_monitoring_system
log_detailed_system_state
notify_external_services
puts "Complex processing completed for signal #{signal}"
rescue => e
# Error handling becomes complex
log_error("Signal handler error", e)
attempt_error_recovery
send_error_notification(e)
end
end
end
end
# BETTER: Simple signal handler with delegation
class SimpleSignalHandler
def initialize
@signal_queue = Queue.new
setup_signal_processor
end
def setup_simple_handler
# Signal handler only queues work
Signal.trap("USR1") do |signal|
@signal_queue.push([:usr1_received, signal, Time.now])
end
end
private
def setup_signal_processor
Thread.new do
loop do
begin
work_item = @signal_queue.pop
process_signal_work(work_item)
rescue => e
warn "Error processing signal work: #{e.message}"
end
end
end
end
def process_signal_work(work_item)
action, signal, timestamp = work_item
case action
when :usr1_received
# Complex processing happens in dedicated thread
ConfigurationManager.reload
WorkerPool.adjust_sizes
DatabasePool.refresh_connections
MetricsCollector.send_updates
SystemLogger.log_state
NotificationService.send_updates
end
end
end
Reference
Signal Module Methods
Method | Parameters | Returns | Description |
---|---|---|---|
Signal.trap(signal, command) |
signal (String/Integer), command (String/Proc/nil) |
String or Proc |
Register signal handler, returns previous handler |
Signal.list |
None | Hash |
Returns hash mapping signal names to numbers |
Signal.signame(signo) |
signo (Integer) |
String or nil |
Returns signal name for given number |
Common POSIX Signals
Signal | Number | Default Action | Description |
---|---|---|---|
SIGHUP |
1 | Terminate | Hangup detected on controlling terminal |
SIGINT |
2 | Terminate | Interrupt from keyboard (Ctrl+C) |
SIGQUIT |
3 | Core dump | Quit from keyboard (Ctrl+) |
SIGKILL |
9 | Terminate | Kill signal (cannot be trapped) |
SIGTERM |
15 | Terminate | Termination signal |
SIGUSR1 |
10 | Terminate | User-defined signal 1 |
SIGUSR2 |
12 | Terminate | User-defined signal 2 |
SIGCHLD |
17 | Ignore | Child process terminated or stopped |
SIGPIPE |
13 | Terminate | Write to broken pipe |
SIGALRM |
14 | Terminate | Timer signal from alarm() |
Predefined Handler Strings
Handler String | Behavior |
---|---|
"DEFAULT" |
Restore system default signal handling |
"IGNORE" |
Ignore the signal completely |
"EXIT" |
Terminate process immediately |
"SYSTEM_DEFAULT" |
Restore original system handler |
Platform Signal Availability
Platform | Available Signals | Notes |
---|---|---|
Linux | Full POSIX set | Includes SIGRTMIN+n real-time signals |
macOS | Full POSIX set | Includes BSD-specific signals like SIGINFO |
Windows | Limited set | Only SIGINT, SIGTERM, SIGBREAK, SIGABRT |
FreeBSD | Full POSIX set | Similar to Linux with BSD extensions |
Solaris | Full POSIX set | Includes Solaris-specific extensions |
Signal Handler Execution Context
Aspect | Behavior |
---|---|
Thread Context | Handlers always execute in main thread |
Signal Queuing | Signals queued during handler execution |
Exception Propagation | Uncaught exceptions can terminate process |
Interrupt Timing | Handlers can interrupt any Ruby operation |
Memory Allocation | Safe to allocate objects in handlers |
File Operations | Generally safe but avoid blocking operations |
Error Conditions
Exception | Cause | Resolution |
---|---|---|
ArgumentError |
Invalid signal name/number | Use Signal.list to verify available signals |
Errno::EINVAL |
Invalid signal operation | Check platform signal support |
SecurityError |
Insufficient privileges | Run with appropriate permissions |
SystemCallError |
System-level signal failure | Check system resources and limits |
Best Practices Summary
Practice | Rationale |
---|---|
Keep handlers simple | Reduces complexity and potential for errors |
Use signal queuing for complex operations | Prevents blocking signal delivery |
Synchronize shared state access | Prevents race conditions in multithreaded apps |
Test signal behavior on target platforms | Ensures compatibility across deployments |
Handle exceptions in signal handlers | Prevents process termination from handler errors |
Avoid blocking operations in handlers | Maintains signal responsiveness |
Use atomic operations when possible | Reduces need for explicit synchronization |