Overview
Ruby exposes system calls through the Process
module, Kernel
module methods, and various built-in functions that interact directly with the operating system. System calls bridge Ruby programs with the underlying OS, enabling process creation, signal handling, resource management, and system information access.
The Process
module serves as the primary interface for system-level operations. Core functionality includes process spawning with Process.spawn
, process forking with Process.fork
, and process waiting with Process.wait
. Signal handling occurs through Process.kill
and signal traps. Process information retrieval uses methods like Process.pid
, Process.ppid
, and Process.getpgrp
.
# Basic process information
current_pid = Process.pid
parent_pid = Process.ppid
process_group = Process.getpgrp
puts "Current PID: #{current_pid}"
puts "Parent PID: #{parent_pid}"
puts "Process Group: #{process_group}"
The Kernel
module provides system call wrappers like system
, exec
, and backtick operators for command execution. These methods offer different approaches to running external commands and capturing output.
# Different ways to execute system commands
system("ls -la") # Returns true/false, output to stdout
result = `ls -la` # Returns command output as string
exec("ls -la") # Replaces current process
Ruby system calls handle platform differences transparently where possible, but many behaviors remain platform-specific. Windows, Linux, macOS, and other Unix systems implement different subsets of POSIX system calls, affecting method availability and behavior.
Basic Usage
Process creation forms the foundation of Ruby system call usage. Process.spawn
creates new processes without replacing the current process, returning the child process ID immediately.
# Spawn a process and get its PID
child_pid = Process.spawn("sleep", "5")
puts "Spawned process #{child_pid}"
# Wait for the process to complete
Process.wait(child_pid)
puts "Process #{child_pid} completed"
Process.fork
creates child processes on Unix systems by duplicating the current process. The parent process receives the child PID, while the child process receives nil
.
# Fork a process
pid = Process.fork do
puts "This runs in the child process"
puts "Child PID: #{Process.pid}"
sleep 2
exit 0
end
if pid
puts "This runs in the parent process"
puts "Child PID: #{pid}"
Process.wait(pid)
puts "Child process completed"
end
Signal handling enables inter-process communication and process control. Process.kill
sends signals to processes, while Signal.trap
registers signal handlers.
# Set up signal handling
Signal.trap("INT") do
puts "Received interrupt signal"
exit 0
end
Signal.trap("TERM") do
puts "Received termination signal"
exit 0
end
puts "Process #{Process.pid} running..."
sleep 30 # Process will handle signals during sleep
Process waiting operations synchronize parent and child processes. Process.wait
blocks until any child process exits, while Process.wait2
returns both PID and exit status.
# Wait for specific process
child_pid = Process.spawn("ruby", "-e", "sleep 3; exit 42")
status = Process.wait2(child_pid)
puts "Process #{status[0]} exited with status #{status[1].exitstatus}"
# Wait for any child process
3.times do |i|
Process.spawn("ruby", "-e", "sleep #{i+1}")
end
# Wait for all children
3.times do
pid, status = Process.wait2
puts "Process #{pid} completed with status #{status.exitstatus}"
end
Error Handling & Debugging
System call errors typically raise specific exception classes that correspond to system error codes. Errno
module constants represent different system errors, enabling precise error handling.
begin
# Attempt to kill a non-existent process
Process.kill("TERM", 99999)
rescue Errno::ESRCH => e
puts "Process not found: #{e.message}"
rescue Errno::EPERM => e
puts "Permission denied: #{e.message}"
rescue SystemCallError => e
puts "System call error: #{e.message}"
end
Process spawning failures occur when executables don't exist, permissions are insufficient, or system resources are exhausted. Different spawn methods handle failures differently.
# system returns false on failure
if system("nonexistent_command")
puts "Command succeeded"
else
puts "Command failed with exit status: #{$?.exitstatus}"
end
# spawn raises exceptions on failure
begin
pid = Process.spawn("nonexistent_command")
rescue Errno::ENOENT => e
puts "Command not found: #{e.message}"
rescue Errno::EACCES => e
puts "Permission denied: #{e.message}"
end
Signal handling debugging requires understanding signal delivery timing and handler execution context. Signals interrupt normal program flow, potentially causing race conditions or incomplete operations.
# Debug signal handling with logging
interrupted = false
Signal.trap("INT") do
puts "Signal received at #{Time.now}"
interrupted = true
end
# Long-running operation with interruption check
1000.times do |i|
if interrupted
puts "Operation interrupted at iteration #{i}"
break
end
sleep 0.1
# Check for interruption periodically
if i % 100 == 0
puts "Completed #{i} iterations"
end
end
Process status debugging examines exit codes and termination reasons. Process::Status
objects provide detailed information about process termination.
# Comprehensive process status checking
def debug_process_status(pid)
_, status = Process.wait2(pid)
puts "Process #{pid} status:"
puts " Exited: #{status.exited?}"
puts " Exit status: #{status.exitstatus}" if status.exited?
puts " Signaled: #{status.signaled?}"
puts " Signal: #{status.termsig}" if status.signaled?
puts " Stopped: #{status.stopped?}"
puts " Stop signal: #{status.stopsig}" if status.stopped?
puts " Core dump: #{status.coredump?}" if status.signaled?
end
# Test with different process outcomes
pid1 = Process.spawn("ruby", "-e", "exit 0") # Normal exit
pid2 = Process.spawn("ruby", "-e", "exit 1") # Error exit
pid3 = Process.spawn("ruby", "-e", "raise 'error'") # Exception
[pid1, pid2, pid3].each { |pid| debug_process_status(pid) }
Production Patterns
Production applications require robust process management patterns that handle failures gracefully and provide monitoring capabilities. Process pools manage multiple worker processes efficiently.
class ProcessPool
def initialize(worker_count, worker_script)
@worker_count = worker_count
@worker_script = worker_script
@workers = {}
@shutdown = false
setup_signal_handlers
start_workers
end
def start_workers
@worker_count.times do |i|
spawn_worker(i)
end
end
def spawn_worker(worker_id)
pid = Process.spawn("ruby", @worker_script, worker_id.to_s)
@workers[pid] = { id: worker_id, started_at: Time.now }
puts "Started worker #{worker_id} (PID: #{pid})"
pid
end
def monitor_workers
until @shutdown
begin
pid, status = Process.wait2(-1, Process::WNOHANG)
if pid
worker_info = @workers.delete(pid)
puts "Worker #{worker_info[:id]} (PID: #{pid}) exited: #{status.exitstatus}"
# Restart worker unless shutting down
unless @shutdown
spawn_worker(worker_info[:id])
end
end
sleep 1
rescue Errno::ECHILD
# No child processes
break if @shutdown
sleep 1
end
end
end
def setup_signal_handlers
Signal.trap("TERM") { shutdown }
Signal.trap("INT") { shutdown }
end
def shutdown
@shutdown = true
puts "Shutting down process pool..."
@workers.keys.each do |pid|
Process.kill("TERM", pid)
end
# Wait for all workers to exit
@workers.keys.each do |pid|
begin
Process.wait(pid)
rescue Errno::ECHILD
# Process already exited
end
end
end
end
Graceful shutdown handling ensures processes complete current work before terminating. This pattern prevents data loss and maintains system consistency.
class GracefulWorker
def initialize
@running = true
@current_job = nil
Signal.trap("TERM") do
puts "Received TERM signal, shutting down gracefully..."
@running = false
end
Signal.trap("INT") do
puts "Received INT signal, shutting down gracefully..."
@running = false
end
end
def run
while @running
@current_job = fetch_job
if @current_job
process_job(@current_job)
complete_job(@current_job)
else
sleep 1 # Wait for new jobs
end
end
# Handle any remaining job
if @current_job
puts "Completing current job before shutdown..."
complete_job(@current_job)
end
puts "Worker shutting down cleanly"
end
private
def fetch_job
# Simulate job fetching
rand < 0.3 ? { id: rand(1000), data: "job_data" } : nil
end
def process_job(job)
puts "Processing job #{job[:id]}"
sleep rand(3) + 1 # Simulate work
end
def complete_job(job)
puts "Completed job #{job[:id]}"
@current_job = nil
end
end
Health monitoring tracks process status and resource usage for operational visibility. This enables proactive issue detection and capacity planning.
class ProcessMonitor
def initialize(pids)
@pids = pids
@stats = {}
end
def collect_stats
@pids.each do |pid|
begin
stat_file = "/proc/#{pid}/stat"
if File.exist?(stat_file)
stat_data = File.read(stat_file).split
@stats[pid] = {
cpu_time: stat_data[13].to_i + stat_data[14].to_i,
memory_pages: stat_data[23].to_i,
start_time: stat_data[21].to_i,
state: stat_data[2],
timestamp: Time.now
}
end
rescue => e
puts "Error collecting stats for PID #{pid}: #{e.message}"
end
end
end
def report_stats
@stats.each do |pid, stats|
memory_mb = (stats[:memory_pages] * 4096) / 1024 / 1024
puts "PID #{pid}: CPU time=#{stats[:cpu_time]}, Memory=#{memory_mb}MB, State=#{stats[:state]}"
end
end
def monitor_continuously
while true
collect_stats
report_stats
puts "---"
sleep 10
end
end
end
Common Pitfalls
Platform-specific behavior creates portability issues across different operating systems. Windows systems don't support Process.fork
, requiring alternative approaches for process creation.
# Platform-specific process creation
def create_worker_process(script)
if RUBY_PLATFORM =~ /win32|mingw/
# Windows - use spawn instead of fork
Process.spawn("ruby", script)
else
# Unix-like systems - fork is available
Process.fork do
exec("ruby", script)
end
end
end
Zombie process accumulation occurs when parent processes don't wait for child processes to complete. Zombie processes consume system resources and can exhaust process table entries.
# Bad: Creates zombie processes
def spawn_workers_badly
10.times do |i|
Process.spawn("ruby", "-e", "sleep 1; puts 'Worker #{i} done'")
end
# No Process.wait - creates zombies!
end
# Good: Proper cleanup prevents zombies
def spawn_workers_properly
pids = []
10.times do |i|
pid = Process.spawn("ruby", "-e", "sleep 1; puts 'Worker #{i} done'")
pids << pid
end
# Wait for all children
pids.each { |pid| Process.wait(pid) }
end
Signal handling race conditions occur when signals interrupt critical sections or when multiple signals arrive simultaneously. Signal handlers execute in unpredictable contexts, potentially corrupting shared state.
# Problematic: Signal handler modifies shared state
@counter = 0
Signal.trap("USR1") do
@counter += 1 # Race condition!
puts "Counter: #{@counter}"
end
# Better: Use atomic operations or synchronization
require 'concurrent'
@counter = Concurrent::AtomicFixnum.new(0)
Signal.trap("USR1") do
value = @counter.increment
puts "Counter: #{value}"
end
Process group and session confusion leads to unexpected signal delivery and job control behavior. Child processes inherit process groups and sessions, affecting how signals propagate.
# Understanding process groups
def demonstrate_process_groups
puts "Parent process group: #{Process.getpgrp}"
# Child inherits process group
pid1 = Process.fork do
puts "Child 1 process group: #{Process.getpgrp}"
sleep 10
end
# Create new process group
pid2 = Process.fork do
Process.setpgrp # Create new process group
puts "Child 2 process group: #{Process.getpgrp}"
sleep 10
end
sleep 2
# Signal sent to process group affects both parent and child1
puts "Sending TERM to process group #{Process.getpgrp}"
Process.kill("-TERM", Process.getpgrp)
# child2 won't receive the signal due to different process group
Process.wait(pid2)
rescue Errno::ECHILD
puts "No more child processes"
end
File descriptor inheritance creates resource leaks when spawned processes inherit open file descriptors from the parent process. This can prevent file deletion and consume system resources.
# Problematic: File descriptors inherited by child
file = File.open("large_file.txt", "w")
pid = Process.spawn("sleep", "60") # Inherits file descriptor
File.unlink("large_file.txt") # Fails - file still open in child
Process.wait(pid)
file.close
# Better: Close file descriptors in child or use close-on-exec
file = File.open("large_file.txt", "w")
file.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC) # Close on exec
pid = Process.spawn("sleep", "60") # File closed in child
File.unlink("large_file.txt") # Succeeds
Process.wait(pid)
file.close
Reference
Process Module Methods
Method | Parameters | Returns | Description |
---|---|---|---|
Process.spawn(*args, **opts) |
command, arguments, options hash | Integer |
Creates new process, returns PID |
Process.fork { block } |
optional block | Integer or nil |
Forks process, returns PID in parent, nil in child |
Process.exec(*args) |
command and arguments | Does not return | Replaces current process with new command |
Process.wait(pid=-1, flags=0) |
process ID, wait flags | Integer |
Waits for process, returns PID |
Process.wait2(pid=-1, flags=0) |
process ID, wait flags | Array |
Waits for process, returns [PID, status] |
Process.waitpid(pid, flags=0) |
process ID, wait flags | Integer |
Waits for specific process |
Process.waitall |
none | Array |
Waits for all child processes |
Process.detach(pid) |
process ID | Thread |
Creates thread to wait for process |
Process.kill(signal, *pids) |
signal, process IDs | Integer |
Sends signal to processes |
Process Information Methods
Method | Parameters | Returns | Description |
---|---|---|---|
Process.pid |
none | Integer |
Current process ID |
Process.ppid |
none | Integer |
Parent process ID |
Process.getpgrp |
none | Integer |
Process group ID |
Process.setpgrp |
none | Integer |
Sets process group ID |
Process.getpriority(which, who) |
priority type, ID | Integer |
Gets process priority |
Process.setpriority(which, who, prio) |
priority type, ID, priority | 0 |
Sets process priority |
Process.uid |
none | Integer |
Real user ID |
Process.euid |
none | Integer |
Effective user ID |
Process.gid |
none | Integer |
Real group ID |
Process.egid |
none | Integer |
Effective group ID |
Signal Constants
Signal | Value | Description |
---|---|---|
Signal::INT |
2 | Interrupt (Ctrl+C) |
Signal::QUIT |
3 | Quit (Ctrl+\) |
Signal::KILL |
9 | Unconditional termination |
Signal::TERM |
15 | Termination request |
Signal::CONT |
18 | Continue stopped process |
Signal::STOP |
19 | Stop process |
Signal::CHLD |
17 | Child process terminated |
Signal::USR1 |
10 | User-defined signal 1 |
Signal::USR2 |
12 | User-defined signal 2 |
Process Status Methods
Method | Parameters | Returns | Description |
---|---|---|---|
status.exited? |
none | Boolean |
True if process exited normally |
status.exitstatus |
none | Integer or nil |
Exit code if exited normally |
status.signaled? |
none | Boolean |
True if process terminated by signal |
status.termsig |
none | Integer or nil |
Signal that terminated process |
status.stopped? |
none | Boolean |
True if process is stopped |
status.stopsig |
none | Integer or nil |
Signal that stopped process |
status.coredump? |
none | Boolean |
True if process created core dump |
status.success? |
none | Boolean |
True if process succeeded |
Spawn Options
Option | Type | Description |
---|---|---|
:chdir |
String | Change working directory |
:umask |
Integer | Set file creation mask |
:in |
IO, String, Symbol | Redirect stdin |
:out |
IO, String, Symbol | Redirect stdout |
:err |
IO, String, Symbol | Redirect stderr |
:close_others |
Boolean | Close non-redirected file descriptors |
:unsetenv_others |
Boolean | Clear environment variables |
:pgroup |
Boolean, Integer | Process group settings |
:rlimit_* |
Array | Resource limits |
Wait Flags
Flag | Description |
---|---|
Process::WNOHANG |
Don't block if no child available |
Process::WUNTRACED |
Return stopped children |
Process::WCONTINUED |
Return continued children |
Common Error Classes
Exception | Condition |
---|---|
Errno::ECHILD |
No child processes to wait for |
Errno::ESRCH |
Process does not exist |
Errno::EPERM |
Operation not permitted |
Errno::ENOENT |
Executable not found |
Errno::EACCES |
Permission denied |
SystemCallError |
Generic system call error |