#デーモンプロセス

デーモンプロセスとは、端末ログインとは独立してバックグラウンドで常駐し、継続的に機能を提供するプロセスのこと。

代表例は Web サーバー (nginx)、SSH サーバー (sshd)、コンテナランタイム (dockerd)や DBMS など。 名前の末尾に d が付く慣習は daemon に由来する。

通常のプロセスは、以下のように起動した端末やセッションに紐づいている。

ターミナル(tty)
    └── セッション
            └── プロセスグループ
                    └── プロセス

そのままだとログアウトや端末終了で後述する SIGHUP が飛びプロセスが停止するため、プロセスがバックグラウンドで動作するには以下が必要になる。

  • 端末 (TTY) からの分離
  • 親プロセスからの独立
  • 標準入出力の扱いを明確化
  • 安全な作業ディレクトリと umask 設定

##伝統的なデーモン化手順

古典的には次の流れを取る。

  1. fork する
  • POSIX ではプロセスグループのリーダーが新規にセッションを開始することを禁止しているため
  1. setsid で新しいセッションリーダーになり、親プロセスのセッション制御対象から外れる
  2. 再度 fork する。setsid でセッションリーダーになると、制御端末をうっかり取得できてしまうことがある。そのためもう一度 fork する。
  3. chdir("/") でマウントを掴み続けないようにする
  • 各プロセスは現在の cwd を持っており、そのディレクトリはアンマウント、削除ができなくなるため
  1. umask を適切に設定する
  • 昔の Unix だと umask を0にするらしいが、実際には適切な値にするのが良さそうだが、よくわかっていない
  1. stdin stdout stderr/dev/null やログ先に切り替える
  2. メインループを実行し、SIGTERM で安全に終了する

実際の Linux では、アプリ側で複雑なデーモン化を実装せず、systemd などのプロセスマネージャに監督させる構成が主流となっている。

##デーモンを制御するシグナル

  • SIGTERM (terminated): 通常停止 (systemctl stopkill のデフォルト)
  • SIGINT (interrupt): 対話実行時の停止 (Ctrl+C)
  • SIGHUP (hangup): 制御端末の切断時に送られるシグナル。デーモンは制御端末を持たないため、慣例として設定ファイルの再読み込みシグナルとして扱われる (nginx -s reloadsystemctl reload など)
  • SIGKILL (kill): 強制終了。INTTERM で終了できないようなプロセスを終了させるために使われる

##簡単なハンズオン

ここでは、シグナルを受け取る Python スクリプトを systemd に管理させてみる。

import signal
import sys
import time
import os
from types import FrameType

running = True # メインループを継続するかのフラグ

def handle_sigterm(signum: int, _frame: FrameType | None) -> None:
    """シグナルハンドラ。シグナルを受け取ったら `running=False` にしてメインループを終了させる。

    Args:
        signum (int): 受信したシグナル番号
        _frame (FrameType): 受信時点でのスタックフレーム
    """
    global running
    print(f"[PID {os.getpid()}] Received signal {signum}. Shutting down...")
    running = False

# シグナルハンドラの登録
signal.signal(signal.SIGTERM, handle_sigterm) # SIGTERM: `systemd stop` や `kill` のデフォルト
signal.signal(signal.SIGINT, handle_sigterm) # SIGINT: `Ctrl+C`など端末からの割り込み

print(f"[PID {os.getpid()}] Starting process")

# メインループ
while running:
    print(f"[PID {os.getpid()}] Working...")
    time.sleep(3)

print(f"[PID {os.getpid()}] Clean exit")
sys.exit(0)

##systemd で管理する

Docker コンテナ内に systemd をインストールして実行させる。

###1. Dockerfile (検証用)

FROM ubuntu:22.04

RUN apt-get update \
  && apt-get install -y init systemd systemd-sysv dbus python3 \
  && apt-get clean

RUN mkdir -p /opt/simple-daemon

COPY simple_daemon.py /opt/simple-daemon/simple_daemon.py
COPY simple_daemon.service /etc/systemd/system/simple_daemon.service
COPY entrypoint.sh /usr/local/bin/entrypoint.sh

RUN chmod 0644 /opt/simple-daemon/simple_daemon.py \
  && chmod 0644 /etc/systemd/system/simple_daemon.service \
  && chmod +x /usr/local/bin/entrypoint.sh

VOLUME ["/sys/fs/cgroup"]
STOPSIGNAL SIGRTMIN+3

ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]

systemd をコンテナ内で動かすためにいくつかの工夫が必要(このあたりはまだ理解できていない…)。

  • init systemd systemd-sysv dbus: systemd 本体と PID 1 として動作するために必要なパッケージ群
  • VOLUME ["/sys/fs/cgroup"]: systemd はプロセス管理に cgroup (Control Groups) を使うため、ホストの cgroup ファイルシステムをマウントする
  • STOPSIGNAL SIGRTMIN+3: docker stop 時に systemd へ送るシグナル。通常の SIGTERM (15) ではなく、systemd が正常シャットダウンするための専用シグナル

entrypoint.sh

コンテナ起動時に実行されるスクリプト。systemd を PID 1 として起動し、その配下でサービスを管理させる。

#!/bin/bash
set -e

# Enable the simple_daemon service
systemctl enable simple_daemon.service

# Start the init system
exec /sbin/init
  • systemctl enable: コンテナ起動時に simple_daemon サービスが自動起動するよう登録する
  • exec /sbin/init: 現在のシェルプロセスを systemd (= /sbin/init) に置き換えて PID 1 として起動する

###2. Unit ファイル

[Unit]
Description=Simple Python Daemon (hands-on)
After=network.target

[Service]
Type=simple
ExecStart=/usr/bin/python3 /opt/simple-daemon/simple_daemon.py
Restart=on-failure
RestartSec=2
WorkingDirectory=/opt/simple-daemon
StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target
  • After=network.target: ネットワークが利用可能になってからこのサービスを起動する
  • Type=simple: ExecStart で起動したプロセスそのものを systemd が監督する方式。手動で daemonize するプログラムを使う場合は Type=forking を検討する
  • Restart=on-failure: プロセスが異常終了した場合に自動で再起動する
  • StandardOutput=journal / StandardError=journal: 標準出力・エラーを journald に転送する。journalctl -u simple_daemon で確認できる
  • WantedBy=multi-user.target: 通常のマルチユーザー起動時にこのサービスを有効にする

その他の詳細はこちらに書いていそう。

余談だが、Docker のコア実装となる moby のリポジトリにも Unit ファイルを見つけた

###3. コンテナ起動と確認

docker build -t ubuntu-systemd-sandbox .
docker run --name ubuntu-systemd-sandbox --rm -d \
  --privileged \
  --cgroupns=host \
  -v /sys/fs/cgroup:/sys/fs/cgroup:rw \
  ubuntu-systemd-sandbox:latest
  • --privileged: コンテナに特権を付与する。systemd がカーネル機能(cgroup, mount 等)を利用するために必要
  • --cgroupns=host: ホストの cgroup 名前空間を共有する
  • -v /sys/fs/cgroup:/sys/fs/cgroup:rw: cgroup ファイルシステムをコンテナにマウントする

コンテナ内で以下を実行してデーモンが起動しているかを確認する。

systemctl status simple_daemon
# ● simple_daemon.service - Simple Python Daemon (hands-on)
#      Loaded: loaded (/etc/systemd/system/simple_daemon.service; enabled; vendor preset: enabled)
#      Active: active (running) since Thu 2026-03-05 09:57:06 UTC; 32s ago
#    Main PID: 45 (python3)
#       Tasks: 1 (limit: 2298)
#      Memory: 2.8M
#         CPU: 23ms
#      CGroup: /docker/afe0a22201f9c1d6ecaab2a2fcf0213afbf61c0ba1413766a90f29d31c85048c/system.slice/simple_daemon.service
#              └─45 /usr/bin/python3 /opt/simple-daemon/simple_daemon.py
#
# Mar 05 09:57:06 afe0a22201f9 systemd[1]: Started Simple Python Daemon (hands-on).

journalctl -u simple_daemon | head -n 5
# Mar 05 09:57:06 afe0a22201f9 systemd[1]: Started Simple Python Daemon (hands-on).
# Mar 05 10:17:33 afe0a22201f9 python3[45]: [PID 45] Starting process
# Mar 05 10:17:33 afe0a22201f9 python3[45]: [PID 45] Working...
# Mar 05 10:17:33 afe0a22201f9 python3[45]: [PID 45] Working...
# Mar 05 10:17:33 afe0a22201f9 python3[45]: [PID 45] Working...

ハンズオンはこれで以上。