CrackedRuby CrackedRuby

Inter-Process Communication

Overview

Inter-Process Communication (IPC) refers to the mechanisms that operating systems provide for processes to manage shared data and coordinate their execution. Unlike threads within the same process that share memory space, processes operate in isolated memory spaces. IPC mechanisms bridge this isolation, enabling processes to exchange data, synchronize actions, and coordinate complex workflows.

IPC forms the foundation of modern software architecture. Web servers handle requests across multiple processes, database systems coordinate between query executors and storage managers, and microservices architectures depend entirely on process-to-process communication. Operating systems implement IPC through various mechanisms, each with distinct characteristics suited to different communication patterns.

The choice of IPC mechanism affects system architecture, performance, and reliability. A message queue provides different guarantees than shared memory, and a Unix socket offers different capabilities than a named pipe. Understanding these differences enables informed architectural decisions.

Ruby provides access to most IPC mechanisms through its standard library and native extensions. While Ruby's Global Interpreter Lock affects threading, it does not restrict process-based concurrency, making IPC particularly relevant for Ruby applications requiring parallelism.

# Basic process creation establishes the foundation for IPC
pid = fork do
  puts "Child process: #{Process.pid}"
  sleep 2
end

puts "Parent process: #{Process.pid}"
Process.wait(pid)
# Parent process: 12345
# Child process: 12346

Key Principles

IPC mechanisms operate through several fundamental approaches. The operating system mediates all inter-process communication, enforcing isolation and access control. Processes cannot directly access each other's memory, so the OS provides controlled channels for data transfer.

Message Passing transfers data explicitly from one process to another. The sender packages data into a message and invokes an OS service to transmit it. The receiver invokes a corresponding service to retrieve the message. This approach makes data flow explicit and facilitates reasoning about system behavior. Pipes, sockets, and message queues implement message passing with different characteristics.

Shared Memory allows multiple processes to map the same physical memory region into their address spaces. Processes can then read and write shared data structures directly without OS mediation for each operation. This provides the highest performance but requires explicit synchronization to prevent race conditions. The operating system manages the mapping but does not control individual reads and writes.

Synchronization Primitives coordinate process execution without transferring data. Semaphores, mutexes, and condition variables allow processes to signal each other and coordinate access to shared resources. These mechanisms prevent race conditions and deadlocks in concurrent systems.

Remote Procedure Calls abstract communication as procedure invocations. One process calls a function that appears local but executes in another process. The RPC system handles marshaling arguments, transmitting the request, executing the remote procedure, and returning results. This pattern simplifies distributed system programming.

Process identity and isolation determine IPC security. Operating systems assign each process a user ID and group ID, enforcing permissions on IPC resources. A process can only access IPC objects it has permission to use, preventing unauthorized communication between processes.

Data serialization becomes necessary when processes communicate across machine boundaries or between different language runtimes. The sending process must convert data structures into a byte stream, and the receiving process must reconstruct them. Format choices affect performance, compatibility, and maintainability.

# Pipes demonstrate unidirectional message passing
reader, writer = IO.pipe

pid = fork do
  writer.close  # Child closes write end
  data = reader.read
  puts "Child received: #{data}"
  reader.close
end

reader.close  # Parent closes read end
writer.puts "Message from parent"
writer.close

Process.wait(pid)
# Child received: Message from parent

Process lifetime management affects IPC reliability. When a process terminates unexpectedly, the OS typically closes its open IPC resources. Other processes must detect and handle this condition. Proper error handling distinguishes robust IPC implementations from fragile ones.

Implementation Approaches

Pipes provide unidirectional byte streams between processes with a parent-child relationship. Anonymous pipes exist only while both ends remain open, making them suitable for communication between processes created through forking. Named pipes (FIFOs) persist in the filesystem, allowing unrelated processes to connect.

Pipes buffer data in kernel memory, blocking writers when the buffer fills and blocking readers when empty. This flow control prevents fast producers from overwhelming slow consumers. The kernel manages buffering automatically, but buffer size limits affect achievable throughput.

Unix Domain Sockets extend pipe functionality with bidirectional communication and more flexible connection patterns. Processes establish socket connections through filesystem paths, supporting both stream-oriented (TCP-like) and datagram (UDP-like) communication. Unix domain sockets outperform TCP sockets for local communication by eliminating network protocol overhead.

TCP/IP Sockets enable communication between processes on different machines or between processes that need network protocol features. The socket API provides a uniform interface for both local and remote communication. Connection-oriented TCP sockets provide reliable, ordered delivery, while connectionless UDP sockets offer lower latency at the cost of reliability guarantees.

Message Queues decouple senders and receivers temporally. A sender places a message in the queue and continues execution without waiting for a receiver. Multiple receivers can consume messages from the same queue, enabling load distribution. The queue persists messages even when no receivers are active, providing durability.

System V message queues integrate with traditional Unix IPC facilities, while POSIX message queues offer a more modern interface. Message queues provide message boundaries—each send operation produces a discrete message that arrives intact at the receiver. This contrasts with byte stream abstractions where message framing requires application-level protocols.

Shared Memory offers the highest performance by eliminating data copying. After mapping shared memory, processes access data with normal memory operations. However, processes must implement synchronization explicitly to prevent race conditions. Semaphores or memory-mapped mutexes coordinate access.

POSIX shared memory objects use filesystem paths for naming, while System V shared memory uses integer keys. Both approaches provide memory segments that multiple processes can map. Changes written by one process become immediately visible to others sharing the region.

Memory-Mapped Files extend shared memory concepts to persistent storage. Processes map a file into their address space and access it with memory operations. The operating system handles synchronizing memory contents with disk storage. This approach simplifies file I/O and enables efficient data sharing through persistent storage.

Signals provide asynchronous notifications between processes. The kernel delivers signals to processes based on events like timer expiration, terminal interrupts, or explicit kill commands. Signal handlers execute asynchronously, complicating reasoning about program state. Signals work best for simple notifications rather than complex communication.

Ruby Implementation

Ruby exposes IPC mechanisms through the IO, Socket, and Process classes in the standard library. The fork method creates child processes, establishing the foundation for process-based parallelism.

Pipes in Ruby use IO.pipe, which returns a connected reader and writer. After forking, processes close the unused end and communicate through the remaining end. Ruby's IO objects provide buffering and encoding conversion automatically.

def pipe_communication(data)
  reader, writer = IO.pipe
  
  pid = fork do
    writer.close
    result = reader.read.upcase
    puts "Transformed: #{result}"
    reader.close
    exit!(0)
  end
  
  reader.close
  writer.write(data)
  writer.close
  Process.wait(pid)
end

pipe_communication("hello world")
# Transformed: HELLO WORLD

Unix Sockets through UNIXSocket and UNIXServer classes provide connection-oriented communication. The server creates a listening socket bound to a filesystem path, and clients connect to that path. Data transfer works identically to network sockets.

require 'socket'

# Server process
server_path = '/tmp/ruby_ipc.sock'
File.delete(server_path) if File.exist?(server_path)

server_pid = fork do
  server = UNIXServer.new(server_path)
  client = server.accept
  
  data = client.gets
  response = "Processed: #{data.strip}"
  client.puts(response)
  
  client.close
  server.close
end

sleep 0.1  # Allow server to start

# Client process
client = UNIXSocket.new(server_path)
client.puts("test data")
result = client.gets
puts result
client.close

Process.wait(server_pid)
File.delete(server_path)
# Processed: test data

TCP Sockets use TCPSocket and TCPServer classes. The API matches Unix sockets but operates over network protocols. Ruby handles host resolution, connection establishment, and protocol details.

require 'socket'

server_pid = fork do
  server = TCPServer.new('localhost', 9999)
  client = server.accept
  
  message = client.read
  client.write("Echo: #{message}")
  
  client.close
  server.close
end

sleep 0.1

client = TCPSocket.new('localhost', 9999)
client.write("network message")
client.close_write

response = client.read
puts response
client.close

Process.wait(server_pid)
# Echo: network message

DRb (Distributed Ruby) provides object-oriented remote procedure calls. Objects in one Ruby process become accessible to other Ruby processes. DRb handles serialization, network communication, and method dispatch transparently.

require 'drb'

# Server object
class Calculator
  def add(a, b)
    a + b
  end
  
  def multiply(a, b)
    a * b
  end
end

server_pid = fork do
  DRb.start_service('druby://localhost:8787', Calculator.new)
  DRb.thread.join
end

sleep 0.5

# Client access
DRb.start_service
calculator = DRbObject.new_nil('druby://localhost:8787')

result1 = calculator.add(5, 3)
result2 = calculator.multiply(4, 7)

puts "Add: #{result1}, Multiply: #{result2}"
# Add: 8, Multiply: 28

Process.kill('TERM', server_pid)
Process.wait(server_pid)

IO.popen combines process creation with pipe communication. It executes a command in a child process and returns an IO object connected to its standard input, output, or both. This simplifies interaction with external programs.

# Read from command output
IO.popen(['echo', 'test']) do |io|
  output = io.read
  puts "Output: #{output}"
end
# Output: test

# Write to command input and read output
IO.popen('bc', 'r+') do |calc|
  calc.puts('5 + 3')
  calc.close_write
  result = calc.read
  puts "Calculation: #{result}"
end
# Calculation: 8

Process Communication with Marshal serializes Ruby objects for IPC. Marshal converts objects to byte streams that can transmit through any IPC mechanism, then reconstructs them in the receiving process.

reader, writer = IO.pipe

pid = fork do
  writer.close
  data = Marshal.load(reader)
  puts "Received: #{data.inspect}"
  reader.close
end

reader.close
complex_data = {name: 'test', values: [1, 2, 3], time: Time.now}
Marshal.dump(complex_data, writer)
writer.close

Process.wait(pid)
# Received: {:name=>"test", :values=>[1, 2, 3], time=>2025-10-07 14:32:10...}

Practical Examples

Worker Pool with Pipes distributes work across multiple processes. The parent process sends tasks through pipes to worker children, which return results. This pattern enables parallelism in CPU-bound Ruby applications.

def worker_pool(tasks, worker_count)
  workers = worker_count.times.map do
    task_reader, task_writer = IO.pipe
    result_reader, result_writer = IO.pipe
    
    pid = fork do
      task_writer.close
      result_reader.close
      
      while task_data = task_reader.gets
        task = Marshal.load(task_data)
        result = yield(task)  # Process task
        Marshal.dump(result, result_writer)
        result_writer.flush
      end
      
      task_reader.close
      result_writer.close
    end
    
    task_reader.close
    result_writer.close
    
    {pid: pid, task_writer: task_writer, result_reader: result_reader}
  end
  
  results = []
  tasks.each_with_index do |task, index|
    worker = workers[index % worker_count]
    Marshal.dump(task, worker[:task_writer])
    worker[:task_writer].flush
    results << Marshal.load(worker[:result_reader])
  end
  
  workers.each do |worker|
    worker[:task_writer].close
    worker[:result_reader].close
    Process.wait(worker[:pid])
  end
  
  results
end

tasks = (1..10).to_a
results = worker_pool(tasks, 3) do |n|
  n * n  # Square each number
end

puts "Results: #{results.inspect}"
# Results: [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

Request-Reply Pattern with Unix Sockets implements a service that processes requests from multiple clients. The server handles each connection in sequence or spawns child processes for concurrent handling.

require 'socket'
require 'json'

def run_server(socket_path)
  File.delete(socket_path) if File.exist?(socket_path)
  server = UNIXServer.new(socket_path)
  
  fork do
    loop do
      client = server.accept
      
      request = JSON.parse(client.gets)
      response = {
        status: 'ok',
        result: process_request(request),
        timestamp: Time.now.to_i
      }
      
      client.puts(JSON.generate(response))
      client.close
    end
  end
end

def process_request(request)
  case request['action']
  when 'reverse'
    request['data'].reverse
  when 'upcase'
    request['data'].upcase
  else
    'unknown action'
  end
end

def send_request(socket_path, action, data)
  client = UNIXSocket.new(socket_path)
  
  request = {action: action, data: data}
  client.puts(JSON.generate(request))
  
  response = JSON.parse(client.gets)
  client.close
  
  response
end

socket_path = '/tmp/service.sock'
server_pid = run_server(socket_path)
sleep 0.1

response1 = send_request(socket_path, 'reverse', 'hello')
response2 = send_request(socket_path, 'upcase', 'world')

puts "Response 1: #{response1['result']}"
puts "Response 2: #{response2['result']}"
# Response 1: olleh
# Response 2: WORLD

Process.kill('TERM', server_pid)
Process.wait(server_pid)
File.delete(socket_path)

Pub-Sub with Named Pipes implements a simple message broadcast system. Multiple subscribers open a named pipe for reading, and a publisher writes messages that all subscribers receive.

require 'thread'

def create_named_pipe(path)
  system("mkfifo #{path}") unless File.exist?(path)
end

def publisher(pipe_path, messages)
  messages.each do |msg|
    # Open for each write to avoid blocking
    File.open(pipe_path, 'w') do |pipe|
      pipe.puts(msg)
    end
    sleep 0.1
  end
end

def subscriber(pipe_path, id)
  fork do
    File.open(pipe_path, 'r') do |pipe|
      while line = pipe.gets
        puts "Subscriber #{id} received: #{line.strip}"
      end
    end
  end
end

pipe_path = '/tmp/pubsub_pipe'
create_named_pipe(pipe_path)

# Start subscribers
sub1 = subscriber(pipe_path, 1)
sub2 = subscriber(pipe_path, 2)

sleep 0.5

# Publish messages
publisher(pipe_path, ['message 1', 'message 2', 'message 3'])

sleep 1

Process.kill('TERM', sub1)
Process.kill('TERM', sub2)
Process.wait(sub1)
Process.wait(sub2)
File.delete(pipe_path)

Coordinated Shutdown with Signals manages graceful process termination. Parent processes signal children to shut down cleanly, allowing them to finish current operations and release resources.

def worker_with_shutdown
  shutdown = false
  
  trap('TERM') do
    shutdown = true
  end
  
  fork do
    counter = 0
    until shutdown
      counter += 1
      sleep 0.5
    end
    puts "Worker shutting down after #{counter} iterations"
  end
end

workers = 3.times.map { worker_with_shutdown }

sleep 2

puts "Initiating shutdown"
workers.each { |pid| Process.kill('TERM', pid) }
workers.each { |pid| Process.wait(pid) }

puts "All workers terminated"
# Worker shutting down after 4 iterations
# Worker shutting down after 4 iterations
# Worker shutting down after 4 iterations
# All workers terminated

Performance Considerations

IPC mechanism selection directly impacts throughput and latency. Shared memory provides the fastest data transfer by eliminating copying, while message passing mechanisms incur overhead from kernel mediation and buffer management.

Shared Memory achieves the lowest latency for large data transfers. After initial mapping overhead, processes access shared data at memory speed. A 1MB shared memory region allows processes to exchange data in microseconds. However, coordination overhead from synchronization primitives can dominate for small transfers.

Unix Domain Sockets outperform TCP sockets for local communication by 30-50% in typical scenarios. The kernel optimizes Unix socket transfers by avoiding network stack processing. For applications communicating on the same machine, Unix sockets should be the default choice over TCP.

Pipes perform well for unidirectional data flows. Kernel buffering reduces system call overhead, and the simple unidirectional model enables optimizations. However, pipe buffers are limited—typically 64KB on Linux—causing writers to block when receivers fall behind.

System call frequency dominates IPC overhead. Each send or receive operation invokes the kernel, incurring context switch costs. Batching multiple messages into larger transfers amortizes this overhead. An application sending 1000 small messages should buffer them into fewer larger sends.

# Inefficient: many small writes
1000.times do |i|
  socket.write("message #{i}\n")
end

# Efficient: batch into larger write
buffer = 1000.times.map { |i| "message #{i}\n" }.join
socket.write(buffer)

Serialization overhead affects high-throughput applications. Marshal provides convenience but adds CPU cost for encoding and decoding. Binary protocols like MessagePack reduce serialization overhead compared to text formats like JSON. For maximum performance, custom binary protocols eliminate unnecessary encoding.

Process creation cost impacts IPC architecture decisions. Forking a Ruby process takes milliseconds, amortizing over long-lived workers but prohibitive for short-lived tasks. Pre-forking server models create worker processes during initialization, avoiding fork overhead per request.

Buffer management affects memory efficiency. Message queues and socket buffers consume kernel memory, limiting concurrent connections. Applications handling thousands of connections must manage buffer sizes carefully to avoid memory exhaustion.

Copy elimination improves throughput for large data transfers. Unix socket credential passing allows sending file descriptors between processes, enabling zero-copy data sharing. Instead of reading data and sending it through a socket, a process can send the file descriptor directly.

require 'socket'

# Send file descriptor through Unix socket
def send_fd(socket, file)
  socket.sendmsg('', 0, nil, [:SOCKET, :RIGHTS, file.fileno])
end

def receive_fd(socket)
  msg, _, _, cred = socket.recvmsg(1)
  fd = cred.unix_rights[0]
  IO.new(fd)
end

Contention reduction in shared memory systems requires careful synchronization design. Fine-grained locking allows higher concurrency than coarse-grained locks but increases complexity. Lock-free algorithms using atomic operations eliminate lock contention entirely for appropriate use cases.

Security Implications

IPC mechanisms bypass network security controls, requiring explicit protection. Processes on the same machine can communicate through Unix sockets or shared memory without firewall mediation. Applications must implement authentication and authorization at the IPC layer.

Unix Socket Permissions control access through filesystem permissions. Creating a Unix socket with restrictive permissions (0600) limits connections to the socket owner. However, this provides coarse-grained control—either full access or no access.

require 'socket'

socket_path = '/tmp/secure_socket.sock'
File.delete(socket_path) if File.exist?(socket_path)

server = UNIXServer.new(socket_path)
File.chmod(0600, socket_path)  # Owner only

# Socket now accessible only to creating user

Credential Passing through Unix sockets enables authentication. The operating system provides the connecting process's user ID, group ID, and process ID to the receiving process. This allows servers to verify client identity and enforce access control.

require 'socket'

def get_peer_credentials(socket)
  cred = socket.getsockopt(:SOCKET, :PEERCRED)
  # Returns struct with pid, uid, gid
  pid, uid, gid = cred.unpack('III')
  {pid: pid, uid: uid, gid: gid}
end

Input Validation remains critical despite process isolation. Malicious or compromised processes can send crafted data through IPC channels. Applications must validate all received data before use, checking types, ranges, and formats. Trusting IPC data based on source process identity alone leads to vulnerabilities.

Serialization formats affect attack surface. Marshal deserializes arbitrary Ruby objects, enabling code execution if attackers control serialized data. JSON or MessagePack restrict deserialization to simple data types, reducing exploit opportunities. Custom binary protocols with explicit type specifications provide the most control.

Denial of Service through IPC resource exhaustion requires rate limiting and resource quotas. A malicious process can flood Unix sockets with connection attempts or fill message queues with bogus messages. Servers must limit concurrent connections, message rates, and queue depths.

Shared memory segments persist after process termination unless explicitly removed. Abandoned shared memory consumes system resources and may contain sensitive data. Applications must implement cleanup procedures, ideally using POSIX shared memory with O_EXCL to detect reuse attempts.

Timing Attacks can leak information through IPC response times. Authentication checks that return immediately for invalid users but slowly for valid users reveal user existence. Constant-time comparison algorithms prevent these leaks.

Race conditions in IPC setup enable attacks. Creating a Unix socket in a shared directory allows attackers to create that path first, capturing connections intended for the legitimate server. Applications should create sockets in protected directories or use abstract socket names that exist only in the kernel.

Common Pitfalls

Forgetting to Close File Descriptors in child processes causes resource leaks. After forking, children inherit copies of all parent file descriptors. Unused descriptors must be closed explicitly to prevent exhaustion.

# Incorrect: descriptors leak in child
reader, writer = IO.pipe
fork do
  data = reader.read  # Writer still open
  puts data
end

# Correct: close unused end
reader, writer = IO.pipe
fork do
  writer.close
  data = reader.read
  reader.close
  puts data
end

Buffering Issues cause apparent data loss or delays. Ruby IO objects buffer data by default. Without explicit flushing, data may remain in buffers instead of transmitting. This particularly affects line-oriented protocols expecting immediate sends.

# Data may not transmit immediately
socket.puts("message")

# Force immediate transmission
socket.puts("message")
socket.flush

Deadlock from Circular Waits occurs when processes wait for each other. Process A writes to B and waits for a response, while B writes to A and waits for a response. Neither can proceed because both are blocked writing. Implementing non-blocking I/O or careful protocol design prevents this.

Zombie Processes accumulate when parents don't wait for children. After a child exits, it remains as a zombie until the parent calls Process.wait. Long-running parents must wait for all children to prevent zombie accumulation.

# Incorrect: zombies accumulate
100.times do
  fork { exit }
end
sleep 5
# 100 zombie processes exist

# Correct: wait for children
pids = 100.times.map { fork { exit } }
pids.each { |pid| Process.wait(pid) }

Marshal Incompatibility between Ruby versions breaks IPC. Marshal format changes across Ruby versions, causing deserialization failures when communicating between processes running different Ruby versions. Text-based formats like JSON provide better cross-version compatibility.

Race Conditions in File-based IPC occur without proper locking. Multiple processes accessing the same file-based resource (named pipes, Unix sockets) can conflict during creation or deletion. Using advisory locks or atomic operations prevents races.

Assuming Atomicity of network operations leads to bugs. Socket reads and writes are not atomic for multi-byte messages. A 1000-byte write may transmit in multiple segments, and reads may receive partial messages. Applications must implement framing protocols or use length prefixes.

# Incorrect: assumes complete transmission
message = "x" * 10000
socket.write(message)

# Correct: ensure complete transmission
message = "x" * 10000
total = 0
while total < message.length
  sent = socket.write(message[total..-1])
  total += sent
end

Ignoring EINTR from system calls causes failures. Signal handlers can interrupt system calls, returning EINTR. Ruby generally retries automatically, but explicit system call wrappers must handle this condition.

Buffer Overflows from unbounded reads exhaust memory. Reading from an untrusted source without size limits allows attackers to consume all memory. Always impose maximum message sizes and enforce them during reads.

Tools & Ecosystem

Redis provides advanced IPC patterns through its data structure server. Multiple processes communicate by reading and writing Redis keys, with atomic operations ensuring consistency. Redis pub/sub implements broadcast messaging, and lists enable job queues.

require 'redis'

redis = Redis.new

# Simple queue pattern
redis.lpush('jobs', 'task_1')
redis.lpush('jobs', 'task_2')

job = redis.brpop('jobs', timeout: 5)
puts "Processing: #{job[1]}"  # task_1

RabbitMQ and ActiveMQ implement AMQP message queuing with reliable delivery guarantees. The Bunny gem provides Ruby AMQP client functionality, enabling processes to exchange messages through durable queues that survive restarts.

dRuby (DRb) in Ruby standard library enables transparent remote object access. Objects in one Ruby process become method-callable from other processes, with DRb handling network communication and serialization automatically.

Sidekiq and Resque implement background job processing through Redis-backed queues. Web processes push jobs into queues, and worker processes fetch and execute them. This pattern decouples request handling from slow operations.

Unicorn and Puma web servers use preforking to handle concurrent requests through process-based parallelism. The master process accepts connections and distributes them to worker processes, achieving concurrency despite Ruby's GIL.

MessagePack provides efficient binary serialization, outperforming JSON and Marshal for IPC data transfer. The msgpack gem integrates MessagePack into Ruby applications, reducing serialization overhead.

Celluloid abstracts actor-based concurrency, simplifying concurrent system design. While primarily thread-based, Celluloid patterns apply to process-based systems, with actors mapping to processes and message passing to IPC.

gRPC enables efficient RPC with Protocol Buffers serialization. The grpc gem provides Ruby gRPC implementation, useful for high-performance service-to-service communication with strict type safety.

ZeroMQ offers brokerless message passing with rich patterns. The ffi-rzmq gem exposes ZeroMQ to Ruby, providing publish-subscribe, push-pull, and request-reply patterns without central brokers.

Reference

IPC Mechanism Comparison

Mechanism Direction Connection Persistence Use Case
Anonymous Pipe Unidirectional Parent-child Process lifetime Simple parent-child data flow
Named Pipe Unidirectional Any processes Filesystem Unrelated process communication
Unix Socket Bidirectional Client-server Socket lifetime Local client-server
TCP Socket Bidirectional Client-server Connection lifetime Network or flexible local IPC
UDP Socket Bidirectional Connectionless N/A Low-latency message exchange
Shared Memory Bidirectional Any processes Manual cleanup High-throughput data sharing
Message Queue Unidirectional Many-to-many Manual cleanup Asynchronous work distribution
Signal Unidirectional Any processes N/A Simple notifications

Ruby IPC Classes

Class Purpose Key Methods
IO.pipe Create pipe pair pipe, read, write, close
Process Process management fork, wait, kill, pid
UNIXSocket Unix socket client new, send, recv, close
UNIXServer Unix socket server new, accept, close
TCPSocket TCP client new, read, write, close
TCPServer TCP server new, accept, close
Marshal Object serialization dump, load
DRbObject Remote objects new, method calls
Signal Signal handling trap, kill

Common Patterns

Pattern Description Implementation
Request-Reply Client sends request, waits for response TCP or Unix socket with blocking I/O
Publish-Subscribe Broadcast to multiple receivers Named pipe or message queue
Pipeline Chain of processing stages Multiple pipes connecting processes
Worker Pool Distribute work across workers Parent sends tasks through pipes
Master-Worker Master coordinates workers Forking server with message passing
Producer-Consumer Decouple production from consumption Message queue or pipe with buffering

Performance Characteristics

Mechanism Latency Throughput Scalability
Shared Memory Microseconds Gigabytes/sec Limited by memory contention
Unix Socket Microseconds Hundreds MB/sec Limited by kernel buffers
TCP Socket Milliseconds Hundreds MB/sec Limited by network stack
Pipe Microseconds Hundreds MB/sec Limited by buffer size
Message Queue Milliseconds Variable Limited by queue size

Security Checklist

Concern Mitigation
Unauthorized access Unix socket permissions, credential checking
Input validation Validate all received data types and ranges
Denial of service Rate limiting, connection limits, timeouts
Data confidentiality Encrypt sensitive data in transit
Serialization attacks Use safe formats (JSON), avoid Marshal from untrusted sources
Resource leaks Close descriptors, clean up shared memory
Race conditions Atomic file operations, proper locking

Error Handling

Error Cause Solution
EPIPE Write to closed pipe Check return values, handle signals
ECONNREFUSED Connection rejected Retry with backoff, verify server running
EINTR System call interrupted Retry operation (Ruby handles automatically)
EAGAIN Resource temporarily unavailable Retry or use blocking operations
EMFILE Too many open files Close unused descriptors, increase limits
ENOENT Socket file missing Verify path, check permissions
EADDRINUSE Address already in use Choose different port, clean up old sockets