SSD Nodes Learn 🎉 VPS $5.50/月起
指南 Matt Connor作者: Matt Connor · 已更新 2026-08-13

systemd 的歷史:它為何勝出?

SysV init 做不到什麼?了解 Upstart 與 launchd 的先行嘗試、各發行版為何在四年內改用 systemd,以及哪些批評確實成立。

systemd 為何勝出

systemd 的歷史始於 SysV init 無法處理的兩件事。SysV init(System V init,Linux 從 AT&T Unix 繼承的啟動系統)無法描述服務相依於哪些項目,也無法在服務啟動後判斷哪些程序屬於該服務。systemd 運用 shell script 無法使用的 kernel 功能,同時解決了這兩個問題:使用 control group 追蹤程序,並透過預先開啟的 listening socket 控制啟動順序。後續發展則是這兩項解法如何擴展到其他 userland 元件;爭議也從此開始,而且其中幾項批評確實有道理。

SysV init 實際執行的工作

在 SysV 系統中,PID 1(核心啟動的第一個程序)會讀取 /etc/inittab、選擇 runlevel,然後執行該 runlevel 的腳本。這些腳本位於 /etc/init.d//etc/rc3.d/ 中的符號連結決定哪些腳本會執行及其順序,因此 /etc/rc3.d/S20nginx 會指向 /etc/init.d/nginx,並以 start 引數呼叫。

#!/bin/sh
### BEGIN INIT INFO
# Provides:          nginx
# Required-Start:    $local_fs $remote_fs $network $syslog
# Required-Stop:     $local_fs $remote_fs $network $syslog
# Default-Start:     2 3 4 5
# Default-Stop:      0 1 6
### END INIT INFO

case "$1" in
  start)
    start-stop-daemon --start --quiet --pidfile /run/nginx.pid \
      --exec /usr/sbin/nginx
    ;;
  stop)
    start-stop-daemon --stop --quiet --retry TERM/30/KILL/5 \
      --pidfile /run/nginx.pid
    ;;
esac

S20nginx 中的 20 是執行順序,而不是相依性。它表示此腳本會在 S19 之後、S21 之前執行。但它沒有說明原因,因此無法進行驗證;若沒有人工判斷哪些腳本彼此獨立且可安全並行執行,也無法同時執行兩個不相關的腳本。

rc 程式會依序執行每個腳本,並等待腳本結束。若某個腳本因等待網路位址而阻塞 30 秒,整個開機流程就會被阻塞 30 秒,即使其他服務完全不使用網路也一樣。

該腳本頂端的 LSB(Linux Standard Base)標頭,原本是為了從 SysV init 內部解決這個問題。Debian 6.0 在 2011 年將 insserv 設為預設值:它會從每個腳本讀取 Required-Start、建立相依性圖,並重新編號符號連結。如此一來,Debian 就能透過 startpar 同時執行彼此獨立的腳本。這確實有所改善,但仍未解決更深層的問題。相依性仍然建立在腳本結束之上。S20nginx 回傳 0 只表示 shell 函式已返回,不表示 nginx 已在接受連線。

init script 無法解決的 5 件事

  • 平行啟動。依檔名排序會為機器上的所有服務建立完整順序,因此開機時間會等於各項工作時間的總和。
  • 就緒狀態。啟動 script 在 daemon 建立子程序後就會結束,而不是等到 daemon 能夠處理請求。因此,下一個 script 往往會過早啟動。
  • 監督。Daemon 會連續 fork 2 次,原始父程序隨即結束,使 daemon 脫離終端機,並由 PID 1 重新收養。init 只會看到子程序結束,無法可靠地連結到仍在執行的程序。
  • 隨需啟動。inetd(internet super-server)能在收到連線時啟動 daemon,但它是獨立的系統,使用自己的設定檔,且無法處理開機時其他服務的啟動順序。
  • 資源控制。init script 無法限制服務的記憶體用量或 CPU 佔用比例。ulimit 只能套用到單一程序,nice 也只能影響 scheduler,因此服務衍生出的失控子程序,看起來就和主機上的其他程序一樣。

監督能力的缺口是日常運作中最棘手的問題。PID file 是當時的替代方案:daemon 將 process ID 寫入 /run/nginx.pid,stop function 再讀取該檔案。如果 daemon 被強制終止,檔案仍會留在系統中。之後 kernel 可能將同一數字重新指派給其他程序,而 start-stop-daemon --stop --pidfile 便會將 signal 傳送給目前擁有該 PID 的程序。過時的 PID file 會讓 init script 終止錯誤的程序。

launchd 先解決了 socket 問題

Apple 在 2005 年於 Mac OS X 10.4 中推出 launchd,由 Dave Zarzycki 撰寫。單一程序取代了 init、rc、xinetd、crond 和 watchdogd。

值得採用的概念是 socket activation。launchd 會先建立所有 listening socket,再啟動 daemon。用戶端連線到尚未啟動的 daemon 時,不會收到 connection refused,因為 kernel 會將連線保留在該 socket 的 backlog queue 中,直到 daemon 呼叫 accept()。兩個 daemon 之間的啟動順序不再需要由管理員宣告。socket 會處理這項工作。

launchd 建立在 Mach IPC(inter-process communication)之上。Mach IPC 隸屬於 Apple 的 XNU kernel,Linux 沒有對等機制。因此,移植這段程式碼並不實際。不過,這個概念仍然被採用。

Upstart 將事件作為工作單位

Canonical 的 Upstart 由 Scott James Remnant 撰寫,於 2006 年 10 月隨 Ubuntu 6.10 發布。Fedora 9 到 Fedora 14 使用了 Upstart,RHEL 6 與 Chrome OS 也同樣採用。Upstart 以事件取代 runlevel,job 會指定哪些事件應啟動或停止該 job。

# /etc/init/example.conf
start on filesystem and net-device-up IFACE!=lo
stop on runlevel [!2345]
respawn
respawn limit 10 5
exec /usr/local/bin/exampled

隨著 job 數量增加,出現了兩個問題。第一個是依賴方向。job 會表示「發生這件事時啟動我」,因此依賴關係的資訊放在了錯誤的檔案中:服務知道自己需要什麼,卻不可能知道明年會有哪些服務需要它。新增服務通常代表必須編輯現有 job,讓它發出新的事件。

第二個是程序追蹤。Upstart 透過使用 ptrace 計算分叉 daemon 的 fork() 呼叫,藉此追蹤該 daemon;這項設定可指定為 expect forkexpect daemon。如果錯估分叉數量,Upstart 可能會監督已經結束的程序,或等待已經完成的分叉。其症狀是 initctl start 無限等待且沒有錯誤訊息,而 job 檔案也無法說明原因。

Upstart 也要求貢獻者簽署 Canonical 的貢獻者協議。這不是工程上的缺陷,但確實影響了參與開發的人員。

重新思考 PID 1,2010 年 4 月

2010 年 4 月 30 日,Lennart Poettering 發表了一篇名為「重新思考 PID 1」的文章。Kay Sievers 與他共同參與這項專案。這項主張包含四個部分。

  • 少啟動一些服務。許多服務可以等到實際收到請求時再啟動。
  • 如果 socket 能夠表達啟動順序,就不要另外宣告順序。先一次開啟所有 socket,再同時啟動所有服務。
  • 使用 control groups 追蹤程序,不要依賴 PID files。
  • 在宣告式檔案中描述服務,讓同一份描述可適用於每個發行版。

第一個版本於同年推出。Fedora 14 在 2010 年 11 月將 systemd 作為可選項目提供,Fedora 15 則在 2011 年 5 月將其設為預設值。

cgroups 如何讓服務監督變得可靠

cgroup(control group)是用來分組程序的 kernel 功能,已在 2008 年併入 Linux 2.6.24。systemd 會將每個服務放入自己的 cgroup。子程序會繼承父程序的 cgroup,而非特權程序無法將自己移出 cgroup。因此,雙重 fork 並不能隱藏任何程序:PID 1 隨時都掌握屬於某個 unit 的完整程序集合。停止服務代表終止其 cgroup 中的所有程序,這正是預設 KillMode=control-group 的行為。

systemctl status 會列印該群組:

● nginx.service - A high performance web server and a reverse proxy server
     Loaded: loaded (/usr/lib/systemd/system/nginx.service; enabled; preset: enabled)
     Active: active (running) since Tue 2026-08-11 09:14:22 UTC; 3min ago
   Main PID: 1042 (nginx)
      Tasks: 3 (limit: 4653)
     Memory: 6.1M (peak: 7.4M)
     CGroup: /system.slice/nginx.service
             ├─1042 "nginx: master process /usr/sbin/nginx"
             ├─1043 "nginx: worker process"
             └─1044 "nginx: worker process"

這段輸出完整解釋了 stale PID file 的問題。不存在會變成 stale 的檔案,因為程序清單是 kernel 狀態。

同一棵樹也承載資源限制,因為 cgroup 原本是為 accounting 建立,之後才被用於追蹤程序。MemoryMax=CPUQuota=TasksMax= 各佔一行。為服務設定記憶體與 CPU 硬性上限 現在只需建立 drop-in file;而在 2009 年,這得修改一個沒人撰寫的 shell script。

為什麼所有發行版都在2011至2015年間改用 systemd

  • Fedora 15,2011年5月。
  • openSUSE 12.1,2011年11月。
  • Mageia 2,2012年5月。
  • Arch Linux,自2012年10月起新安裝預設使用 systemd。
  • RHEL 7,2014年6月。
  • SLES 12,2014年10月。
  • Debian 8,2015年4月。
  • Ubuntu 15.04,2015年4月。

原因大多很單純,因此切換速度很快。

  • 每個發行版都能使用同一個 unit file,因此上游專案開始隨附 .service 檔案,發行版也不再為每個套件和每個版本各自維護 shell script。
  • ConsoleKit 約在2012年停止維護後,桌面工作階段追蹤改由 systemd-logind 負責。GNOME 需要 logind,因此不使用 systemd 的發行版必須尋找替代方案。該替代方案 elogind 是從 systemd 中抽出並獨立維護的 logind。
  • 裝置管理員 udev 在2012年4月併入 systemd 原始碼樹。提供 udev 的發行版此後都必須追蹤 systemd 的 repository。Gentoo 因此 fork 了 eudev
  • 容器讓可靠的程序追蹤和每個服務的限制更加重要,因為這兩者都是 cgroup 的功能。每當你讓 Docker Compose stack 在重新開機後恢復執行時,哪個 supervisor 負責管理容器程序仍然是需要處理的問題。

Debian 的決策引發了最大聲量。Technical Committee 在2014年2月進行投票,結果平票;主席 Bdale Garbee 投下決定票,支持 systemd。幾天後,Ubuntu 宣布將遵循 Debian,而不是繼續使用 Upstart。2014年11月,一群 Debian 開發者將該發行版 fork 為 Devuan,並在2017年5月發布 Devuan 1.0。

公平陳述各項反對意見

範圍。 現在由同一個專案提供 PID 1、日誌 daemon、登入工作階段管理、裝置管理員、網路設定 daemon、DNS(domain name system)解析器、NTP(network time protocol)用戶端、容器執行工具及開機載入程式。常見的辯護理由是:這些都是不必安裝的獨立二進位檔。這點沒錯,但無法回答反對意見。一旦桌面環境需要 logind,而 logind 又從 systemd 的程式樹中獨立發布,選擇就不再完全自由。這就是該論點所說的耦合,而且確實已經發生。

二進位日誌。 journald 會寫入具索引的二進位格式,而不是純文字。這提供了純文字從未提供的功能:依單位和優先級篩選、結構化欄位,以及傳送程式無法偽造的中繼資料,因為 journald 會自行記錄單位與 cgroup。journalctl -u nginx -p err --since "-1h" 可用日期正規表示式取代 grep。但代價也確實存在。無法開機的機器,不能在救援 shell 中使用 less 讀取日誌。請改為將 journalctl 指向已掛載的磁碟:

sudo journalctl --directory /mnt/var/log/journal --boot -1 --priority err

這裡還有另一個容易忽略的問題。除非存在 /var/log/journal,否則 journald 會將日誌保存在 /run/log/journal,也就是記憶體中。在不存在該目錄的機器上,重新開機後 journalctl -b -1 不會顯示任何內容,而這正是你最需要日誌的時候。請檢查並修正:

journalctl --disk-usage
sudo mkdir -p /var/log/journal
sudo systemd-tmpfiles --create --prefix /var/log/journal
sudo systemctl restart systemd-journald

現在 journalctl --disk-usage 應顯示 /var/log/journal 下的封存日誌。如果也需要純文字格式,請在 /etc/systemd/journald.conf 中設定 ForwardToSyslog=yes,並保留 rsyslog 的安裝。

開機問題的可除錯性。 單位卡住時,主控台只會顯示一行訊息,之後沒有其他內容:

[  *** ] A start job is running for Wait for Network to be Configured (1min 32s / no limit)

其實有工具可以進一步調查:卡住時使用 systemctl list-jobs,之後使用 systemd-analyze blamesystemd-analyze critical-chain,並在 kernel command line 中加入 systemd.log_level=debug。公平地說,反對意見是:只要了解 sh,任何人都能從頭到尾讀懂 init script;但遇到卡住的單位時,必須知道十幾個命令中應該使用哪一個。這確實是成本。每位系統管理員都要付出一次,而當時有許多系統管理員同時付出了這項成本。

所有人都會受到影響的預設值。 systemd 230 在 2016 年變更了 logind 的預設值,讓使用者登出時終止殘留的使用者程序。當啟動它們的工作階段結束時,分離的 tmuxscreen 工作階段也會終止。各發行版在 /etc/systemd/logind.conf 中提供 KillUserProcesses=no,而支援的做法是 loginctl enable-linger <user>。單一專案中的一項預設值變更,就改變了數百萬人所依賴的使用習慣。這就是「太多 userland 集中在同一處」在實務上的含義。

預設相依性就是安全性暴露面。 2024 年 3 月,xz-utils 中的後門以 Debian 和 Ubuntu 上的 sshd 為目標。上游 OpenSSH 並未連結 libsystemd。這些發行版將其加入修補內容,讓 sshd 能向 systemd 回報就緒狀態;而 libsystemd 又拉入了 liblzma,後門正存在於其中。就緒協定本身只是傳送到 $NOTIFY_SOCKET 所指定 socket 的單一 datagram,因此從未需要任何 library。systemd 的回應是使用 dlopen 載入壓縮 library,讓這些 library 不再預設連結。

另一類相關的錯誤也呈現相同模式:2017 年,一個以數字開頭的 User= 值會被視為無效,導致單位以 root 身分執行,而不是啟動失敗。因此,一個拼字錯誤就可能變成權限提升。後續版本會拒絕啟動該單位。

在自己的 systemctl 提示字元中回顧

上述每個問題,現在都對應到檔案中的一項指令,而且你可以直接讀取該檔案。

  • 序列化開機流程變成 After=Wants=,而 systemd-analyze critical-chain 會顯示實際阻塞開機的項目。
  • 就緒狀態變成 Type=notify;服務準備好接受請求時,會將 READY=1 寫入 $NOTIFY_SOCKET。舊版 daemon 仍可使用 Type=forking 搭配 PIDFile=,如果 PID 檔案始終未出現,這類型會在 start operation timed out. Terminating. 時失敗。
  • 監督機制改由 cgroup 負責,因此 Restart=on-failure 搭配 RestartSec= 可取代 wrapper script,而 StartLimitBurst= 可避免 crash loop 永遠持續執行。
  • inetd 變成與 .service unit 並列的 .socket unit。
  • ulimit 變成 MemoryMax=CPUQuota=TasksMax=
  • init script 中的 su - appuser -c 行變成 User=NoNewPrivileges=yesProtectSystem=strict,因此 以非特權使用者執行服務 是 unit 的預設形式,不再需要額外處理。
[Unit]
Description=Example API
Wants=network-online.target
After=network-online.target postgresql.service
Requires=postgresql.service

[Service]
Type=notify
ExecStart=/usr/local/bin/exampled
Restart=on-failure
RestartSec=2
MemoryMax=512M
CPUQuota=50%
TasksMax=128
User=exampled
NoNewPrivileges=yes
PrivateTmp=yes
ProtectSystem=strict
StateDirectory=exampled

[Install]
WantedBy=multi-user.target

檔案中有一行,是每個人都會犯一次的錯誤。Requires=postgresql.service 是相依條件,不是啟動順序:它表示 Postgres 失敗時,你的 unit 也會失敗,但不表示要先啟動 Postgres。沒有 After=postgresql.service 時,兩者會同時啟動,而你的服務會連線到尚未有程序監聽的埠。兩者刻意分開,因為有時你只需要其中一項。ProtectSystem=strict 會將檔案系統以唯讀方式掛載給此服務,這就是設定 StateDirectory= 的原因:它會在 /var/lib 下提供一個可寫入的路徑給服務。

在 2026 年的伺服器上,最容易看到 2005 年遺留行為的例子,是 Ubuntu 24.04 上的 SSH;截至 2026 年 8 月,該版本搭載 systemd 255。ssh.service 預設由 socket 啟動:ssh.socket 持有監聽 socket,收到連線時才啟動 sshd。因此,在 /etc/ssh/sshd_config 中設定 Port 2222 不會生效,因為開啟該埠的程序不是 sshd。這項變更必須放在 socket unit 中。

sudo systemctl edit ssh.socket
[Socket]
ListenStream=
ListenStream=2222

空白的 ListenStream= 會清除從套件提供的 unit 繼承而來的值。如果省略它,就會同時取得兩個埠,因為 systemd 會將清單附加到原值,而不是取代原值。接著套用變更並進行檢查,整個過程都要保持第二個 SSH 工作階段開啟:

sudo systemctl daemon-reload
sudo systemctl restart ssh.socket
sudo ss -lntp | grep 2222

ss 應列出一個由 systemd 持有、監聽 2222 埠的 socket,而不是由 sshd 持有。這就是 20 年後在你的 VPS 上實現的 launchd 設計。如果偏好舊行為,執行 sudo systemctl disable --now ssh.socket,再執行 sudo systemctl enable --now ssh.service,即可使用會長時間執行的 sshd,並重新從自己的設定檔讀取 Port

你會遇到哪些細節,取決於所使用的版本。因此,在規劃升級前,值得先了解 LTS 與 interim Ubuntu release 的差異。當你管理多台機器時,所有地方的 unit file 都完全相同,這正是從單一位置管理多台伺服器如今成為設定管理問題,而不再是 shell scripting 問題的原因。自行撰寫 unit 時,service 與 timer 的配對則能完成你在 2009 年必須分別交由 init script 和 cron line 處理的工作。

FAQ

為什麼 Linux 發行版會以 systemd 取代 SysV init?

原因有兩個工程面因素,以及一個維護面因素。SysV init 依檔名排序服務;這是位置而不是相依性。它也無法追蹤從父程序 fork 後脫離的 daemon,因此過時的 PID 檔案可能會終止錯誤的程序。systemd 以 socket activation 和相依性指示詞解決啟動順序問題,並以 control group 解決程序追蹤問題。維護因素則決定了採用速度:同一個 unit file 可在所有發行版上運作,因此上游專案會提供 .service 檔案,發行版維護者也不再為每個套件撰寫 shell script。Fedora 15 在 2011 年 5 月切換,Ubuntu 15.04 則是最後一個主要維持原方案的發行版,直到 2015 年 4 月才切換。

systemd 是一個巨型二進位檔嗎?

不是。原始碼樹會建置出許多獨立程式。PID 1 是 /usr/lib/systemd/systemd,而 journald、logind 和 udevd 都是使用各自二進位檔的獨立程序;執行 ls /usr/lib/systemd/ 即可在自己的主機上查看。至今仍存在的批評,焦點是版本發布的耦合,而不是二進位檔大小:這些程式會一同發布,並共用私有介面,因此各發行版通常會整批採用;GNOME 等軟體也逐漸開始特別依賴 logind。

我仍然可以在不使用 systemd 的情況下執行 Linux 嗎?

可以。Devuan 提供 sysvinit,Gentoo 預設使用 OpenRC,Void 使用 runit,Alpine 使用搭配 OpenRC 的 busybox init,而 Slackware 繼續保留 BSD 風格的 script。代價是相容性維護工作。預期使用 logind 的桌面軟體需要 elogind;這是以獨立套件維護的 systemd logind。而且越來越多伺服器軟體現在只提供 .service 檔案,因此您必須自行撰寫並維護啟動 script。

為什麼 journal 使用二進位格式,而不是純文字檔?

因為 journald 會儲存含索引的結構化欄位,提供依 unit 和優先順序篩選的功能,以及傳送程式無法偽造的中繼資料:journald 會自行記錄 unit、cgroup 和實際 UID,而不是信任日誌行內容。代價是必須使用 journalctl 讀取,即使是在救援系統中也是如此;此時可使用 journalctl --directory /mnt/var/log/journal 將它指向已掛載的磁碟。如果也需要文字格式,請在 /etc/systemd/journald.conf 中設定 ForwardToSyslog=yes

哪個機制取代了直接編輯 /etc/init.d script?

Drop-in 檔案。請勿編輯 /usr/lib/systemd/system/ 中的 unit,因為套件升級會覆寫它。執行 sudo systemctl edit nginx.service,systemd 便會建立 /etc/systemd/system/nginx.service.d/override.conf,並將其內容合併到套件提供的 unit 上方。systemctl cat nginx.service 會顯示合併後的結果,systemd-delta 會列出主機上的所有 override。手動完成任何編輯後,請執行 sudo systemctl daemon-reload,否則下一個指令會輸出 Warning: The unit file, source configuration file or drop-ins of nginx.service changed on disk.

#systemd#linux#init#sysvinit#history