Processes, fork and signals

fork and wait

fork copies the process. The child runs the block and exits; the parent gets the child's pid and must wait for it, or the process becomes a zombie:

ruby -e 'pid = fork { exit 0 }; Process.wait(pid); p $?.exitstatus'

0

The child shares the memory as a copy-on-write snapshot, so a change in the child is invisible to the parent. A result comes back through a pipe, a file or an exit status, not through a variable.

The exit status

$? is a Process::Status from the last waited child. exitstatus is the number passed to exit, success? is the boolean a caller usually wants, and termsig is set when a signal ended the process instead:

pid = fork { exit 3 } Process.wait(pid) p $?.exitstatus p $?.success?

ruby -e 'pid = fork { exit 3 }; Process.wait(pid); p $?.exitstatus; p $?.success?'

3 false

A worker pool that reports "one job failed" reads the status, not the output. A shell script and a CI job read the same number.

Signals

trap installs a handler for a signal. The handler runs in the main thread between Ruby instructions, which is why it must be short:

trap("USR1") { puts "signal received" } child = fork do sleep 0.1 Process.kill("USR1", Process.ppid) end Process.wait(child)

ruby signal.rb

signal received

SIGTERM is the shutdown request, SIGINT is Ctrl-C, and SIGHUP is the one a server uses to reload its configuration. A long-running script traps TERM to finish the current job and exit cleanly; a script that ignores it is killed after the grace period with no cleanup.

Processes for CPU work

CRuby's GVL lets only one thread run Ruby code at a time, so threads help with waiting (network, disk) and not with arithmetic. Four processes use four cores:

ruby -e 'pids = 4.times.map { |i| fork { sleep 0.1; exit!(i) } }; p pids.map { |pid| Process.wait2(pid).last.exitstatus }'

[0, 1, 2, 3]

The child must exit! or exit explicitly: falling off the end of a fork block runs the parent's at_exit handlers twice. The status list is the four exits in pid order, which is the order the parent waited, not the order they finished.

What to watch for

A fork in a process with open connections copies the handles. A database connection opened before the fork is shared, and two processes writing to one socket corrupt the protocol. Reconnect in the child, or fork before opening anything.

fork is not available on Windows. A tool that must run there uses threads for I/O and a separate process launch (Process.spawn) for CPU work.

Next: Fibers and the scheduler

Article author: Arthur Isaev