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 |