Unix daemons are programs that run in the background. Nginx, Postgres and OpenSSH are a few examples. They use a some special tricks to "detatch" their processes, and let them run independently of any terminal.
I've always been kind of fascinated with daemons - maybe it's the name - and I thought it'd be fun to do a post illustrating how they work. And specifically, how you can create them in Ruby.
...but first.
Don't try this at home!
You probably don't want to make a daemon. There are much easier ways to get the job done.
You might want to make a program that runs in the background. No problem. Your OS provides a system to let you run normal programs in the background.
On ubuntu, this is accomplished via Upstart of systemd. On OSX it's launchd. There are others. But they all work according to the same concept. You provide a configuration file telling the system how to start and stop the long-running program. Then...well, that's pretty much it. You can start the program using a system command like service my_app start
and it runs in the background.
In short, upstart is simple & reliable while old-school daemons are arcane and very hard to get right.
...but if that's the case, why should we learn about daemons? Well, because fun! And we'll learn some interesting facts about Unix processes along the way.
The simplest daemon
Now that you've been told to never make a daemon, let's make some daemons! As of Ruby 1.9, this is incredibly simple. All you have to do is use the Process.daemon method.
# Optional: set the process name to something easy to type<br>$PROGRAM_NAME = "rubydaemon"<br>
# Make the current process into a daemon
Process.daemon()
# Once per second, log the current time to a file
loop do
File.open("/tmp/rubydaemon.log", "a") { |f| f.puts(Time.now) }
sleep(1)
end
Now, when I run this script, control passes back to the console. If I tail my log, I can see that the timestamp is being added every second, just like I expected.
So that was easy. But it still doesn't explain how daemons work. To really understand that, we need to do the daemonization manually.
Changing the parent process
If you use bash to run a normal program, that program's process is a child of bash. But with daemons, it doesn't matter how you launch them. Their parent process is always the "root" process provided by the OS.
You can tell this by looking at the daemon's parent id. A daemon's parent id is always 1. In the example below we use pstree to show this:
$ pstree
-+= 00001 root /sbin/launchd
|--- 72314 snhorne rubydaemon
Interestingly enough, this is also what "orphaned processes" look like. An orphaned process is a child process whose parent has terminated.
So, to create a daemon we have to intentionally orphan a process. The code below does this.
# Optional: set the process name to something easy to type
$PROGRAM_NAME = "rubydaemon"
# Create a new child process and exit the parent. This "orphans"
# our process and creates a daemon.
exit if fork()
# Once per second, log the current time to a file
loop do
File.open("/tmp/rubydaemon.log", "a") { |f| f.puts(Time.now) }
sleep(1)
end
The call to fork results in two processes running the same code. The original process is the parent of the new process. Fork returns a truthy value for the parent and a falsy value for the child. So exit if fork()
only exits the parent.
Detatching from the current session
Our "daemonization" code has a few problems. While it successfully orphans the process, it's still part of the terminal's session. That means that if you kill the terminal, you kill the daemon. To fix this, we need to create a new session and re-fork. Not familiar with unix session groups? Here's a good StackOverflow post.
# Optional: set the process name to something easy to type
$PROGRAM_NAME = "rubydaemon"
# Create a new child process and exit the parent. This "orphans"
# our process and creates a daemon.
exit if fork
# Create a new session, create a new child process in it and
# exit the current process.
Process.setsid
exit if fork
# Once per second, log the current time to a file
loop do
File.open("/tmp/rubydaemon.log", "a") { |f| f.puts(Time.now) }
sleep(1)
end
Re-routing STDIN, STDOUT and STDERR
Another problem that the code above has is that it leaves the existing STDOUT, etc in place. That means that if you launch the daemon from a terminal, anything the daemon writes to STDOUT will be sent to your terminal. Not good.
But you can actually re-route STDIN, STDOUT, and STDERR to any path. Here we re-route to /dev/null.
# Optional: set the process name to something easy to type
$PROGRAM_NAME = "rubydaemon"
# Create a new child process and exit the parent. This "orphans"
# our process and creates a daemon.
exit if fork
# Create a new session, create a new child process in it and
# exit the current process.
Process.setsid
exit if fork
STDIN.reopen "/dev/null"
STDOUT.reopen "/dev/null", "a"
STDERR.reopen '/dev/null', 'a'
# Once per second, log the current time to a file
loop do
File.open("/tmp/rubydaemon.log", "a") { |f| f.puts(Time.now) }
sleep(1)
end
Changing the working directory
Finally, the working directory of the daemon is whatever directory we happened to be in when we ran it. That's probably not the best idea, as I might decide to delete the directory later on. So let's change the directory to / .
# Optional: set the process name to something easy to type
$PROGRAM_NAME = "rubydaemon"
# Create a new child process and exit the parent. This "orphans"
# our process and creates a daemon.
exit if fork
# Create a new session, create a new child process in it and
# exit the current process.
Process.setsid
exit if fork
STDIN.reopen "/dev/null"
STDOUT.reopen "/dev/null", "a"
STDERR.reopen '/dev/null', 'a'
Dir.chdir("/")
# Once per second, log the current time to a file
loop do
File.open("/tmp/rubydaemon.log", "a") { |f| f.puts(Time.now) }
sleep(1)
end
Haven't I seen this before?
This sequence of steps is basically what every Ruby daemon had to do before the Process.daemon method was added to Ruby core. I pretty much copied it line for line from an ActiveSupport extension to the Process module which was removed in Rails 4.x. You can see that method here.