systemdのType= simple、forking、notifyの違い
systemdのunitがactiveなのにdaemonが終了する原因を解説します。Type= simple、exec、forking、oneshot、notifyの違いと、実際のmain PIDを特定する方法が分かります。
systemd はプロセスが終了しているのに、なぜ unit を active と報告するのか
systemd の service unit は、systemd が main process として扱う 1 つのプロセスが存続している間、active の状態を維持します。どのプロセスを対象にするかは、[Service] セクションの Type= で決まります。誤った値を指定すると、systemd は shell wrapper や短時間で終了する親プロセスを監視し、対象の daemon は同じ unit 内で終了してしまいます。unit は、監視対象として指定されたプロセスについて正しい状態を報告しています。
restart policy を変更しても、この問題は解決しません。Restart= は main process の終了時に動作するため、main PID(process identifier)が実行中のプロセスに属している限り、Restart=always は実行されません。まず Type= を修正してください。main process が実際に終了した後の systemd の動作は別の判断であり、Restart= と RestartSec= のガイドで説明しています。
Type= が実際に決めること
すべての Type= の値は、2 つの質問に同時に答えます。この unit を systemd が起動済みと見なせるのはいつか、そしてメインプロセスはどれか、という質問です。
1 つ目の答えは起動順序を制御します。After= でこの unit を指定する unit は、systemd がこの unit を起動済みと見なすまで待機します。「起動済み」と早すぎる段階で報告する Type= では、サービスが要求に応答できる前に依存する unit が実行されます。
2 つ目の答えは監視を制御します。systemd は、unit が生成するすべてのプロセスを cgroup(control group)に配置します。cgroup は、プロセスをまとめて制限および終了できるようにするカーネル機能です。systemctl stop がクリーンアップを行う仕組みも cgroup です。KillMode= のデフォルトは control-group であるため、unit を停止すると、その中にあるすべてのプロセスにシグナルが送られます。メイン PID は、これより限定的なものです。メイン PID は、終了すると unit を終了させ、その終了ステータスが unit の結果になる単一のプロセスを指します。cgroup をメイン PID と同じものとして扱うと、混乱が始まります。
Type=simple はバイナリの実行前に起動済みと報告する
Type=simple は、ExecStart= が設定されていて、Type= と BusName= のどちらも存在しない場合のデフォルトです。systemd はプロセスを作成すると、直ちに unit を起動済みとみなし、そのプロセスをメイン PID として扱います。後続の unit は、サービスバイナリが実行される前に直ちに開始されます。
この点が、よくある意外な動作の原因です。ExecStart= のパスに誤字があっても、開始ジョブは成功します。実行に失敗した時点で、少し遅れて失敗が発生します。systemd はこのケースを終了コード 203 で記録します。systemd の独自の表では、このコードを EXEC と呼び、サービスバイナリの実行に失敗したことを示します。したがって、systemctl start がエラーなしで戻っても、バイナリが存在するとは限りません。
フォアグラウンドで動作し、バックグラウンドへ移行しないプログラムには simple を使用します。現在の大半のデーモンや、自分で作成するほとんどすべてのプログラムが該当します。
Type=exec はプログラムが実際に起動するまで待機します
Type=exec は simple に手順を 1 つ追加したものです。systemd は、fork とバイナリの実行が両方とも成功した後にのみ、unit が起動したとみなします。バイナリが存在しない場合や、解決できない User= がある場合は、起動ジョブ自体が失敗します。成功したと報告した直後に、ひそかに失敗することはありません。
Type=exec は systemd 240 で追加されたため、現在のすべてのサーバー向けディストリビューションで利用できます。2026 年 8 月時点で、Ubuntu 24.04 には systemd 255 が、Debian 13 には systemd 257 が搭載されています。使用中のバージョンは systemctl --version で確認します。
起動時に同期処理が 1 つ増えることがコストです。一方、systemctl start から正確な終了ステータスを取得できます。フォアグラウンドプログラムでは、simple より exec を優先してください。
Type=forking と、メイン PID が失われる仕組み
Type=forking は、ExecStart= のプロセスが子プロセスを fork してから意図的に終了することを systemd に伝えます。systemd は最初のプロセスが終了するまで待機し、その後で unit が起動済みと判断します。残った子プロセスが daemon です。この方式は SysV の時代に由来します。当時は init script が戻った後に daemon を監視する仕組みがなく、実行中のプロセスを記録する手段は PID file だけでした。この制約は、systemd が init script に取って代わった理由の中心にあります。
問題はプロセスの識別です。systemd が起動したプロセスはすでに終了しているため、systemd は残ったプロセスのうち、どれがメインプロセスかを判断する必要があります。PIDFile= には daemon が書き込む file を指定します。通常は /run 配下の path です。systemd はそこから PID を読み取ります。また、その file の PID がすでにこの service に属しているプロセスを指しているかも確認します。そのため、無関係なプロセスを指す stale file は信頼されず、拒否されます。
PIDFile= がない場合は GuessMainPID= が適用され、既定値は yes です。この推測が信頼できるのは、service が単一のプロセスに落ち着く場合だけです。manual には制限が明記されています。daemon が複数のプロセスで構成される場合、推測を誤る可能性があり、障害検出が機能しなくなります。unit のメイン PID が 0 になることもあります。これは、systemd が監視すべきプロセスをまったく認識していないことを意味します。
fork する多くの daemon には、foreground で動作させる switch もあります。その switch を Type=exec とともに使用し、PIDFile= の行を削除してください。構成要素が少ないほど、PID を失う可能性も減ります。
終了する処理には Type=oneshot
Type=oneshotは、プロセスが実行されて終了することを想定しています。systemd は、プロセスが終了した後に初めてユニットを started として扱います。そのため、別のユニットが完了を待つ必要がある処理には oneshot が適しています。ユニットで Type= と ExecStart= のどちらも指定しない場合も、暗黙のデフォルトはこれです。
oneshotには、固有の動作が2つあります。複数の ExecStart= 行を指定できる唯一のタイプであり、それらの行は順番に実行されます。また、起動タイムアウトはデフォルトで無効です。そのため、ハングした oneshot は、TimeoutStartSec= を自分で設定しない限り、永久に待機します。
プロセスが終了すると、ユニットは inactive に戻ります。RemainAfterExit=yesを指定すると、プロセスがまったく実行されていなくても active の状態を維持します。これは、このページの冒頭にある症状を意図的に実現する形です。処理の目的が何かを実行し続けることではなく、状態を残すことなら正しい動作です。たとえば、ファイアウォールのルールセットを読み込む処理や、コンテナスタックを起動する処理が該当します。これは、再起動後に復帰する Docker Compose スタックの基盤となるパターンでもあります。この場合、ユニットが compose コマンドを実行して終了し、起動したコンテナがユニットより長く稼働するため、ユニットは active のままになります。oneshotユニットは、スケジュールによって実行される処理にも使用されます。これは、cron ではなく systemd timer でジョブを実行する方法のもう一つの側面です。
Type=notify により、サービスは準備完了のタイミングを通知できます
Type=notify により、判断をサービス側に移します。systemd は、プロセスが NOTIFY_SOCKET 環境変数で受け取ったパスの Unix ソケット経由で READY=1 を送信するまで、起動ジョブを完了させません。C インターフェースは sd_notify(3) で、多くのサーバーがすでに対応しています。
これは、「起動済みか」という問いに対する正確な判定です。simple と exec は、サービスが設定を読み込んだり、待ち受けソケットを開いたりする前に起動済みと報告します。そのため、依存する unit が早く起動し、最初の接続に失敗することがあります。notify は、サービス自身が準備完了を通知した時点で起動済みと報告します。
systemd は、そのメッセージをメインプロセスからのものとしてのみ受け付けます。これが NotifyAccess=main の意味であり、Type=notify はこれを暗黙に指定します。メッセージが子プロセスやヘルパーから送信される場合は、NotifyAccess=all を設定します。シェルスクリプトから systemd-notify --ready を呼び出すこともできますが、これは別の短時間プロセスとして実行されます。そのため NotifyAccess=all が必要であり、送信元がすでに終了していると、systemd がメッセージの送信元を特定できないことがあります。プロトコルを自ら実装するサービスのほうが信頼性は高くなります。
関連する設定として、次の 2 つも確認しておくとよいでしょう。systemd 253 以降で利用できる Type=notify-reload は、同じハンドシェイクをリロードにも適用します。そのため systemctl reload は、シグナルの送信時ではなく、サービスがリロード完了を通知した時点で戻ります。WatchdogSec= は、通知を行うサービスに一定間隔で keep-alive メッセージを送信するよう要求します。systemd は期限内にメッセージを受信できない場合、失敗として扱います。
Type=dbus と Type=idle
Type=dbus は、system と desktop のサービスが相互通信に使用するメッセージバスである D-Bus 上で、サービスが名前を取得するまで待機します。BusName= が必要で、BusName= を設定するとデフォルトになります。実際にバス名を登録するサービスにのみ使用してください。
Type=idle は simple と同様に動作しますが、キューに入ったジョブのディスパッチが完了するまでプログラムの実行を遅延させます。待機時間の上限は 5 秒です。これは、ブート時のコンソール出力がステータスメッセージと混在しないようにするための機能です。順序制御には使用せず、通常のサービスにも設定しないでください。
ラッパースクリプトによって systemd が誤った PID を監視し続ける理由
次の構成が、元の症状を引き起こします。
[Service]
Type=simple
ExecStart=/opt/app/run.sh#!/bin/bash
export APP_ENV=production
/opt/app/bin/server --config /etc/app.yaml &
/opt/app/bin/exporter --port 9101systemd は shell をメイン PID として記録します。shell は exporter をフォアグラウンドで実行している間も生存します。server が終了しても shell はそれを検知しないため、メイン PID は生存したままです。unit は引き続き active であり、Restart= が処理すべき対象もありません。どちらのプロセスも終始 unit の cgroup に属しているため、systemctl stop によるクリーンアップは正常に機能します。壊れているのは監視であり、クリーンアップではありません。
修正方法は、unit が実際に実行する長時間稼働プロセスの数によって異なります。
1 つの場合は、shell をそのプロセスに置き換えます。
#!/bin/bash
export APP_ENV=production
exec /opt/app/bin/server --config /etc/app.yamlexec は shell を指定されたプログラムに置き換え、同じ PID を維持します。そのため、systemd が記録した PID は daemon の PID になります。さらによい方法は、wrapper 自体を削除することです。Environment= と EnvironmentFile= で変数を渡し、ExecStartPre= でセットアップ処理を実行すれば、systemd は daemon を直接起動でき、構造上その PID を把握できます。
2 つの場合は、unit を単一の PID で表すことはできません。2 つの unit に分割し、After= と Wants= で起動順序を指定します。1 プロセスにつき 1 unit とする構成は、systemd が適切に監視できる方法です。また、各プロセスに個別の再起動動作を持たせる唯一の方法でもあります。
ExitType=cgroup で変わること
ExitType= は systemd 250 で追加されました。デフォルトは main です。メインプロセスが終了すると、unit は停止したと見なされます。ExitType=cgroup を指定すると、cgroup 内にプロセスが1つでも存在する限り、unit は実行中と見なされます。
[Service]
Type=simple
ExitType=cgroup
ExecStart=/opt/app/launcherこれは特定の問題を解決します。実際の処理を起動して終了するランチャーは、ExitType=main の場合、systemd に unit が停止したと判断させ、残ったプロセスを強制終了させます。ExitType=cgroup では、unit はプロセスグループ全体に従います。
解決できない問題も明確にしておく必要があります。ExitType=cgroup はプロセスが少なくとも1つ動作している間、unit を active に保ちます。そのため、2つの daemon を保持する unit は、一方が終了しても active のままです。ランチャーのケースは解決できます。ただし、1つの unit が複数の独立したプロセスを監視する supervisor になるわけではありません。ExitType= は Type=oneshot とも併用できません。
リソース使用量の計上先も cgroup です。そのため、MemoryMax= や CPUQuota= などの制限は、unit が起動したすべてのプロセスに適用されます。Type= がメイン PID について何を示すかには左右されません。この機能の詳細は、systemd で service のメモリと CPU 使用量を制限するで説明しています。
systemd が実際に監視しているプロセスの確認方法
デバッグ対象の unit で、次の手順を順番に実行します。まず systemd が読み込んだ内容を確認し、次に systemd が追跡している内容を確認します。その後、プロセステーブルと比較します。
systemctl cat app.service
systemctl show -p Type,ExitType,GuessMainPID,PIDFile,MainPID,RemainAfterExit app.servicesystemctl cat は、unit に適用されるすべての drop-in と unit file をまとめて表示します。編集したはずのファイルではなく、systemd が読み込んだ内容を確認できます。systemctl show は、明示的に記述していないデフォルト値を含む、実効値を表示します。次に進む前に、MainPID の値を確認してください。
systemd-cgls --unit=app.service
ps -o pid,ppid,stat,etime,args -p "$(systemctl show -p MainPID --value app.service)"systemd-cgls は、unit の cgroup に属するすべてのプロセスを一覧表示します。ps の行には、systemd が監視する単一のプロセスが示されます。2 つを合わせて確認してください。MainPID が 0 の場合、systemd が監視するプロセスはありません。cgroup にデーモンも存在する一方で、MainPID が shell を示している場合は、前述のラッパー構成です。想定より多くのプロセスが cgroup に存在する場合は、ランチャーまたは fork するデーモンが関係しています。
systemctl status app.service
journalctl -u app.service -bsystemctl status は、状態行と cgroup ツリーをまとめて表示します。そのため、多くの場合に両方の疑問を同時に解決できます。journalctl -u を -b で今回のブートに限定すると、systemd が unit について記録した起動・停止イベントと、認識した終了コードを確認できます。デーモンが journal ではなく独自のログファイルに書き込む場合は、そのファイルも確認してください。systemd が記録できるのは、systemd に到達した内容だけだからです。
Type= を変更した場合は、reload と restart を実行します。
systemd-analyze verify /etc/systemd/system/app.service
sudo systemctl daemon-reload
sudo systemctl restart app.servicesystemd-analyze verify はファイルを解析し、受け入れられない設定を報告します。daemon-reload により、systemd はディスク上の unit file を再読み込みします。変更した Type= は、すでに実行中の unit には適用されません。そのため、restart は必須です。
続いて、変更をテストします。systemd-cgls から、実際に確認したいプロセスの PID を取得して kill します。直後に systemctl is-active app.service を実行します。Type= が正しければ、unit は active 状態を離れます。active のままなら、systemd は別のプロセスを監視しています。
使用すべき systemd サービスの Type= はどれか
- フォアグラウンドで動作し続けるプログラム:
Type=exec。 - 準備完了通知に対応するプログラム:
Type=notify。リロードも確認する場合はnotify-reloadも使用します。 - バックグラウンドへ移行する必要があるデーモン:
PIDFile=と組み合わせたType=forking、またはType=execによるフォアグラウンド切り替えを使用します。 - 処理を実行して終了するスクリプト:
Type=oneshot。状態を残すことが目的だった場合はRemainAfterExit=yesも使用します。 - 子プロセスを実行したまま終了するランチャー:
ExitType=cgroupと組み合わせたType=simpleを使用します。
サードパーティー製デーモンに必要なものが分からない場合は、まずパッケージに含まれる unit ファイルを確認してください。ディストリビューションが提供する unit に対して systemctl cat を実行すると、上流が選択した Type= が表示されます。この選択は、あなた自身の設定よりも多くの人によって検証されています。
FAQ
systemd の unit がプロセスの終了後も active のままなのはなぜですか?
systemd がメインプロセスとして扱うプロセスが、まだ生きているためです。systemd は unit の cgroup 内にあるすべてのプロセスではなく、Type= に基づいてサービスごとに選択した 1 つの PID を監視します。Type=simple で起動したラッパースクリプトが、通常の原因です。シェルがメイン PID になるため、シェルがバックグラウンドで起動した daemon が終了しても、unit は active のままになります。systemctl show -p MainPID app.service を実行してから、systemd-cgls --unit=app.service で unit の cgroup を一覧表示し、2 つの結果を比較します。
Type=simple と Type=exec の違いは何ですか?
Type=simple では、systemd がプロセスを作成した時点で unit の起動が完了したとみなします。バイナリが実行される前の段階です。そのため、ExecStart= のパスが誤っていても、start job は成功した後に失敗します。Type=exec では、実行が成功するまで待機するため、その失敗は start job 自体で報告されます。どちらも同じプロセスをメイン PID として扱います。Type=exec には systemd 240 以降が必要です。
Type=forking でも PIDFile= は必要ですか?
daemon が PIDFile を書き込む場合は必要です。指定しないと、systemd は GuessMainPID= にフォールバックします。これは推測に基づく方法で、単一のプロセスに落ち着くサービスでしか確実に機能しません。推測が誤っている場合や不可能な場合、その unit では失敗の検出と自動再起動が機能しなくなります。PIDFile= には、daemon が書き込む正確なパスを指定します。通常は /run 配下です。
RemainAfterExit=yes はいつ使用すべきですか?
プロセスを実行し続けることではなく、システム状態を変更することが unit の目的である場合に使用します。ファイアウォールルールを読み込む、またはコンテナスタックを起動する Type=oneshot unit は、処理が完了すると終了します。RemainAfterExit=yes がない場合、unit は inactive になり、systemctl stop には停止対象がなく、ExecStop= クリーンアップを実行する方法もありません。RemainAfterExit=yes を設定すると、unit はプロセスがない状態でも active のままになります。この動作は、この用途では意図したものです。
Type= の変更には daemon-reload が必要ですか?
必要です。unit の再起動も行います。systemctl daemon-reload により、systemd はディスク上の unit ファイルを再読み込みします。ただし、実行中のインスタンスは起動時に使用した Type= を保持します。テスト前に sudo systemctl daemon-reload を実行し、その後 sudo systemctl restart app.service を実行してください。そうしないと、古い監視動作を確認することになります。