Background Jobs#

Fork#

The simplest and the most obvious way to run the background process is to fork the current process to split it to two processes. There are two ways to do it:

fork { block } - when the block is given, it will be executed in a subprocess

fork - without the block, fork behaves the same as fork() function from the Unix standard library: it returns PID of the process in the parent, and nil in the child.

Notice that child process inherits all I/O from the parent, so stdout, stdin and all the open file descriptors.

pid = fork
if pid  # pid is not nil, we are in the parent process
  puts "Parent process, child pid is #{pid}"
  puts `ps -ef | grep #{pid}`
  sleep 5
  puts "Parent process finishes"
else   # pid is nil, we are in the child process
  puts "Child process, pid is nil"
  sleep 2
  puts "Child process finishes"
end

The example above launches fork to split into parent and child process. The parent process shows the child PID and waits for 5 seconds, while the child sleeps for only two seconds. This is to ensure that the child finishes before parent, otherwise the child process may became a zombie.

The other way to use fork is to put the child code into a block:

pid = fork do
  puts "Child process, pid is nil"
  sleep 2
  puts "Child process finished"
end
puts "Parent process, child pid is #{pid}"
sleep 5
puts "Parent process finished"

Wait for the Background Process to Finish#

When running the backgroup process, parent process should not finish before ensure, that all the childs are not running. There is a method for that: Process.wait. You can give the child pid as an argument - in this case it will wait for this specific process to finish. Without any arguments, it waits for any child process to finish (so if you run more than one child, you should control all of them).

pid1 = fork do
  sleep 10
  puts "Child 1 process finished"
end
pid2 = fork do
  sleep 5
  puts "Child 2 process finished"
end
puts "Parent process, childen pids are #{pid1}, #{pid2}"
Process.wait pid1
Process.wait pid2

puts "Parent process finished"

Prevent the Zombie Apocalypse#

When you do not want to wait for you child process to finish, you should detach the children process from the parent. Then, you can clearly exit your parent, without the danger of zombie. In the example below, parent process quits and leaves the child running:

pid = fork do
  10.times do
    puts `date`
    sleep 1
  end
  puts "Child process finished"
end
puts "Parent process, child pid is #{pid}"
Process.detach pid
puts "Parent process finished"

When the Child Becames a Daemon#

If you just detach the child process from the parent, it will work in a background. But it will not beacame a daemon - when you close your terminal, the process may receive SIGHUP signal, which by default causes the process to exit. The other problem is stdin, stdout and stderr - they are attached to the terminal, so when you quit your shell they are lost, and the background process dies.

So to create a true daemon, you must reopen the standard I/O and redirect it to somewhere else (a file or /dev/null) and catch the SIGHUP signal.

pid = fork do
  $stdin.reopen  "/dev/null"       # ignore stdin
  $stdout.reopen "/tmp/test", "a"  # stdout to a file
  $stderr.reopen "/tmp/test", "a"  # stderr to a file
  trap(:HUP) do                    # ignore SIGHUP
    puts "SIGHUP received, ignoring"
  end
  60.times do
    puts `date`
    sleep 1
  end
  puts "Daemon process ended"
end
puts "Parent process, daemon pid is #{pid}"
Process.detach pid
puts "Parent process ended"

In the example above we for and detach the child process, as it was in the previous scripts. Then, in the child code, we reopen (redirect) all the standard I/O to file or the the /dev/null. After this, we build a trap for SIGHUP - trap(:HUP) { block } runs the block every time the process receives SIGHUP.

Of course you can catch not only the SIGHUP, see Signal.list for the complete hash of signal name and the corresponding number. Notice that this numbers may vary in the different OS. And of course you cannot catch the SIGKILL. There is a method Signal.kill(signal, pid) to send the signal to the specified process.

Daemonize the Process with Daemons Gem#

For now we know how to run a daemon, and we can control it by sending some signals. But it would be nice, if we could have an interface to control the backgound job, like the standard start/stop/status switches in Unix daemons. Well, There is a Gem for that. It is called daeamons and provides the interface to control your background jobs:

require 'daemons'

Daemons.run_proc('lazyd') do  # run daemon with name 'lazyd'
  loop do
    sleep(1)                  # do nothing, it is just an example
  end
end

As you can see, all the server code is a block passed to Daemons.run_proc method. You can assign the process name as the argument. Now, just run the script with start, stop or status command line arguments:

% ruby daemon.rb start
% ruby daemon.rb status
lazyd: running [pid 50486]
% ps -ef|grep 50486
  501 50486     1   0  8:54PM ??         0:00.06 lazyd
  501 50519 49230   0  8:54PM ttys003    0:00.00 grep 50486
% ruby daemon.rb stop
lazyd: trying to stop process with pid 50486...
lazyd: process with pid 50486 successfully stopped.