CrackedRuby CrackedRuby

Process States and Transitions

Overview

Process states represent the distinct conditions a process occupies during its lifetime in an operating system. Each process moves through various states as the OS scheduler allocates CPU time, handles I/O operations, and manages system resources. Understanding these states provides insight into how operating systems manage concurrent execution and resource allocation.

The concept originates from early multiprogramming systems where efficient CPU utilization required managing multiple processes simultaneously. Modern operating systems employ state models to track each process's current condition and determine scheduling decisions. The scheduler examines process states to select which process receives CPU time, ensuring fair resource distribution and system responsiveness.

A process begins in a creation state when the OS allocates initial resources. It transitions to a ready state after initialization, waiting in a queue for CPU assignment. Once scheduled, the process enters a running state where it executes instructions. The process may transition to waiting states for I/O completion or other events, then return to ready when the event occurs. Finally, the process reaches a terminated state when execution completes or an error occurs.

# Basic process creation and state observation
pid = fork do
  puts "Child process executing"
  sleep 2
end

# Parent continues immediately - child is now in ready/running state
puts "Parent process: child PID is #{pid}"

# Wait for child to complete - monitors state transition to terminated
Process.wait(pid)
puts "Child process completed"

The state model varies across operating systems, but most implement variations of five fundamental states: new, ready, running, waiting, and terminated. Some systems include additional states for suspended processes, stopped processes, or zombie processes awaiting cleanup. The transitions between states follow strict rules enforced by the kernel, preventing invalid state changes that could compromise system stability.

Process state management affects system performance, resource utilization, and application responsiveness. Applications that spawn multiple processes must understand state transitions to avoid resource leaks, zombie processes, or deadlocks. System administrators monitor process states to diagnose performance issues, identify misbehaving applications, and optimize resource allocation.

Key Principles

Process states form a finite state machine where each state represents a distinct condition with defined transition rules. The OS kernel maintains state information in process control blocks (PCBs), data structures containing the process's current state, program counter, register values, memory allocation, and scheduling information.

The Five Primary States

The new state occurs during process creation when the OS allocates a PCB and initial resources but hasn't yet admitted the process for execution. The process remains in this state briefly while the kernel initializes data structures and prepares for scheduling. Not all operating systems explicitly model this state; some combine creation with the ready state.

The ready state contains processes prepared to execute but waiting for CPU assignment. The OS maintains ready processes in a queue, ordered by scheduling algorithm priority. Multiple processes typically occupy the ready state simultaneously, competing for available CPU cores. The scheduler selects processes from this queue based on priority, time quantum allocation, or other criteria.

The running state indicates active execution on a CPU core. A process in this state executes instructions, performs computations, and modifies memory. Only one process per CPU core can occupy the running state at any moment. Modern multicore systems run multiple processes simultaneously, with each core executing one process.

The waiting state (sometimes called blocked state) contains processes that cannot continue execution until some event completes. Common reasons include waiting for I/O operations, network responses, synchronization signals, or timer expirations. Waiting processes don't consume CPU time, allowing the scheduler to run other processes. The OS maintains separate queues for different wait conditions.

The terminated state occurs when process execution completes, either normally or abnormally through errors or signals. The OS deallocates most resources immediately but retains the PCB briefly to communicate the exit status to the parent process. Without proper cleanup, terminated processes become zombies, consuming system resources unnecessarily.

State Transition Triggers

Transitions from new to ready occur when the OS admits the process for scheduling, allocating necessary resources and adding the process to the ready queue. This admission policy prevents system overload by limiting concurrent processes.

Ready to running transitions happen when the scheduler selects a process for execution. Selection criteria depend on the scheduling algorithm: round-robin, priority-based, multilevel feedback queues, or real-time scheduling. The dispatcher performs context switching, saving the current process state and loading the selected process state.

Running to ready transitions occur during preemption when a higher-priority process becomes ready or when a time quantum expires in time-sharing systems. The scheduler moves the running process back to the ready queue and selects another process. Preemption enables responsive multitasking and prevents CPU monopolization.

Running to waiting transitions happen when a process requests operations that cannot complete immediately. System calls for file I/O, network operations, or synchronization primitives trigger this transition. The kernel moves the process to an appropriate wait queue and immediately schedules another ready process.

Waiting to ready transitions occur when the awaited event completes. Device drivers signal I/O completion, network stacks indicate data arrival, or timers expire. The kernel moves the process from the wait queue to the ready queue, making it eligible for scheduling again.

Running to terminated transitions result from normal process completion, explicit exit system calls, or abnormal termination through signals or exceptions. The kernel performs cleanup operations, releases resources, and retains the exit status for the parent process.

# Demonstrating state transitions through process control
child_pid = fork do
  # Child enters running state immediately after fork
  puts "Child running (PID: #{Process.pid})"
  
  # Transition to waiting state during I/O
  sleep 1  # Waiting for timer
  
  # Returns to ready/running when timer expires
  puts "Child resumed after wait"
  
  # Will transition to terminated on exit
end

# Parent in running state
puts "Parent running (PID: #{Process.pid})"

# Parent transitions to waiting during Process.wait
status = Process.wait(child_pid)

# Parent returns to running after child terminates
puts "Child terminated with status: #{status}"

Scheduling Context

The scheduler examines process states continuously, making decisions based on current system conditions. Context switching overhead—time spent saving and restoring process state—impacts system performance. Frequent context switches reduce overall throughput, while infrequent switches harm responsiveness. The scheduler balances these concerns based on system workload and policy.

Processes in the waiting state don't participate in scheduling decisions, reducing scheduler workload. However, moving processes between states requires kernel intervention, creating synchronization challenges in multicore systems where multiple CPUs access shared scheduler data structures.

Ruby Implementation

Ruby provides the Process module for interacting with operating system processes, enabling process creation, state monitoring, and lifecycle management. While Ruby doesn't expose process states directly, developers can observe state transitions through process control operations and system calls.

Process Creation and Forking

The Process.fork method creates a child process, duplicating the parent's memory space and file descriptors. The child process begins in a ready or running state immediately after creation. Fork returns the child's PID to the parent and nil to the child, allowing different code paths.

# Process forking demonstrates state transitions
pid = fork do
  # Child process executes this block
  # Starts in running state
  puts "Child PID: #{Process.pid}"
  puts "Parent PID: #{Process.ppid}"
  
  # Perform work
  result = (1..1000000).sum
  puts "Child computed result: #{result}"
end

# Parent continues immediately
# Both processes in ready/running states
puts "Parent created child #{pid}"

# Parent transitions to waiting during Process.wait
# Waits for child to reach terminated state
Process.wait(pid)
puts "Child process completed"

Process State Monitoring

Ruby cannot directly query process states from within a process, but external tools and system calls provide state information. The Process.waitpid method with the Process::WNOHANG flag checks for state changes without blocking, allowing non-blocking state monitoring.

# Non-blocking state checking
pid = fork do
  sleep 3
  puts "Child completing"
end

# Check if child has terminated without blocking
loop do
  result = Process.waitpid(pid, Process::WNOHANG)
  
  if result.nil?
    puts "Child still running (in ready/running/waiting state)"
    sleep 0.5
  else
    puts "Child terminated (detected state transition)"
    break
  end
end

Thread States in Ruby

Ruby threads represent lightweight concurrency within a process. Thread states mirror process states conceptually but operate at a different level. Ruby's Thread class exposes state information through the status method, returning strings indicating thread conditions.

# Thread states parallel process states
thread = Thread.new do
  sleep 2  # Thread enters sleep (waiting) state
  puts "Thread executing"
end

# Check thread state
puts "Thread status: #{thread.status}"  # "sleep"

# Wait for thread completion
thread.join

puts "Thread status: #{thread.status}"  # false (terminated)

Thread statuses include:

  • "run": actively executing (running state)
  • "sleep": waiting for condition (waiting state)
  • false: terminated normally (terminated state)
  • nil: terminated with exception (terminated state)
  • "aborting": terminating abnormally (transitioning to terminated)

Process Group and Session Management

Ruby manages process groups and sessions through Process methods. Process groups represent collections of related processes, typically a parent and its children. Sessions contain process groups, representing user login contexts or job control boundaries.

# Process group management
pid = fork do
  # Create new process group with child as leader
  Process.setpgid(0, 0)
  puts "Child process group: #{Process.getpgrp}"
  
  # Spawn grandchild in same group
  grandchild = fork do
    puts "Grandchild process group: #{Process.getpgrp}"
    sleep 10
  end
  
  Process.wait(grandchild)
end

puts "Parent process group: #{Process.getpgrp}"
Process.wait(pid)

Signal Handling and State Changes

Signals trigger process state transitions, interrupting execution or causing termination. Ruby's Signal module allows signal handling, enabling custom behavior during state transitions.

# Signal handling affects process states
interrupted = false

# Register signal handler
Signal.trap("INT") do
  interrupted = true
  puts "Received interrupt signal"
end

pid = fork do
  puts "Child waiting for signal"
  sleep 100  # Child in waiting state
end

# Let child enter waiting state
sleep 1

# Send signal causes state transition
Process.kill("INT", pid)

# Wait for child to handle signal and terminate
Process.wait(pid)
puts "Child terminated after signal"

Daemon Process Implementation

Daemon processes run in the background without controlling terminals. Creating daemons requires careful state management to detach from the parent process and session.

# Daemon process implementation
def daemonize
  # Fork and exit parent (child continues)
  exit if fork
  
  # Create new session (process becomes session leader)
  Process.setsid
  
  # Fork again and exit (grandchild continues)
  # Grandchild cannot acquire controlling terminal
  exit if fork
  
  # Change working directory
  Dir.chdir "/"
  
  # Close standard file descriptors
  STDIN.reopen "/dev/null"
  STDOUT.reopen "/dev/null", "w"
  STDERR.reopen "/dev/null", "w"
  
  # Daemon is now detached and running
  loop do
    # Perform daemon work
    sleep 60
  end
end

# Start daemon process
pid = fork { daemonize }
Process.detach(pid)  # Parent won't wait for daemon

Implementation Approaches

Process state management requires careful architectural decisions about state tracking, transition control, and resource cleanup. Different approaches suit different application requirements and system constraints.

Explicit State Machine Pattern

Implementing an explicit state machine provides clear visibility into process lifecycle stages. This approach defines states as discrete values and transitions as functions that verify preconditions before state changes.

class ProcessState
  STATES = [:new, :ready, :running, :waiting, :terminated].freeze
  VALID_TRANSITIONS = {
    new: [:ready],
    ready: [:running, :terminated],
    running: [:ready, :waiting, :terminated],
    waiting: [:ready, :terminated],
    terminated: []
  }.freeze
  
  attr_reader :current_state, :pid
  
  def initialize(pid)
    @pid = pid
    @current_state = :new
    @state_history = [:new]
    @mutex = Mutex.new
  end
  
  def transition_to(new_state)
    @mutex.synchronize do
      unless VALID_TRANSITIONS[@current_state].include?(new_state)
        raise "Invalid transition from #{@current_state} to #{new_state}"
      end
      
      @current_state = new_state
      @state_history << new_state
      execute_state_entry_actions(new_state)
    end
  end
  
  def execute_state_entry_actions(state)
    case state
    when :terminated
      cleanup_resources
    when :waiting
      register_wait_condition
    end
  end
  
  private
  
  def cleanup_resources
    # Release resources on termination
  end
  
  def register_wait_condition
    # Set up wait queue tracking
  end
end

This approach enforces valid transitions at compile time, preventing impossible state sequences. The state history tracking aids debugging and performance analysis. Thread-safe state transitions prevent race conditions in concurrent environments.

Observer Pattern for State Changes

The observer pattern decouples state change detection from state change handling. Components register interest in specific transitions and receive notifications when those transitions occur.

class ProcessStateObserver
  def initialize
    @observers = Hash.new { |h, k| h[k] = [] }
    @states = {}
  end
  
  def register_observer(pid, transition, &block)
    @observers[pid] << { transition: transition, callback: block }
  end
  
  def update_state(pid, new_state)
    old_state = @states[pid]
    @states[pid] = new_state
    
    transition = [old_state, new_state]
    @observers[pid].each do |observer|
      if observer[:transition] == transition
        observer[:callback].call(pid, old_state, new_state)
      end
    end
  end
end

# Usage
observer = ProcessStateObserver.new

observer.register_observer(process_pid, [:running, :waiting]) do |pid, old, new|
  puts "Process #{pid} blocked: #{old} -> #{new}"
end

observer.register_observer(process_pid, [:waiting, :ready]) do |pid, old, new|
  puts "Process #{pid} unblocked: #{old} -> #{new}"
end

Process Pool Management

Managing multiple processes requires tracking states across the entire pool. Process pools maintain collections of worker processes, monitoring their states and replacing terminated workers.

class ProcessPool
  def initialize(size)
    @size = size
    @workers = {}
    @mutex = Mutex.new
    spawn_workers
  end
  
  def spawn_workers
    @size.times do |i|
      spawn_worker(i)
    end
  end
  
  def spawn_worker(id)
    pid = fork do
      Signal.trap("TERM") { exit }
      worker_loop(id)
    end
    
    @mutex.synchronize do
      @workers[pid] = {
        id: id,
        state: :running,
        started_at: Time.now
      }
    end
  end
  
  def monitor_workers
    loop do
      check_worker_states
      replace_terminated_workers
      sleep 1
    end
  end
  
  def check_worker_states
    @workers.keys.each do |pid|
      begin
        status = Process.waitpid(pid, Process::WNOHANG)
        if status
          @mutex.synchronize do
            @workers[pid][:state] = :terminated
            @workers[pid][:ended_at] = Time.now
          end
        end
      rescue Errno::ECHILD
        # Process already reaped
        @mutex.synchronize { @workers.delete(pid) }
      end
    end
  end
  
  def replace_terminated_workers
    @mutex.synchronize do
      @workers.select { |_, info| info[:state] == :terminated }.each do |pid, info|
        @workers.delete(pid)
        spawn_worker(info[:id])
      end
    end
  end
  
  private
  
  def worker_loop(id)
    loop do
      perform_work(id)
      sleep rand(1..5)
    end
  end
  
  def perform_work(id)
    puts "Worker #{id} (PID: #{Process.pid}) processing"
  end
end

Practical Examples

Monitoring Child Process States

Applications that spawn child processes must monitor state transitions to handle completion, errors, and resource cleanup properly.

class ChildProcessManager
  def initialize
    @children = {}
  end
  
  def spawn_child(command)
    pid = fork do
      exec(command)
    end
    
    @children[pid] = {
      command: command,
      started_at: Time.now,
      state: :running
    }
    
    pid
  end
  
  def monitor_all
    @children.keys.each do |pid|
      monitor_child(pid)
    end
  end
  
  def monitor_child(pid)
    return unless @children[pid]
    
    begin
      status = Process.waitpid(pid, Process::WNOHANG)
      
      if status
        exit_status = $?.exitstatus
        @children[pid][:state] = :terminated
        @children[pid][:exit_status] = exit_status
        @children[pid][:ended_at] = Time.now
        
        handle_termination(pid, exit_status)
      end
    rescue Errno::ECHILD
      # Child already reaped by another call
      @children.delete(pid)
    end
  end
  
  def handle_termination(pid, exit_status)
    info = @children[pid]
    duration = info[:ended_at] - info[:started_at]
    
    if exit_status == 0
      puts "Child #{pid} completed successfully after #{duration}s"
    else
      puts "Child #{pid} failed with status #{exit_status}"
      restart_child(info[:command]) if info[:should_restart]
    end
  end
  
  def wait_all
    @children.keys.each do |pid|
      begin
        Process.wait(pid)
        @children[pid][:state] = :terminated
      rescue Errno::ECHILD
        # Already terminated
      end
    end
  end
  
  def restart_child(command)
    puts "Restarting failed child: #{command}"
    spawn_child(command)
  end
end

# Usage
manager = ChildProcessManager.new

# Spawn multiple child processes
manager.spawn_child("sleep 5 && echo 'Task 1'")
manager.spawn_child("sleep 3 && echo 'Task 2'")
manager.spawn_child("false")  # Will fail

# Monitor state changes
10.times do
  manager.monitor_all
  sleep 1
end

manager.wait_all

Handling Zombie Processes

Zombie processes occur when a child terminates but the parent hasn't read its exit status. Proper state management prevents zombie accumulation.

class ZombiePreventionExample
  def initialize
    @running = true
    setup_signal_handlers
  end
  
  def setup_signal_handlers
    # Handle SIGCHLD to reap terminated children
    Signal.trap("CHLD") do
      reap_children
    end
    
    Signal.trap("INT") do
      @running = false
    end
  end
  
  def reap_children
    # Reap all terminated children without blocking
    loop do
      pid = Process.waitpid(-1, Process::WNOHANG)
      break unless pid
      
      puts "Reaped child #{pid} (preventing zombie state)"
    end
  rescue Errno::ECHILD
    # No children to reap
  end
  
  def spawn_workers(count)
    count.times do |i|
      fork do
        puts "Worker #{i} (PID: #{Process.pid}) starting"
        sleep rand(1..5)
        puts "Worker #{i} (PID: #{Process.pid}) completing"
      end
    end
  end
  
  def run
    spawn_workers(5)
    
    # Main process continues
    while @running
      puts "Main process active (#{Time.now})"
      sleep 2
    end
    
    # Clean up any remaining children
    Process.waitall
  end
end

# Run example
example = ZombiePreventionExample.new
example.run

Graceful Process Shutdown

Implementing graceful shutdown requires coordinating state transitions across multiple processes, handling signals, and ensuring resource cleanup.

class GracefulShutdownManager
  def initialize
    @workers = []
    @shutdown_initiated = false
    setup_signals
  end
  
  def setup_signals
    Signal.trap("TERM") do
      initiate_shutdown
    end
    
    Signal.trap("INT") do
      initiate_shutdown
    end
  end
  
  def initiate_shutdown
    return if @shutdown_initiated
    @shutdown_initiated = true
    
    puts "Shutdown initiated - sending signals to workers"
    
    # Send TERM signal to all workers
    @workers.each do |pid|
      begin
        Process.kill("TERM", pid)
        puts "Sent TERM to worker #{pid}"
      rescue Errno::ESRCH
        # Process already terminated
      end
    end
    
    # Wait for graceful termination with timeout
    wait_for_workers(timeout: 10)
    
    # Force kill remaining workers
    force_kill_remaining
  end
  
  def wait_for_workers(timeout:)
    deadline = Time.now + timeout
    
    while Time.now < deadline && !@workers.empty?
      @workers.dup.each do |pid|
        begin
          status = Process.waitpid(pid, Process::WNOHANG)
          if status
            @workers.delete(pid)
            puts "Worker #{pid} terminated gracefully"
          end
        rescue Errno::ECHILD
          @workers.delete(pid)
        end
      end
      
      sleep 0.5
    end
  end
  
  def force_kill_remaining
    @workers.each do |pid|
      begin
        Process.kill("KILL", pid)
        puts "Force killed worker #{pid}"
        Process.wait(pid)
      rescue Errno::ESRCH, Errno::ECHILD
        # Already gone
      end
    end
    
    @workers.clear
  end
  
  def spawn_worker
    pid = fork do
      setup_worker_signals
      worker_loop
    end
    
    @workers << pid
    pid
  end
  
  def setup_worker_signals
    Signal.trap("TERM") do
      puts "Worker #{Process.pid} received TERM, shutting down gracefully"
      @worker_running = false
    end
  end
  
  def worker_loop
    @worker_running = true
    
    while @worker_running
      perform_work
      sleep 1
    end
    
    cleanup_worker_resources
    puts "Worker #{Process.pid} exiting cleanly"
  end
  
  def perform_work
    # Simulate work
  end
  
  def cleanup_worker_resources
    # Close files, flush buffers, etc.
  end
end

Tools & Ecosystem

System Monitoring Tools

The ps command displays process information including current states. Different flags reveal various aspects of process execution.

# Show detailed process state information
ps aux

# Show process state codes
ps -eo pid,state,cmd

# Monitor specific process
ps -p <PID> -o pid,state,cmd,time

# Common state codes:
# R - Running (running state)
# S - Sleeping (waiting state)
# D - Uninterruptible sleep (waiting for I/O)
# Z - Zombie (terminated, awaiting reaping)
# T - Stopped (suspended state)

The top and htop commands provide real-time process monitoring with state information, CPU usage, and resource consumption. These tools help identify processes stuck in specific states or consuming excessive resources.

Ruby Gems for Process Management

The sys-proctable gem provides detailed process information including state data on Unix-like systems.

require 'sys/proctable'

# Query process information
Sys::ProcTable.ps do |process|
  puts "PID: #{process.pid}"
  puts "State: #{process.state}"
  puts "Command: #{process.cmdline}"
  puts "---"
end

# Get specific process info
info = Sys::ProcTable.ps(pid: Process.pid)
puts "Current process state: #{info.state}"

The god gem monitors processes and manages state transitions, restarting failed processes and maintaining desired state configurations.

# god configuration example
God.watch do |w|
  w.name = "worker"
  w.start = "ruby worker.rb"
  w.keepalive
  
  # Monitor state transitions
  w.transition(:up, :start) do |on|
    on.condition(:process_running) do |c|
      c.running = false
    end
  end
  
  w.transition(:start, :up) do |on|
    on.condition(:process_running) do |c|
      c.running = true
    end
  end
end

The bluepill gem provides similar process monitoring functionality with a focus on simplicity and reliability.

# bluepill configuration
Bluepill.application("myapp") do |app|
  app.process("worker") do |process|
    process.start_command = "ruby worker.rb"
    process.pid_file = "/var/run/worker.pid"
    
    process.checks :cpu_usage, :every => 10.seconds, 
                    :below => 50, :times => 3
    
    process.checks :mem_usage, :every => 10.seconds,
                    :below => 150.megabytes, :times => [3, 5]
  end
end

Process Monitoring with Systemd

Modern Linux systems use systemd for process management, tracking states and managing service lifecycles.

# Query systemd service state
def check_service_state(service_name)
  output = `systemctl is-active #{service_name}`.strip
  
  case output
  when "active"
    :running
  when "inactive"
    :terminated
  when "failed"
    :terminated
  when "activating"
    :starting
  else
    :unknown
  end
end

# Monitor service state changes
def monitor_service(service_name, interval: 5)
  previous_state = nil
  
  loop do
    current_state = check_service_state(service_name)
    
    if current_state != previous_state
      puts "Service #{service_name}: #{previous_state} -> #{current_state}"
      previous_state = current_state
    end
    
    sleep interval
  end
end

Process Debugging Tools

The strace command traces system calls, revealing when processes enter waiting states for I/O or other operations.

# Trace system calls showing state transitions
strace -p <PID>

# Track specific system calls causing state changes
strace -e trace=wait4,select,poll,epoll_wait -p <PID>

The lsof command lists open files, helping diagnose why processes remain in waiting states.

# Show what a process is waiting on
lsof -p <PID>

# Show processes waiting on specific files
lsof /path/to/file

Common Pitfalls

Zombie Process Accumulation

Failing to reap terminated child processes creates zombies that consume process table entries. While zombies don't consume memory or CPU, excessive zombies exhaust available PIDs.

# WRONG: Creates zombie processes
10.times do
  fork do
    puts "Child #{Process.pid} executing"
    sleep 1
  end
end

# Parent doesn't wait - children become zombies
sleep 100

# CORRECT: Properly reap children
pids = []
10.times do
  pids << fork do
    puts "Child #{Process.pid} executing"
    sleep 1
  end
end

# Wait for all children
pids.each { |pid| Process.wait(pid) }

# CORRECT: Use signal handler for automatic reaping
Signal.trap("CHLD") do
  begin
    Process.waitpid(-1, Process::WNOHANG) while true
  rescue Errno::ECHILD
  end
end

Race Conditions in State Checking

Checking process state and acting on the result creates race conditions if the state changes between check and action.

# WRONG: Race condition between check and action
def unsafe_kill_process(pid)
  if process_running?(pid)
    # State might change here!
    Process.kill("TERM", pid)
  end
end

# CORRECT: Handle exceptions from state changes
def safe_kill_process(pid)
  Process.kill("TERM", pid)
rescue Errno::ESRCH
  # Process already terminated
  puts "Process #{pid} not found"
end

Improper Signal Handling

Ignoring signals or handling them incorrectly leads to processes stuck in unexpected states or failing to terminate properly.

# WRONG: Blocks signal handling
Signal.trap("TERM") do
  # Long-running operation in signal handler
  cleanup_large_dataset  # BAD: Blocks signal handling
end

# CORRECT: Set flag and handle in main loop
@shutdown = false

Signal.trap("TERM") do
  @shutdown = true
end

loop do
  break if @shutdown
  
  perform_work
  
  if @shutdown
    cleanup_resources
    exit 0
  end
end

# CORRECT: Use proper signal handling for worker coordination
Signal.trap("TERM") do
  Thread.new do
    # Handle cleanup in separate thread
    graceful_shutdown
  end
end

Deadlock from Circular Waiting

Processes waiting on each other create deadlocks where no process can transition from waiting to ready.

# WRONG: Potential deadlock
mutex1 = Mutex.new
mutex2 = Mutex.new

thread1 = Thread.new do
  mutex1.synchronize do
    sleep 0.1
    mutex2.synchronize { puts "Thread 1" }
  end
end

thread2 = Thread.new do
  mutex2.synchronize do
    sleep 0.1
    mutex1.synchronize { puts "Thread 2" }
  end
end

# CORRECT: Acquire locks in consistent order
thread1 = Thread.new do
  mutex1.synchronize do
    mutex2.synchronize { puts "Thread 1" }
  end
end

thread2 = Thread.new do
  mutex1.synchronize do  # Same order as thread1
    mutex2.synchronize { puts "Thread 2" }
  end
end

Orphan Process Creation

Parent processes terminating before children create orphans adopted by init. Unintended orphans continue running without supervision.

# WRONG: Parent exits leaving children running
def create_orphans
  5.times do
    fork do
      loop do
        puts "Orphaned child #{Process.pid} running"
        sleep 5
      end
    end
  end
  
  # Parent exits immediately - children orphaned
  exit
end

# CORRECT: Wait for children or explicitly detach
def managed_children
  pids = []
  
  5.times do
    pids << fork do
      perform_limited_work
      exit
    end
  end
  
  # Wait for all children to complete
  pids.each { |pid| Process.wait(pid) }
end

# CORRECT: Intentional daemon detachment
def create_daemon
  pid = fork do
    Process.setsid  # Create new session
    
    fork do  # Double fork prevents zombie
      Dir.chdir "/"
      
      STDIN.reopen "/dev/null"
      STDOUT.reopen "/dev/null"
      STDERR.reopen "/dev/null"
      
      daemon_work_loop
    end
    
    exit  # First child exits
  end
  
  Process.wait(pid)  # Reap first child
  # Grandchild now running as daemon
end

Incorrect Exit Status Handling

Misinterpreting or ignoring exit statuses prevents proper error detection and recovery.

# WRONG: Ignores exit status
pid = fork { exit 1 }
Process.wait(pid)
# Didn't check if child succeeded

# CORRECT: Check exit status
pid = fork { exit 1 }
Process.wait(pid)

if $?.exitstatus != 0
  puts "Child failed with status #{$?.exitstatus}"
  handle_child_failure
end

# CORRECT: Distinguish different termination causes
pid = fork { raise "Error!" }
Process.wait(pid)

if $?.exited?
  puts "Exited normally with status #{$?.exitstatus}"
elsif $?.signaled?
  puts "Terminated by signal #{$?.termsig}"
elsif $?.stopped?
  puts "Stopped by signal #{$?.stopsig}"
end

Reference

Process State Definitions

State Description CPU Usage Schedulable
New Process being created, resources allocated None No
Ready Waiting for CPU assignment in ready queue None Yes
Running Actively executing instructions on CPU Active N/A
Waiting Blocked waiting for event completion None No
Terminated Execution completed, exit status available None No

State Transition Matrix

From State To State Trigger Kernel Action
New Ready Admission Add to ready queue
Ready Running Scheduler dispatch Context switch to process
Running Ready Preemption or time quantum Context switch, return to queue
Running Waiting I/O or event request Move to wait queue
Running Terminated Exit or error Release resources
Waiting Ready Event completion Move to ready queue
Waiting Terminated Signal or error Release resources

Ruby Process Module Methods

Method Purpose Return Value
Process.fork Create child process PID to parent, nil to child
Process.spawn Create process without blocking PID
Process.wait Wait for any child termination PID of terminated child
Process.waitpid Wait for specific child PID when terminated
Process.waitpid2 Wait with status object Array of PID and status
Process.waitall Wait for all children Array of PID and status pairs
Process.detach Create thread to wait for process Thread object
Process.kill Send signal to process Number of processes signaled
Process.pid Get current process PID Integer
Process.ppid Get parent process PID Integer

Wait Flags

Flag Behavior Use Case
Process::WNOHANG Return immediately if no child terminated Non-blocking state check
Process::WUNTRACED Report stopped children Job control
Process::WCONTINUED Report continued children Job control

Common Unix Signals

Signal Number Default Action Purpose
SIGTERM 15 Terminate Graceful shutdown request
SIGKILL 9 Terminate Forced termination
SIGINT 2 Terminate Interrupt from terminal
SIGCHLD 17 Ignore Child state changed
SIGSTOP 19 Stop Suspend process execution
SIGCONT 18 Continue Resume stopped process
SIGHUP 1 Terminate Terminal disconnection
SIGQUIT 3 Terminate with core Quit with core dump

Process State Codes (ps command)

Code State Description
R Running Currently executing or ready to run
S Sleeping Interruptible sleep, waiting for event
D Disk Sleep Uninterruptible sleep, usually I/O
Z Zombie Terminated, awaiting parent reaping
T Stopped Suspended by signal
I Idle Kernel thread idle state

Exit Status Interpretation

Status Meaning Common Cause
0 Success Normal completion
1 General error Application-specific error
2 Misuse Invalid command or arguments
126 Cannot execute Permission denied
127 Command not found Path or binary missing
128+N Signal termination Terminated by signal N
130 SIGINT received Control-C pressed
137 SIGKILL received Forced termination
143 SIGTERM received Graceful termination request