17 Contacting the UNIX Daemons of Old
Introduction
In this chapter, you’ll look at long-running background processes on Unix-like systems and how Python can fit into that world. We’ll cover the classic daemon shape, modern systemd service files, and the python-daemon library.
Daemons are the sort of programs that run quietly in the background while everyone else is looking at the foreground application. They’re useful, powerful, and occasionally dramatic when they aren’t supervised properly.
Concepts Covered
- Unix processes, PIDs, sessions, and controlling terminals
- Service management with
systemd - Daemon context setup with
python-daemonand PEP 3143 - File descriptors, working directories,
umask, signals, and PID files
Why This Matters
Security tools aren’t always short-lived scripts. Sometimes you need a collector, monitor, or agent that runs in the background, starts reliably, logs clearly, and shuts down without leaving the system in a messy state.
Objective
Explain what makes a Unix daemon different from an ordinary foreground script, then sketch a small Python service that can be supervised safely.
Daemon and service examples can change how a machine starts and supervises processes. Treat the commands as learning material unless you’re working in a disposable lab environment.
Daemons are programs on Unix-based systems that run quietly in the background, unlike most applications you use directly. On Windows, these kinds of programs are usually called services. The broad idea is similar, though not every service is a daemon, and some desktop applications include service components behind the scenes.
macOS is a Unix-based system and uses daemons. The word “service” also has a separate macOS meaning: software that performs a function selected from the Services menu.
In Unix, daemons usually start life as a process. A process is a running instance of a program, and the kernel gives each process a unique PID (Process Identification Number).
Traditionally, a daemon is created when its parent process terminates and the child process detaches from the controlling terminal. In looser everyday speech, people often use “daemon” for any background process.
There are usually a few steps involved in turning a foreground process into a daemon. We’ll walk through the shape without getting lost in every historical detail.
This isn’t a daemon-making 101 tutorial, though. Proper daemonisation is one of those advanced rituals that depends heavily on the operating system, the service manager, the permissions you do and don’t have, and how much time you want to spend fighting process groups, signals, logs, and startup behaviour.
For our purposes, the important idea is simpler: sometimes you want code to keep running quietly in the background after you start it. Here, we’re just going to understand the shape of the problem and use the smallest useful version of it.
(Optional) Remove unnecessary variables from the environment. Variables that aren’t passed when the daemon starts are often lost, because the child process doesn’t know what the parent was doing.
Execute as a background task by forking and exiting in the parent half of the fork. This lets the daemon’s parent receive exit notifications and keep running normally.
Detach from the session that invoked the daemon
- Dissociating from the controlling tty (Text Terminals)
- Create a new session and become the leader of that session
- Become a process group leader
If the daemon wants to make sure it won’t acquire a new controlling tty (Text Terminals) even by accident, it may fork and exit again.
Set the root directory
/as the current working directory so the process doesn’t keep a mounted directory in use. That lets the operating system continue normal operation and unmount file systems when needed.Change the
umaskto 0. This letsopen(),creat(), and other operating system calls specify their own permissions instead of depending on the parent.Close all files opened by the parent, including file descriptors, standard streams (
stdin,stdout, andstderr), and anything else the daemon can reopen later or receive explicitly.Use a logfile, the console, or
/dev/nullasstdin,stdout, andstderr.
You’ll often hear people talk about what makes a “well-behaved” daemon. In practice, that means it starts predictably, logs usefully, handles signals, keeps file permissions sane, shuts down cleanly, and stays in the circle you summoned it in.
Using systemd
systemd is a Unix-based software suite that provides core building blocks for many Linux systems. It includes a system and service manager as well as an init system that can bootstrap user space and manage user processes.
systemd often makes manual daemonising unnecessary. It’s still worth considering who will use your application and whether their system uses systemd.
Once your Python application is ready, you’ll usually create a service file for systemd. It needs the .service extension and is commonly saved in /lib/systemd/system/, which requires sudo.
vim /lib/systemd/system/myapplication.service
Add content like this, changing the description, script filename, and location to match your application.
[Unit]
Description=Dummy Service
After=multi-user.target
[email protected]
[Service]
Type=simple
ExecStart=/usr/bin/python3 /usr/bin/dummy_service.py
StandardInput=tty-force
[Install]
WantedBy=multi-user.target
To escape and save the file using sudo, you can use :w !sudo tee % in Vim. If that sentence means nothing to you, that’s fine; the important part is that service files usually require administrator privileges to write.
Once you’ve created the service, reload the systemctl daemon so it reads the new file.
You’ll need to reload the daemon each time you change the .service file.
$ sudo systemctl daemon-reload
Next, enable the service to start on boot and start it for the current session.
$ sudo systemctl enable myapplication.service
$ sudo systemctl start myapplication.service
If you want to check the service status, run the status command. It’ll show whether the service is running, which pid it’s using, and its memory and CPU (Central Processing Unit) usage.
$ sudo systemctl status myapplication.service
Finally, if you need to stop the service, run the stop command.
$ sudo systemctl stop myapplication.service
Using python-daemon
python-daemon is based on PEP 3143, which defines a standard daemon process library. It isn’t included with Python 3.14 by default, so install it with python -m pip install python-daemon.
Many older examples suggest DaemonRunner, but that approach is deprecated. Use DaemonContext instead.
To get started with DaemonContext, create a with block using the context manager.
with daemon.DaemonContext():
main()This gives you a working, very simple daemon, and you’re probably wondering about all the other pieces now: setting the working directory, preserving important files, and handling operating system signals, so let’s get into the useful bits.
Handling the File System
As we mentioned earlier, daemons are funny little things because they detach from the command that started them. That means a daemon can have its own UID (User Identification), GID (Group Identification), root directory, working directory, and umask. The default configuration handles a lot of this automatically, but sometimes we need to customise it so the service behaves the way we expect.
To change the root directory, which can help confine your daemon to its own directory tree, set the chroot_directory variable to a valid directory on your file system. The working_directory can be set in a similar way and is the more common way of confining a daemon. By default, DaemonContext sets the working directory to root /.
with daemon.DaemonContext(
chroot_directory=None,
working_directory='/var/lib/ose'):
print(os.getcwd())You might notice that you don’t get any output from print(os.getcwd()). Daemonising a process closes files opened by the parent, including file descriptors and standard streams (stdin, stdout, and stderr). Any files required by the daemon can be opened later or explicitly preserved.
But never fear: preserving files is straightforward enough once you know which handles the daemon needs to keep.
Configuring the UID (User Identification) and GID (Group Identification) may also be important if the daemon needs specific permissions. Set uid and/or gid when you need that control. Keep in mind that the user running the daemon must have those permissions; if they don’t, DaemonContext will raise a DaemonOSEnvironmentError exception.
with daemon.DaemonContext(
uid=1001,
gid=777):
print(os.getuid())
print(os.getgid())You might also want to set the daemon umask, which controls the permissions new files start with.
with daemon.DaemonContext(
umask=0o002):
your_mask = os.umask(0)
print(your_mask)
os.umask(your_mask)How to Calculate umask
To calculate the final permission for a directory, start with the base permission 777 and remove the bits set in the umask. Keep in mind that the left-most digit is for the owner, the second left-most digit is for the group, and the final digit is for others.
| Umask digit | Removes | Leaves on a directory |
|---|---|---|
0 |
nothing | rwx (read, write, and execute) |
1 |
execute | rw- (read and write) |
2 |
write | r-x (read and execute) |
3 |
write and execute | r-- (read only) |
4 |
read | -wx (write and execute) |
5 |
read and execute | -w- (write only) |
6 |
read and write | --x (execute only) |
7 |
read, write, and execute | --- (no permissions) |
Preserving Files
Closing files is part of what DaemonContext is supposed to do, but sometimes you need particular files to stay open. You can preserve those file handles with the files_preserve variable.
some_important_file = open('camio.db', 'r')
with daemon.DaemonContext(
files_preserve=['camio.db']):
print(some_important_file.readlines())Along with keeping files open, we can also redirect stdin, stdout, and stderr away from os.devnull and keep them open.
with daemon.DaemonContext(
stdout=sys.stdout,
stderr=sys.stderr):
print("Hello Forneus!")Handling Operating System Signals
Signals from the operating system matter no matter how the process is used. For background processes they’re especially important, because signals may be one of the few ways a user can interact with the daemon. DaemonContext lets you define a dictionary with the signal_map argument and map common signals to handler functions.
This is one of those places where boring plumbing becomes user experience. A daemon that ignores the obvious stop signal isn’t mysterious; it’s annoying.
| Signal | Portable number | Default action | Description |
|---|---|---|---|
SIGABRT |
6 | Terminate (core dump) | Process abort signal |
SIGALRM |
14 | Terminate | Alarm clock |
SIGBUS |
N/A | Terminate (core dump) | Access to an undefined portion of a memory object |
SIGCHLD |
N/A | Ignore | Child process terminated, stopped, or continued |
SIGCONT |
N/A | Continue | Continue executing, if stopped |
SIGFPE |
N/A | Terminate (core dump) | Erroneous arithmetic operation |
SIGHUP |
1 | Terminate | Hangup |
SIGILL |
N/A | Terminate (core dump) | Illegal instruction |
SIGINT |
2 | Terminate | Terminal interrupt signal |
SIGKILL |
9 | Terminate | Kill (cannot be caught or ignored) |
SIGPIPE |
N/A | Terminate | Write on a pipe with no one to read it |
SIGPOLL |
N/A | Terminate | Pollable event |
SIGPROF |
N/A | Terminate | Profiling timer expired |
SIGQUIT |
3 | Terminate (core dump) | Terminal quit signal |
SIGSEGV |
N/A | Terminate (core dump) | Invalid memory reference |
SIGSTOP |
N/A | Stop | Stop executing (cannot be caught or ignored) |
SIGSYS |
N/A | Terminate (core dump) | Bad system call |
SIGTERM |
15 | Terminate | Termination signal |
SIGTRAP |
5 | Terminate (core dump) | Trace/breakpoint trap |
SIGTSTP |
N/A | Stop | Terminal stop signal |
SIGTTIN |
N/A | Stop | Background process attempting read |
SIGTTOU |
N/A | Stop | Background process attempting write |
SIGURG |
N/A | Ignore | High bandwidth data is available at a socket |
SIGUSR1 |
N/A | Terminate | User-defined signal 1 |
SIGUSR2 |
N/A | Terminate | User-defined signal 2 |
SIGVTALRM |
N/A | Terminate | Virtual timer expired |
SIGWINCH |
N/A | Ignore | Terminal window size changed |
SIGXCPU |
N/A | Terminate (core dump) | CPU time limit exceeded |
SIGXFSZ |
N/A | Terminate (core dump) | File size limit exceeded |
import signal
def shutdown(signum, frame): # signum and frame are mandatory
sys.exit(0)
with daemon.DaemonContext(
signal_map={
signal.SIGTERM: shutdown,
signal.SIGTSTP: shutdown
}):
main()There Can Be Only ONE!
Daemons often use resources that only one process can access at a time. This is common for TCP ports and some files on disk. You want to make sure multiple daemon instances aren’t competing for those resources, because that can lead to exceptions or race conditions.
To ensure only one daemon is running at a time, we can create a PID lock file. This file contains the PID of the running process and prevents the same program from running more than once.
Part of spawning a new process is checking that there isn’t already a lock file.
import lockfile
with daemon.DaemonContext(
pidfile=lockfile.FileLock('/var/run/spam.pid')):
main()Starting/Stopping/Restarting
This is where the benefits of python-daemon run out. DaemonContext doesn’t take care of start, stop, and restart commands for you. DaemonRunner has code related to this behaviour, but it’s deprecated, so treat it as historical reference rather than something to copy directly.
One extension to this, although not the most elegant, can still be useful for getting around the problem.
Given a Python application that does something, which is the technical term for it anyway, we can create an initialisation shell script that runs the application and manages termination and restarts.
#!/usr/bin/env python3
import sys
import os
import time
import argparse
import logging
import daemon
from daemon import pidfile
debug_p = False
def do_something(logf):
### This does the "work" of the daemon
logger = logging.getLogger('eg_daemon')
logger.setLevel(logging.INFO)
fh = logging.FileHandler(logf)
fh.setLevel(logging.INFO)
formatstr = '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
formatter = logging.Formatter(formatstr)
fh.setFormatter(formatter)
logger.addHandler(fh)
while True:
logger.debug("this is a DEBUG message")
logger.info("this is an INFO message")
logger.error("this is an ERROR message")
time.sleep(5)
def start_daemon(pidf, logf):
### Launch the daemon in its context
with daemon.DaemonContext(
working_directory='/var/lib/eg_daemon',
umask=0o002,
pidfile=pidfile.TimeoutPIDLockFile(pidf),
) as context:
do_something(logf)
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Example daemon in Python")
parser.add_argument('-p', '--pid-file', default='/var/run/eg_daemon.pid')
parser.add_argument('-l', '--log-file', default='/var/log/eg_daemon.log')
args = parser.parse_args()
start_daemon(pidf=args.pid_file, logf=args.log_file)You can then use the following .sh script to start, stop, and restart the application. One feature I like to add when building an application this way is a run() command that doesn’t daemonise the application, which is especially helpful for debugging or when I don’t want the program running in the background.
#!/bin/bash
# ------------------------------------------------------------------
# A bash script for better management of python-daemon
# ------------------------------------------------------------------
VERSION=0.1.0
SUBJECT=some-unique-id
USAGE="Usage: command -ihv args"
startscript(){
# get the current PID of the script you're trying to run
PID=$(ps aux | grep '[s]criptname.py' | awk '{print $2}')
# if the PID is missing, start the application
if [ -z "$PID" ]; then
./scriptname.py start
else
# otherwise, print an error and leave the process alone
echo -n "ERROR: The process is already running."
echo
fi
}
stopscriptname(){
# get the current PID of the script you're trying to run
PID=$(ps aux | grep '[s]criptname.py' | awk '{print $2}')
# if the PID is missing, there's nothing to stop
if [ -z "$PID" ]; then
echo -n "ERROR: scriptname is not running"
else
# otherwise, kill the script using the built-in UNIX kill command
kill $PID
fi
}
statusscriptname(){
# get the current PID of the script you're trying to run
PID=$(ps aux | grep '[s]criptname.py' | awk '{print $2}')
# report whether the application is running and return the PID
if [ -z "$PID" ]; then
echo "scriptname is not running"
else
echo "scriptname is running with PID $PID"
fi
}
runscriptname(){
# get the current PID of the script you're trying to run
PID=$(ps aux | grep '[s]criptname.py' | awk '{print $2}')
# if the PID is missing, run the application normally without daemonising
if [ -z "$PID" ]; then
./scriptname.py run
else
# otherwise, print an error and leave the process alone
echo -n "ERROR: The process is already running."
echo
fi
}
restartscriptname(){
# run the stop script, then the start script, to restart the process
stopscriptname
startscriptname
}
case "$1" in
# like a case statement, the first argument decides which function to run
start) startscriptname ;;
stop) stopscriptname ;;
run) runscriptname ;;
restart) restartscriptname ;;
status) statusscriptname ;;
*) echo "usage: $0 start | stop | run | restart | status" >$2
exit 1
;;
esac
Checkpoint
Sketch the lifecycle of a supervised background Python process in five steps: start, detach or hand off to a supervisor, write logs, respond to a stop signal, and clean up a PID file or other exclusive resource. What could go wrong if two copies start at once?
Extension Activity
Compare the systemd service-file approach with python-daemon. Write down which one you would choose for a Linux lab machine and which one you would choose for portable Python code that may run outside systemd.
Further Reading
- Daemon Definition.
- fork(2) Linux manual page.
- python-daemon Source Code.
- What is Umask and How To Setup Default umask Under Linux?.
- signal — Set handlers for asynchronous events.
- class threading.Lock.
- argparse — Parser for command-line options, arguments and subcommands.
- Linux Documentation on Processes.
- macOS Documentation of Launch Daemons and Agents.
- PEP 3143 - Standard Daemon Process Library.
- Linux Daemon Using python-daemon with PID File and Logging.