How to Write a Unix Daemon with Python

DONG Yuxuan @ Dec 23, 2019


In Unix, a daemon is a program that runs as a background process. A daemon usually executes some periodic tasks like downloading mails. They don’t need interact with users often and they need keep running during the system is running.

Nowadays, writing daemons has no diffrence with writing normal programs. We can just write the main loop that performs the periodic task and make service manager like systemd to handle daemon things. This article is to talk about ancient technologies, wrting daemons without a service manager.

Let’s start with a simple program daemon1.py.

#!/usr/bin/env python3

from time import sleep

while True:
	# Some computation
	sleep(0.1)

We hope the computation being executed in the background. However, if we runs the program from a terminal,

$ python3 daemon1.py

the terminal will be blocked until we press CTRL+C to terminate.

An experienced Unix user knows it can be ran in the background by the follwing command.

$ python3 daemon1.py &

However, it will be terminated after we close the terminal if the huponexit option is on.

You may think to call the system function setsid to detach from the session(terminal). However, setsid can’t be called from a process group leader and the program is a learder as you lunch it from the terminal.

Easy to think of, we can make the program starts a subprocess and the parent process exits immediately. The subprocess calls setsid to detach from the terminal session and do the computation.

#!/usr/bin/env python3
# FILENAME: daemon2.py

from time import sleep
import sys
import os

if os.fork() > 0:
	sys.exit(0)

os.setsid()
while True:
	# Some computation
	sleep(0.1)

The advantage of daemon2.py is not just it can run without a terminal. Also you can run it in the background without adding the & in the command.

If you’re writing a special-purpose daemon, the above code is enough. The rest things you should be careful is not to try to interact with a terminal in your computation code. For example, not to read from stdin, not to write to stdout and so on.

However, if you want to build a library or framework which works fine on all Unix systems, for example, the function below,

def daemonize(computation):
	``` Execute computation() in a daemon process ```

	if os.fork() > 0:
		return

	os.setsid()
	computation()

the above code is not enough. Because you can’t ensure the computation function will not try to take control of a terminal again. This could happen if computation tries to open a terminal-associated device in System V-based systems. See POSIX.1-2008 Section 11.1.3, “The Controlling Terminal”.

The controlling terminal for a session is allocated by the session leader in an implementation-defined manner. If a session leader has no controlling terminal, and opens a terminal device file that is not already associated with a session without using the O_NOCTTY option (see open()), it is implementation-defined whether the terminal becomes the controlling terminal of the session leader. If a process which is not a session leader opens a terminal file, or the O_NOCTTY option is used on open(), then that terminal shall not become the controlling terminal of the calling process.

A process can take control of a terminal only if it’s a session leader. Because the subprocess called setsid, it is a session leader so this can really happen. To avoid the case, we create a sub-subprocess. This is the famous “double-fork trick”.

def daemonize(computation):
	``` Execute computation() in a daemon process ```

	if os.fork() > 0:
		return

	# From now, we're in the subprocess
	os.setsid() # The subprocess becomes the session leader to detach from the terminal

	if os.fork() > 0:
		return

	# From now, we're in the sub-subprocess which is not a session leader
	computation()

Now, if computation opens a terminal device, the daemon will not attach to a terminal. So our program becomes a real daemon, it will not exit after the terminal being closed.

The basic theory is over here. You can free play based on the code. But I think I’m obliged to tell you how other people usually play with the rest part.

The most usual daemonize is like the follwing.

def daemonize(computation):
	``` Execute computation() in a daemon process ```

	if os.fork() > 0:
		return

	# From now, we're in the subprocess

	os.setsid() # The subprocess becomes the session leader to detach from the terminal

	os.chdir('/')
	os.umask(0)

	if os.fork() > 0:
		return

	# From now, we're in the sub-subprocess which is not a session leader

	sys.stdout.flush()
	sys.stderr.flush()
	si = open(os.devnull, 'r')
	so = open(os.devnull, 'a+')
	se = open(os.devnull, 'a+')

	os.dup2(si.fileno(), sys.stdin.fileno())
	os.dup2(so.fileno(), sys.stdout.fileno())
	os.dup2(se.fileno(), sys.stderr.fileno())

	computation()

These are not what you must do. Personally, I usually provide a logfile argument to daemonize and redirect stderr to it.