CrackedRuby logo

CrackedRuby

System Calls

Ruby system calls provide direct access to operating system functionality for process management, signal handling, and system information retrieval.

Core Built-in Classes Process and System
2.10.4

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