新人エンジニアは「デーモン」がわからない　~ 3. Pythonでデーモンを作るとどんな感じ？~

# はじめに

やっとPythonでデーモンを作るところまで来ました〜。
サクッと行ってみましょ〜。


# Pythonでデーモンを書く


✅ Pythonでデーモンを作る「要点」

|項目	|内容|
|-------|----|
|1. forkで親子分離|	プロセスを2つに分けて親を終了し、端末との関係を切る
|2. セッションの切り離し|	setsid() により新しいセッションを開始（独立）
|3. 標準出力・入力・エラーの切断|	/dev/null などにリダイレクトする
|4. 作業ディレクトリとumaskの変更|	安全な初期化（例：/に移動、umask=0など）
|5. 無限ループで処理待機|	常駐動作の実装
|6. PIDファイルやログ出力|	状態を確認しやすくする


## 手順と解説

### 最小構成のデーモン
pydaemon.py
```python
import os
import sys
import time

def daemonize():
    # 1回目のforkで親プロセスを終了
    if os.fork() > 0:
        sys.exit(0)

    # セッションを切り離す
    os.setsid()

    # 2回目のfork（セッションリーダーを回避）
    if os.fork() > 0:
        sys.exit(0)

    # カレントディレクトリをルートに変更
    os.chdir("/")
    os.umask(0)

    # 標準入出力を /dev/null に向ける
    sys.stdout.flush()
    sys.stderr.flush()
    with open('/dev/null', 'rb', 0) as f:
        os.dup2(f.fileno(), sys.stdin.fileno())
    with open('/dev/null', 'ab', 0) as f:
        os.dup2(f.fileno(), sys.stdout.fileno())
        os.dup2(f.fileno(), sys.stderr.fileno())

def main():
    # 無限ループ
    while True:
        with open("/tmp/pydaemon.log", "a") as f:
            f.write(f"{time.ctime()}: I'm alive!\n")
        time.sleep(60)

if __name__ == "__main__":
    daemonize()
    main()
```


#### 解説

|ステップ|処理内容|解説|
|--------|------|------|
os.fork()|子プロセスを作る|親を終了して端末との関係を切る
os.setsid()|新しいセッションを作る|親セッションから完全に切り離す
2回目の os.fork()|再び親を終了|セッションリーダーをやめるため（安全策）
os.chdir("/")|ルートに移動|カレントディレクトリをアンマウントから守る
os.umask(0)|umaskの初期化|セキュアなファイル作成を保証
os.dup2|標準入出力の切断|ターミナルがなくても動けるように
main()|無限ループで動作|デーモンとして常駐する処理


#### 実行方法

`python3 pydaemon.py`
→ 端末には何も出ませんが、/tmp/pydaemon.log に1分ごとにログが記録されていきます。


#### 補足：停止方法

`ps aux | grep pydaemon.py`
`kill <PID>`

あるいは pidファイル を作るようにすれば `kill $(cat /var/run/pydaemon.pid)` のように扱えます。


### まとめ：Pythonでデーモンを作るには

|必須の処理|理由|
|--------|----|
fork() → setsid() → fork()| 完全な独立プロセス化
chdir("/"), umask(0)| 安全な実行環境に初期化
/dev/null への入出力切断| 端末切断時にも落ちないように
無限ループで処理継続| 常駐プロセスとしての基本動作







# PIDファイルを出力して管理できるようにする

PIDファイルを出力して `kill $(cat ～.pid)` で管理できるようにする方法について。

目標：
- デーモンが起動したときに 自分のプロセスID（PID）をファイルに書き出す
- 終了するときに自動でそのPIDファイルを削除する
- `kill $(cat /var/run/pydaemon.pid)` で簡単に停止できる


改良版：PIDファイル付き pydaemon.py

```python
import os
import sys
import time
import atexit

# PIDファイルのパス（通常は /var/run）
PIDFILE = "/tmp/pydaemon.pid"

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

    os.setsid()

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

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

    sys.stdout.flush()
    sys.stderr.flush()
    with open('/dev/null', 'rb', 0) as f:
        os.dup2(f.fileno(), sys.stdin.fileno())
    with open('/dev/null', 'ab', 0) as f:
        os.dup2(f.fileno(), sys.stdout.fileno())
        os.dup2(f.fileno(), sys.stderr.fileno())

    # PIDファイルの作成
    with open(PIDFILE, 'w') as f:
        f.write(str(os.getpid()) + '\n')

    # 終了時にPIDファイルを削除する
    atexit.register(lambda: os.remove(PIDFILE))

def main():
    while True:
        with open("/tmp/pydaemon.log", "a") as f:
            f.write(f"{time.ctime()}: I'm alive!\n")
        time.sleep(60)

if __name__ == "__main__":
    daemonize()
    main()
```


実行手順

`python3 pydaemon.py`
→ /tmp/pydaemon.pid にプロセスIDが書き込まれます。


停止方法

`kill $(cat /tmp/pydaemon.pid)`

→ 終了すると、自動的に .pid ファイルも削除されます（atexit のおかげ）。


補足：パーミッションの注意
- 実際の運用では PIDファイルは /var/run/pydaemon.pid に置くのが慣習ですが、書き込みに root 権限が必要です。
- 一般ユーザーでテストする場合は /tmp/pydaemon.pid でOK。


まとめ

|機能|実現方法|
|---|-------|
PIDを保存する| with open(PIDFILE, 'w') で自プロセスIDを書き出す
終了時に削除| atexit.register() でクリーンに削除
kill管理| kill $(cat PIDFILE) で確実に終了できる







# systemd で本格的に運用してみよう！

ではいよいよ Pythonデーモンを systemd に登録して、Linuxのサービスとして起動(start)・停止(stop)・自動起動(enable)管理できるようにしましょう！

ゴール：
- Pythonデーモン（pydaemon.py）を systemd サービスとして登録
- `systemctl start pydaemon` で起動
- `systemctl enable pydaemon` で自動起動
- `systemctl stop pydaemon` で安全に停止


 ## ① スクリプトの準備

たとえば以下のように設置：

/home/youruser/pydaemon.py

忘れずに実行権限を付けておきます：

`chmod +x /home/youruser/pydaemon.py`


## ② systemd ユニットファイルの作成

`sudo nano /etc/systemd/system/pydaemon.service`

中身は以下のようにします：

```ini
[Unit]
Description=Python Daemon Example
After=network.target

[Service]
ExecStart=/usr/bin/python3 /home/youruser/pydaemon.py
Restart=always
User=youruser
Group=youruser

[Install]
WantedBy=multi-user.target
```

youruser は自分のユーザー名に置き換えてください
ExecStart は python3 のフルパス（ `which python3` で確認）


## ③ systemd に反映

`sudo systemctl daemon-reload`


## ④ 起動してみる！

`sudo systemctl start pydaemon.service`

状態確認：

`sudo systemctl status pydaemon.service`

ログの確認（Python側がログ出力している場合）：

`cat /tmp/pydaemon.log`

- 自動起動設定（再起動後も常駐）

`sudo systemctl enable pydaemon.service`


- 停止方法

`sudo systemctl stop pydaemon.service`


### 補足：ログを journal に流したいとき

Pythonスクリプト内の標準出力／標準エラーを journald に直接出したいなら、以下のように systemd 側で設定します：
```ini
StandardOutput=journal
StandardError=journal
```
ただし、スクリプト側が /dev/null に dup2 していると journal に流れないので、そちらの調整が必要です。

## まとめ

|操作|コマンド|
|---|-------|
サービス起動	|sudo systemctl start pydaemon
停止	|sudo systemctl stop pydaemon
自動起動ON	|sudo systemctl enable pydaemon
状態確認	|sudo systemctl status pydaemon