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-daemon and 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.

WarningBe gentle with real systems

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.

Important

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.

  1. (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.

  2. 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.

  3. Detach from the session that invoked the daemon

    1. Dissociating from the controlling tty (Text Terminals)
    2. Create a new session and become the leader of that session
    3. Become a process group leader
  4. 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.

  5. 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.

  6. Change the umask to 0. This lets open(), creat(), and other operating system calls specify their own permissions instead of depending on the parent.

  7. Close all files opened by the parent, including file descriptors, standard streams (stdin, stdout, and stderr), and anything else the daemon can reopen later or receive explicitly.

  8. Use a logfile, the console, or /dev/null as stdin, stdout, and stderr.

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.

Important

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.

Important

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.

Important

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.

Note

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