DONG Yuxuan @ Dec 23, 2019 Asia/Shanghai
This article discusses how to write daemons without service managers.
It’s also how service managers like
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
#!/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()
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.
People usually redirect
/dev/null. This is to avoid
computation reading/writing a closed file.
People usually call
os.chdir('/') to set the working directory to the root path.
People usually call
os.umask(0) to avoid permission problems.
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.