SSD Nodes Learn 🎉 VPS $5.50/月〜
ガイド Matt Connor著者 Matt Connor

systemdのType=simpleとforkingの違いとPID確認方法

systemdのunitがactiveなのにデーモンが消える原因を解説します。Type=simple、exec、forking、oneshot、notifyの違いと、実際のメインPIDを確認する方法がわかります。

systemd はプロセスが終了したのに、なぜ unit を active と報告するのですか

systemd のサービス unit は、systemd がメインプロセスとみなす 1 つのプロセスが生存している間、active の状態を維持します。[Service] セクションの Type= により、そのプロセスが決まります。誤った値を設定すると、systemd はシェルラッパーや短時間で終了する親プロセスを監視します。その間に、対象のデーモンが同じ unit 内で終了してしまうことがあります。unit は、監視対象として指定されたプロセスについては正しい状態を報告しています。

この場合、再起動ポリシーを変更しても解決しません。メインプロセスが終了したときに Restart= が動作するため、メイン PID(プロセス識別子)がまだ実行中のプロセスを指している間は、Restart=always は実行されません。まず Type= を修正してください。メインプロセスが実際に終了した後に systemd が行う処理は別の判断であり、Restart= と RestartSec= のガイドで説明しています。

Type=が実際に決めること

すべての Type= の値は、2 つの問いに同時に答えます。systemd はこの unit がいつ起動済みと判断できるのか。また、どのプロセスをメインプロセスとするのかです。

1 つ目の答えは、起動順序を制御します。After= でこの unit を指定する unit は、systemd がこの unit を起動済みと判断するまで待機します。「started」と早すぎる時点で報告する 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 はプロセスを作成すると、ユニットを直ちに起動済みとみなし、そのプロセスをメイン PID として扱います。後続のユニットは、サービスバイナリが実行される前に直ちに開始されます。

この点が、よくある意外な動作の原因です。ExecStart= のパスに誤字があっても、開始ジョブ自体は成功します。その直後、実行に失敗した時点でエラーになります。systemd はこのケースを終了コード 203 として記録します。systemd の表では、このコードを EXEC と呼び、サービスバイナリの実行に失敗したことを示すと定義しています。したがって、systemctl start がエラーなしで戻っても、バイナリが存在することの証明にはなりません。

フォアグラウンドで動作し、自身をバックグラウンドへ移行しないプログラムには simple を使用します。これは、現在のほとんどのデーモンや、自分で作成するほぼすべてのプログラムに当てはまります。

Type=exec はプログラムが実際に起動するまで待機します

Type=execsimple にもう 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 です。

問題はプロセスの識別です。systemd が起動したプロセスは終了しているため、systemd は残ったプロセスのうち、どれがメインプロセスかを判断する必要があります。PIDFile= には daemon が書き込むファイルを指定します。通常は /run 配下のパスです。systemd はそのファイルから PID を読み取ります。また、そのファイルの PID がすでにこのサービスに属しているプロセスを指していることも確認します。そのため、関係のないプロセスを指す古いファイルは信頼されず、拒否されます。

PIDFile= がない場合は GuessMainPID= が適用され、デフォルトは yes です。この推測が信頼できるのは、サービスが単一のプロセスに落ち着く場合だけです。マニュアルには制限が明記されています。daemon が複数のプロセスで構成されている場合、推測を誤る可能性があり、障害検知が機能しなくなります。unit のメイン PID が 0 になることもあります。これは、systemd が監視できるプロセスがまったくないことを意味します。

fork する daemon の多くには、フォアグラウンドで実行し続けるスイッチもあります。そのスイッチを Type=exec と組み合わせ、PIDFile= の行を削除してください。構成要素が減るほど、PID を失う可能性も減ります。

完了する処理には Type=oneshot

Type=oneshotは、プロセスが実行されて終了することを前提としています。systemd は、起動したユニットが終了した後でのみ、そのユニットを起動済みとして扱います。そのため、別のユニットが完了を待つ必要がある処理には 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 を送信するまで、start ジョブを完了させません。C インターフェースは sd_notify(3) で、多くのサーバーがすでに対応しています。

これは、「起動済みか」という問いに対する正確な判定です。simpleexec は、サービスが設定を読み込んだり、待ち受けソケットを開いたりする前に起動済みと報告します。そのため、依存ユニットが早く起動し、最初の接続に失敗することがあります。notify は、サービス自身が準備完了を通知した時点で起動済みと報告します。

systemd は、メインプロセスからの通知だけを受け付けます。これが NotifyAccess=main の意味であり、Type=notify はこれを暗黙に指定します。子プロセスやヘルパーから通知する場合は、NotifyAccess=all を設定します。シェルスクリプトから systemd-notify --ready を呼び出すこともできますが、これは別の短時間プロセスとして実行されます。そのため NotifyAccess=all が必要です。また、送信元がすでに終了している場合、systemd はその通知をどのプロセスが送信したか特定できないことがあります。プロトコルをサービス自身が実装する方が信頼性は高くなります。

関連する設定として、2 つ覚えておくと便利なものがあります。systemd 253 以降で利用できる Type=notify-reload は、同じハンドシェイクを reload にも拡張します。そのため systemctl reload は、シグナルの送信時ではなく、サービスが reload の完了を通知した時点で戻ります。WatchdogSec= は、通知に対応したサービスに一定間隔で keep-alive メッセージを送信させます。systemd は期限内にメッセージを受信できない場合、失敗として扱います。

Type=dbus と Type=idle

Type=dbus は、システムサービスとデスクトップサービスが相互通信に使用するメッセージバスである D-Bus 上で、サービスが名前を取得するまで待機します。BusName= が必要で、BusName= を設定するとデフォルトになります。実際にバス名を登録するサービスにのみ使用してください。

Type=idlesimple と同様に動作しますが、キューに入ったジョブがディスパッチされるまでプログラムの実行を遅延させます。待機時間の上限は 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 9101

systemd はシェルをメイン PID として記録します。シェルは exporter をフォアグラウンドで実行している間、動作し続けます。server が終了してもシェルはそれを認識しないため、メイン PID は生存したままです。unit は active のままで、Restart= が処理すべき対象もありません。どちらのプロセスも常に unit の cgroup 内にあるため、systemctl stop によるクリーンアップは正しく実行されます。壊れているのは監視であり、クリーンアップではありません。

修正方法は、unit が実際に持つ長時間実行プロセスの数によって異なります。

1 つだけなら、シェルをそのプロセスに置き換えます。

#!/bin/bash
export APP_ENV=production
exec /opt/app/bin/server --config /etc/app.yaml

exec はシェルを指定したプログラムに置き換え、同じ PID を維持します。そのため、systemd が記録した PID はデーモンの PID になります。さらによい方法は、ラッパー自体を削除することです。Environment=EnvironmentFile= で変数を渡し、ExecStartPre= でセットアップ処理を実行すれば、systemd はデーモンを直接起動でき、最初からその PID を把握できます。

2 つある場合、unit を表す単一の PID はありません。2 つの unit に分割し、After=Wants= で起動順序を指定します。プロセスごとに 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 をアクティブな状態にします。そのため、2つのデーモンを保持する unit は、一方が終了してもアクティブなままです。ランチャーの問題は解決します。ただし、1つの unit が複数の独立したプロセスを監視する supervisor になるわけではありません。ExitType=Type=oneshot は同時に使用できません。

リソース使用量の計上も cgroup 単位で行われます。そのため、MemoryMax=CPUQuota= などの制限は、Type= がメイン PID について何を示しているかに関係なく、unit が起動したすべてのプロセスに適用されます。この点については、systemd でサービスのメモリ使用量と CPU 使用量を制限するで説明しています。

systemd が実際に監視しているプロセスを確認する方法

デバッグしている unit に対して、次の手順を順番に実行します。まず systemd が読み込んだ内容を確認し、次に追跡している内容を確認してから、プロセステーブルと比較します。

systemctl cat app.service
systemctl show -p Type,ExitType,GuessMainPID,PIDFile,MainPID,RemainAfterExit app.service

systemctl cat は、unit に適用されるすべての drop-in と unit ファイルをまとめて表示します。編集した覚えのあるファイルではなく、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 が監視するプロセスはありません。MainPID の結果が shell で、同じ cgroup にデーモンも含まれている場合は、前述のラッパーのケースです。想定より多くのプロセスが cgroup に含まれている場合は、ランチャーまたは fork するデーモンが関与しています。

systemctl status app.service
journalctl -u app.service -b

systemctl 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.service

systemd-analyze verify はファイルを解析し、受け入れられない設定を報告します。daemon-reload により、systemd はディスク上の unit ファイルを再読み込みします。変更した Type= は実行中の unit には適用されないため、restart は必須です。

その後、変更をテストします。systemd-cgls から、実際に確認したいプロセスの PID を取得して kill します。直後に systemctl is-active app.service を実行してください。Type= が正しければ、unit は active 状態を離れます。active のままであれば、systemd は別のプロセスを監視しています。

使用すべき systemd サービスの Type= はどれか

  • フォアグラウンドで動作し続けるプログラム: Type=exec です。
  • readiness 通知に対応するプログラム: 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 になるため、シェルがバックグラウンドで起動したデーモンが終了しても、unit は active のままになります。systemctl show -p MainPID app.service を実行し、続いて systemd-cgls --unit=app.service で unit の cgroup を一覧表示して、両者を比較してください。

Type=simple と Type=exec の違いは何ですか?

Type=simple では、systemd がプロセスを作成した時点で unit の起動完了と見なします。バイナリが実行される前の段階です。そのため、ExecStart= のパスが誤っていても、start job は成功した後に失敗します。Type=exec では、実行に成功するまで待機するため、その失敗が start job 自体によって報告されます。どちらも同じプロセスをメイン PID として扱います。Type=exec には systemd 240 以降が必要です。

Type=forking でも PIDFile= は必要ですか?

デーモンが PID ファイルを書き込む場合は、必要です。指定しない場合、systemd は GuessMainPID= にフォールバックします。これは推測に基づく方法であり、単一のプロセスに落ち着くサービスでのみ信頼できます。推測が誤っているか不可能な場合、その unit では失敗検出と自動再起動が機能しなくなります。PIDFile= には、デーモンが書き込む正確なパスを指定してください。通常は /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 を実行してください。これを行わないと、古い監視動作を確認することになります。