SSD Nodes Learn Hosting plans →
指南 Matt Connor作者: Matt Connor · 已更新 2026-08-30

systemd 服務 Type 設定指南:simple, forking, notify

systemd 服務顯示 active 但 daemon 已消失?本文詳解 Type=simple, forking, notify 與 oneshot 的正確用法,教您如何鎖定正確的主 PID 並解決服務監控失效問題。

為什麼 systemd 在處理程序終止時仍回報單元為 active

systemd 服務單元只要其定義的主處理程序(main process)仍在執行,狀態就會維持 active,而 [Service] 區段中的 Type= 則決定了該處理程序為何。若設定錯誤,systemd 可能會誤將 shell 包裝器(wrapper)或短暫存在的父處理程序視為監控對象,導致您真正關心的 daemon 在單元內部終止時,systemd 卻毫無察覺。該單元回報的狀態,僅反映了它被指派監控的處理程序之真實情況。

修改重啟策略對此無濟於事。Restart= 僅在主處理程序退出時觸發,因此只要主 PID(處理程序識別碼)仍屬於某個執行中的程序,Restart=always 就不會執行。請務必先修正 Type=。至於主處理程序真正退出後 systemd 的後續行為,則屬於另一項設定,詳見 Restart= 與 RestartSec= 指南。

Type= 參數的具體作用為何

每個 Type= 的值會同時回答兩個問題:systemd 何時認定該單元已啟動,以及哪一個行程是主行程。

第一個答案決定了啟動順序。若其他單元在 After= 中指定了您的單元,它們會等待直到 systemd 判定您的單元已啟動為止。若 Type= 過早回報「已啟動」,會導致相依單元在您的服務準備就緒前就先行執行。

第二個答案決定了監控機制。systemd 會將單元衍生的所有行程放入 cgroup(控制群組)中;這是一項核心功能,能將行程分組以便統一限制資源或終止。systemctl stop 正是透過 cgroup 進行清理:KillMode= 的預設值為 control-group,因此停止單元時會向群組內的所有行程發送訊號。主 PID(Main PID)的定義則較為狹義。它是唯一一個其結束會導致單元終止,且其結束狀態會成為單元執行結果的行程。將 cgroup 視為等同於主 PID,正是造成混淆的根源。

Type=simple 會在二進位檔執行前報告服務已啟動

當 ExecStart= 被設定,且 Type= 與 BusName= 皆未出現時,Type=simple 為預設值。systemd 會建立處理程序,立即將該單元視為已啟動,並將該處理程序視為主要 PID。後續的單元會立即開始,甚至在服務二進位檔尚未執行前就已啟動。

最後這個細節解釋了一個常見的意外情況。ExecStart= 路徑中的拼字錯誤仍會產生一個顯示成功的啟動工作,而失敗則會在稍後執行失敗時才出現。systemd 會將此情況記錄為結束代碼 203,其內部表格將其命名為 EXEC,定義為無法執行服務二進位檔。因此,systemctl start 回傳無錯誤並不代表您的二進位檔確實存在。

對於留在前景且不會自行轉入背景的程式,請使用 simple。這適用於大多數現代守護行程以及您自行編寫的幾乎所有程式。

Type=exec 會等待程式實際啟動

Type=exec 是 simple 的進階版本。systemd 必須在 fork 與二進位檔執行皆成功後,才會將該單元視為已啟動。若二進位檔遺失或 User= 無法解析,啟動作業會直接失敗,而非回報成功後再於稍後默默中斷。

Type=exec 於 systemd 240 版本引入,因此目前所有伺服器發行版皆已支援。截至 2026 年 8 月,Ubuntu 24.04 搭載 systemd 255,Debian 13 則搭載 systemd 257。請使用 systemctl --version 檢查您的版本。

此設定的代價是啟動時會多出一個同步步驟,但優點是能從 systemctl start 獲得真實的結束狀態。對於前景程式,建議優先使用 exec 而非 simple。

Type=forking 與主 PID 遺失的問題

Type=forking 會告知 systemd,ExecStart= 中的處理程序將會 fork 出一個子處理程序,隨後自行結束。systemd 會等待該初始處理程序結束,才將該單元標記為已啟動。留下來的子處理程序即為 daemon。此習慣源自 SysV 時代,當時 init script 執行完畢後便無任何機制監控 daemon,PID 檔案成為記錄執行狀態的唯一依據;這項限制正是 為何 systemd 取代 init scripts 的核心原因之一。

困難點在於識別身分。由於 systemd 啟動的處理程序已經結束,systemd 必須判斷存活下來的處理程序中,哪一個才是主處理程序。請將 PIDFile= 設定為 daemon 寫入的檔案路徑(通常位於 /run 下),systemd 便會從中讀取 PID。systemd 同時會驗證該檔案中的 PID 是否確實屬於此服務,因此若檔案過期且指向無關的處理程序,該 PID 將被拒絕,而非被信任。

若未設定 PIDFile=,則會套用 GuessMainPID=,其預設值為 yes。此自動猜測機制僅在服務維持單一處理程序時才可靠。手冊明確指出其限制:若 daemon 包含多個處理程序,猜測結果可能錯誤,導致失敗偵測失效。單元亦可能出現主 PID 為 0 的情況,這代表 systemd 完全無法進行監控。

大多數會進行 fork 的 daemon 都提供將其維持在前台(foreground)執行的參數。請搭配 Type=exec 使用該參數,並刪除 PIDFile= 行。減少運作中的組件,即能降低 PID 遺失的風險。

Type=oneshot 用於執行完畢的工作

Type=oneshot 預期處理程序會執行並結束。systemd 僅在該單元結束後才視其為已啟動,這使得 oneshot 成為其他單元必須等待之任務的正確設定方式。當單元未指定 Type= 或 ExecStart= 時,這也是預設的隱含類型。

oneshot 有兩種特定行為。它是唯一接受多行 ExecStart= 指令的類型,且這些指令會依序執行。其啟動逾時預設為停用,因此若 oneshot 發生掛起,除非您自行設定 TimeoutStartSec=,否則它將無限期等待。

處理程序結束後,單元會回到 inactive 狀態。RemainAfterExit=yes 會使其保持 active 狀態,且不會有任何處理程序在執行。這是本頁開頭所述症狀的刻意版本,當單元的工作是留下狀態而非維持某個程序執行時(例如載入防火牆規則集或啟動容器堆疊),此設定是正確的。這也是 重啟後自動恢復的 Docker Compose 堆疊 背後的模式,其中單元執行 compose 指令後隨即結束,但因為它啟動的容器存活時間更長,單元仍保持 active 狀態。oneshot 單元也是排程觸發的對象,這是 使用 systemd timer 取代 cron 執行工作 的另一半核心。

Type=notify lets the service say when it is ready

Type=notify moves the decision to the service. systemd holds the start job open until the process sends READY=1 over a Unix socket whose path it receives in the NOTIFY_SOCKET environment variable. The C interface is sd_notify(3), and many servers support it already.

This is the accurate answer to the question "is it started". simple and exec report started before the service has read its configuration or opened its listening socket, so a dependent unit can start too early and fail its first connection. notify reports started at the moment the service itself says it is ready.

systemd accepts that message from the main process only, which is what NotifyAccess=main means, and Type=notify implies it. If the message comes from a child or a helper, set NotifyAccess=all. A shell script can call systemd-notify --ready, but that runs as a separate short lived process, so it needs NotifyAccess=all and systemd may not be able to attribute a message whose sender has already exited. A service that speaks the protocol itself is more reliable.

Two related settings are worth knowing. Type=notify-reload, available since systemd 253, extends the same handshake to reloads, so systemctl reload returns when the service reports the reload is finished instead of returning when the signal was sent. WatchdogSec= asks a notifying service to send a keep-alive message on an interval, and systemd treats a missed deadline as a failure.

Type=dbus 與 Type=idle

Type=dbus 會等待服務在 D-Bus 上取得名稱;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 9101

systemd 會將 shell 記錄為主要 PID。當 exporter 在前景執行時,shell 會保持運作。若 server 終止,shell 不會察覺,因此主要 PID 依然存活,單元狀態仍為 active,且 Restart= 沒有任何處理對象。由於兩個處理程序始終位於該單元的 cgroup 中,systemctl stop 仍能正確執行清理。損壞的是監控機制,而非清理程序。

解決方案取決於該單元實際包含多少個長駐處理程序。

若只有一個,請用該程式取代 shell。

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

exec 會以指定的程式取代 shell 並保留相同的 PID,因此 systemd 記錄的 PID 現在即屬於該 daemon。更好的做法是移除封裝腳本。Environment= 與 EnvironmentFile= 可用來傳遞變數,ExecStartPre= 可處理設定步驟,如此 systemd 便能直接啟動 daemon,並在建立時即掌握其 PID。

若有兩個處理程序,則沒有單一 PID 能代表該單元。請將其拆分為兩個單元,並使用 After= 與 Wants= 設定執行順序。每個處理程序對應一個單元是 systemd 能妥善監控的架構,這也是確保每個處理程序能擁有各自重啟行為的唯一方式。

ExitType=cgroup 的變更

ExitType= 於 systemd 250 版本中加入。預設值為 main:當主程序結束時,該單元即被視為已停止。若設定為 ExitType=cgroup,則只要 cgroup 中仍有任何程序存活,該單元就會被視為執行中。

[Service]
Type=simple
ExitType=cgroup
ExecStart=/opt/app/launcher

此設定解決了一個特定問題。在 ExitType=main 下,若啟動器(launcher)啟動了實際工作程序後隨即結束,systemd 會將該單元視為已停止並殺掉剩餘的程序。使用 ExitType=cgroup 時,單元狀態會追蹤整個群組。

請釐清此設定無法解決的問題。ExitType=cgroup 會在至少有一個程序存活時保持單元為啟用狀態,因此若一個單元包含兩個 daemon,其中一個死亡後,該單元仍會保持啟用。它僅修復了啟動器的問題,並不會將單一單元變成多個獨立程序的監控器。此外,ExitType= 無法與 Type=oneshot 同時使用。

cgroup 同時也是資源統計的依據,因此無論 Type= 對主 PID 的定義為何,諸如 MemoryMax= 與 CPUQuota= 等限制皆會套用於該單元所產生的每一個程序。關於此部分的詳細說明,請參閱 使用 systemd 限制服務的記憶體與 CPU。

如何找出 systemd 實際監控的處理程序

請依照順序在您正在偵錯的單元(unit)上執行下列步驟。先讀取 systemd 載入的內容,接著讀取它所追蹤的內容,最後將其與處理程序列表進行比對。

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

systemctl cat 會列印出單元檔案以及所有套用的 drop-in 設定,確保您讀取的是 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 會列出該單元 cgroup 中的所有處理程序。ps 行描述了 systemd 所監管的單一處理程序。請將兩者對照閱讀。MainPID 為 0 表示 systemd 沒有可監控的處理程序。若 cgroup 中同時包含您的 daemon 與一個解析為 shell 的 MainPID,則屬於上述的 wrapper 情況。若 cgroup 中的處理程序數量超出預期,則代表涉及了啟動器(launcher)或分岔(forking)的 daemon。

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

systemctl status 會同時列印狀態行與 cgroup 樹狀結構,因此通常能同時回答上述兩個問題。journalctl -u 若加上 -b 限制為本次開機,可顯示 systemd 為該單元記錄的啟動與停止事件,以及它所偵測到的結束代碼。若 daemon 是將日誌寫入自己的檔案而非 journal,請同時讀取該檔案,因為 systemd 只能記錄傳送給它的資訊。

當您變更 Type= 後,請重新載入並重新啟動。

systemd-analyze verify /etc/systemd/system/app.service
sudo systemctl daemon-reload
sudo systemctl restart app.service

systemd-analyze verify 會解析檔案並回報無法接受的設定。daemon-reload 會讓 systemd 從磁碟重新讀取單元檔案。變更後的 Type= 不會自動套用到已在執行的單元,因此重新啟動是必要的步驟,而非選用。

接著測試變更。從 systemd-cgls 取得您實際關注的處理程序 PID 並將其終止(kill)。隨後立即執行 systemctl is-active app.service。若 Type= 設定正確,該單元應會離開 active 狀態。若它仍保持 active,代表 systemd 仍在監控其他處理程序。

應該使用哪種 systemd 服務 Type=

  • 保持在前台執行的程式:Type=exec。
  • 支援就緒通知的程式:Type=notify,若同時確認重新載入則使用 notify-reload。
  • 強制進入背景執行的守護行程:Type=forking 搭配 PIDFile=,或使用其前台執行參數搭配 Type=exec。
  • 執行完畢後即退出的腳本:Type=oneshot,若目的是保留狀態則加上 RemainAfterExit=yes。
  • 啟動後即退出但子行程持續執行的啟動器:Type=simple 搭配 ExitType=cgroup。

若不確定第三方守護行程所需的類型,請先閱讀其封裝的 unit file。對發行版提供的 unit 執行 systemctl cat,即可查看上游開發者選擇的 Type=,該選擇已經過比您更廣泛的測試。

FAQ

為什麼我的 systemd unit 在處理程序終止後仍顯示為 active?

因為 systemd 視為主要處理程序的程序仍然存活。systemd 針對每個服務僅監控單一 PID(依據 Type= 決定),而非監控該 unit cgroup 中的所有處理程序。使用 Type=simple 啟動的包裝腳本(wrapper script)通常是主因:shell 本身是主要 PID,因此當 shell 在背景啟動的 daemon 結束時,該 unit 仍會保持 active 狀態。請執行 systemctl show -p MainPID app.service,接著使用 systemd-cgls --unit=app.service 列出該 unit 的 cgroup,並比較兩者差異。

Type=simple 與 Type=exec 有何不同?

Type=simple 在 systemd 建立處理程序後即視為啟動完成,此時二進位檔尚未執行,因此 ExecStart= 中的路徑錯誤仍會導致啟動任務顯示成功,隨後才發生失敗。Type=exec 則會等待執行成功,因此啟動任務本身會直接回報失敗。兩者皆將同一個處理程序視為主要 PID。Type=exec 需要 systemd 240 或更新版本。

使用 Type=forking 時仍需要 PIDFile= 嗎?

是的,只要 daemon 會寫入 PID 檔案就需要。若無此設定,systemd 會退而求其次使用 GuessMainPID=,這僅是推測,且僅在服務最終穩定為單一處理程序時才可靠。當推測錯誤或無法推測時,該 unit 的失敗偵測與自動重啟功能將會失效。請將 PIDFile= 指向 daemon 實際寫入的路徑,通常位於 /run 下。

何時應該使用 RemainAfterExit=yes?

當 unit 的目的是變更系統狀態,而非維持處理程序執行時。例如執行防火牆規則或啟動容器堆疊的 Type=oneshot unit,在工作完成後即會退出;若無 RemainAfterExit=yes,該 unit 會變為 inactive,導致 systemctl stop 無法停止服務,也無法執行 ExecStop= 的清理工作。設定此選項後,unit 會在沒有處理程序的情況下保持 active,這正是此類場景的預期行為。

變更 Type= 是否需要執行 daemon-reload?

是的,且必須重啟該 unit。systemctl daemon-reload 會讓 systemd 重新讀取磁碟上的 unit 檔案,但執行中的實例仍會保留啟動時的 Type=。測試前請務必執行 sudo systemctl daemon-reload 並接著執行 sudo systemctl restart app.service,否則您觀察到的仍是舊有的監控行為。