はじめに
Raspberry Pi と Raspberry Pi Pico 2 W を使って、LAN 内で完結する IoT アプリケーションを作っているのですが、OTA アップデートの機構を追加したくなりました。
↑ のような構成をとっていて、インターネットに繋げたタイミングで、GitHub Release に上げている新しいバージョンのファイルを取りに行き、アップデートするようにしました。
OTA アップデートの対象は、
- Raspberry Pi Pico 2 W の firmware
- nginx から配信しているフロントエンド用の静的ファイル
- Raspberry Pi で動かしている Flask アプリケーション
の 3 つですが、
今回の対象は Raspberry Pi で動かしている systemd で管理された Flask アプリケーションです。
- GitHub Release から新しいバージョンのファイルをダウンロード
- 動作中の Flask アプリケーションの停止(
systemctl stop) - 新しいバージョンに入れ替え
- 再起動
という過程で、フロントエンドからのリクエストをトリガーに、Flask アプリケーションが内部でサブプロセスを起動して、アップデートを実行するようにしました。
2 の「動作中の Flask アプリケーションの停止」をサブプロセス内から実行しようとすると、親のプロセスが停止したタイミングで、サブプロセスも死んでしまうので、アップデートがうまく行きません。
これを systemd-run を使って、サブプロセスを別のサービスとして動かすことによって解決したので、それについて書こうと思います。
課題
上で説明した、systemd で管理しているアプリケーションからサブプロセスを作成する簡易的なコードは以下です
-
python スクリプト(
/opt/demo/parent.py)import subprocess import time def main() -> None: subprocess.Popen(["sleep", "3600"]) while True: time.sleep(60) if __name__ == "__main__": main() -
unit ファイル
[Unit] Description=systemd-managed demo parent After=network.target [Service] Type=simple ExecStart=python3 /opt/demo/parent.py StandardOutput=journal Restart=no
これを起動した際、プロセスは以下の状態になります。
$ sudo systemctl status demo.service
● demo.service - systemd-managed demo parent
Loaded: loaded (/etc/systemd/system/demo.service; static)
Active: active (running) since Sun 2026-05-31 14:16:07 JST; 6min ago
Main PID: 16787 (python3)
Tasks: 2 (limit: 38003)
Memory: 5.0M (peak: 5.4M)
CPU: 26ms
CGroup: /system.slice/demo.service
├─16787 python3 /opt/demo/parent.py
└─16788 sleep 3600
$ pstree -p -s 16788
systemd(1)───python3(16787)───sleep(16788)
$ systemd-cgls -u demo.service
Unit demo.service (/system.slice/demo.service):
├─16787 python3 /opt/demo/parent.py
└─16788 sleep 3600
$ ps -o pid,sid,comm -p 16787,16788
PID SID COMMAND
16787 16787 python3
16788 16787 sleep
- 親プロセスとサブプロセスのセッション(SID)が同じ
- 親プロセスとサブプロセスの cgroup が同じ
の状態です。
SID の共有
SID を共有しているセッションリーダー(親プロセス)が閉じられた場合、
同一セッションのプロセスに対して SIGHUP が送られることになるので、デフォルトの設定だとサブプロセスも同様に終了します。
ただし今回は親プロセスを systemd で管理しているので、この死に方は発生しないです。
Python の subprocess.Popen では、start_new_session を有効化することによって、別のセッションでプロセスを起動することができます。
import subprocess
import time
def main() -> None:
- subprocess.Popen(["sleep", "3600"])
+ subprocess.Popen(["sleep", "3600"], start_new_session=True)
while True:
time.sleep(60)
if __name__ == "__main__":
main()
$ sudo systemctl restart demo.service
$ sudo systemctl status demo.service
● demo.service - systemd-managed demo parent
Loaded: loaded (/etc/systemd/system/demo.service; static)
Active: active (running) since Sun 2026-05-31 15:19:31 JST; 2s ago
Main PID: 20489 (python3)
Tasks: 2 (limit: 38003)
Memory: 5.4M (peak: 5.5M)
CPU: 30ms
CGroup: /system.slice/demo.service
├─20489 python3 /opt/demo/parent.py
└─20491 sleep 3600
$ ps -o pid,sid,comm -p 20489,20491
PID SID COMMAND
20489 20489 python3
20491 20491 sleep
cgroup の共有
対象の unit と cgroup を共有しているプロセスは、unit が終了した時点で、デフォルトでは親プロセスもろともすべて終了します。
KillMode=
Specifies how processes of this unit shall be killed. One of control-group, mixed, process, none.
If set to control-group, all remaining processes in the control group of this unit will be killed on unit stop (for services: after the stop command is executed, as configured with ExecStop=). If set to mixed, the SIGTERM signal (see below) is sent to the main process while the subsequent SIGKILL signal (see below) is sent to all remaining processes of the unit's control group. If set to process, only the main process itself is killed (not recommended!). If set to none, no process is killed (strongly recommended against!). In this case, only the stop command will be executed on unit stop, but no process will be killed otherwise. Processes remaining alive after stop are left in their control group and the control group continues to exist after stop unless empty.
子プロセスが死なないような他のオプションもありますが、非推奨になっています。
Note that it is not recommended to set KillMode= to process or even none, as this allows processes to escape the service manager's lifecycle and resource management, and to remain running even while their service is considered stopped and is assumed to not consume any resources.
systemd-run を使って解決する
systemd-run を使うと、実行したいコマンドを現在のサービス(cgroup)から切り離して、systemd 管理下で、一時的な別サービス(transient service)として独立して実行することができます。
これにより、親プロセスが停止しても、影響を受けずに処理を続行させることが可能になります。
import subprocess
import time
def main() -> None:
- subprocess.Popen(["sleep", "3600"])
+ subprocess.Popen([
+ "systemd-run",
+ "--unit=demo-worker",
+ "--description=Transient Sleep Worker",
+ "sleep", "3600"
+ ])
while True:
time.sleep(60)
if __name__ == "__main__":
main()
$ sudo systemctl restart demo.service
$ sudo systemctl status demo.service
● demo.service - systemd-managed demo parent
Loaded: loaded (/etc/systemd/system/demo.service; static)
Active: active (running) since Sun 2026-05-31 16:36:18 JST; 3s ago
Main PID: 24406 (python3)
Tasks: 2 (limit: 38003)
Memory: 5.2M (peak: 6.5M)
CPU: 24ms
CGroup: /system.slice/demo.service
└─24406 python3 /opt/demo/parent.py
$ sudo systemctl status demo-worker.service
● demo-worker.service - Transient Sleep Worker
Loaded: loaded (/run/systemd/transient/demo-worker.service; transient)
Transient: yes
Active: active (running) since Sun 2026-05-31 16:36:18 JST; 1min 57s ago
Main PID: 24408 (sleep)
Tasks: 1 (limit: 38003)
Memory: 184.0K (peak: 512.0K)
CPU: 2ms
CGroup: /system.slice/demo-worker.service
└─24408 /usr/bin/sleep 3600
$ systemd-cgls -u demo.service
Unit demo.service (/system.slice/demo.service):
└─24406 python3 /opt/demo/parent.py
$ systemd-cgls -u demo-worker.service
Unit demo-worker.service (/system.slice/demo-worker.service):
└─24408 /usr/bin/sleep 3600
cgroup を共有しない状態になっています。
なので、元のプロセスを停止しても、サブプロセスとして起動したプロセスに影響が出ないようになります。
$ sudo systemctl stop demo.service
$ sudo systemctl status demo-worker.service
● demo-worker.service - Transient Sleep Worker
Loaded: loaded (/run/systemd/transient/demo-worker.service; transient)
Transient: yes
Active: active (running) since Sun 2026-05-31 16:36:18 JST; 6min ago
Main PID: 24408 (sleep)
Tasks: 1 (limit: 38003)
Memory: 184.0K (peak: 512.0K)
CPU: 2ms
CGroup: /system.slice/demo-worker.service
└─24408 /usr/bin/sleep 3600
参考