A theoretical introduction to unix daemons in Ruby

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 thought it’d be fun to do a post illustrating how they work in Ruby.

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.

daemon_running

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.

What to do next:
  1. Try Honeybadger for FREE
    Honeybadger helps you find and fix errors before your users can even report them. Get set up in minutes and check monitoring off your to-do list.
    Start free trial
    Easy 5-minute setup — No credit card required
  2. Get the Honeybadger newsletter
    Each month we share news, best practices, and stories from the DevOps & monitoring community—exclusively for developers like you.
    author photo

    Starr Horne

    Starr Horne is a Rubyist and Chief JavaScripter at Honeybadger.io. When she's not neck-deep in other people's bugs, she enjoys making furniture with traditional hand-tools, reading history and brewing beer in her garage in Seattle.

    More articles by Starr Horne
    Stop wasting time manually checking logs for errors!

    Try the only application health monitoring tool that allows you to track application errors, uptime, and cron jobs in one simple platform.

    • Know when critical errors occur, and which customers are affected.
    • Respond instantly when your systems go down.
    • Improve the health of your systems over time.
    • Fix problems before your customers can report them!

    As developers ourselves, we hated wasting time tracking down errors—so we built the system we always wanted.

    Honeybadger tracks everything you need and nothing you don't, creating one simple solution to keep your application running and error free so you can do what you do best—release new code. Try it free and see for yourself.

    Start free trial
    Simple 5-minute setup — No credit card required

    Learn more

    "We've looked at a lot of error management systems. Honeybadger is head and shoulders above the rest and somehow gets better with every new release."
    — Michael Smith, Cofounder & CTO of YvesBlue

    Honeybadger is trusted by top companies like:

    “Everyone is in love with Honeybadger ... the UI is spot on.”
    Molly Struve, Sr. Site Reliability Engineer, Netflix
    Start free trial