Systemd簡介與使用

小小怪醫芙蘭發表於2018-11-21

按下電源鍵,隨著風扇轉動的聲音,顯示器上開啟的圖示亮起。之後,只需要靜靜等待幾秒鐘,登入介面顯示,輸入密碼,即可愉快的玩耍了。
這是我們大概每天都做的事情。那麼中間到底發生了什麼?
簡單地說,從BIOS或者UEFI開始讀取硬碟。接下來,進入bootloader(LILO或者GRUB),bootloader開始載入核心,核心初始化完畢後,緊接著進入使用者空間的初始化。
使用者空間的啟動的第一個程式即pid=1,就是從一個叫init的程式開始的,這也是本文的主角

1. Systemd簡介與使用

1.1. 使用者空間的啟動順序

使用者的空間的大致啟動順序如下:

  • init
  • 基礎底層服務,如udevd(裝置管理器),syslogd(日誌管理)
  • 網路配置
  • 中高層服務,如cron(定時器)
  • 登入提示符(getty)、GUI、mysql(如果設定開機啟動的話)

init是核心啟動的第一個使用者空間程式,主要負責啟動、終止系統中的基礎服務程式。

Linux下,init主要有三個實現版本:

  • System V,傳統的init
  • Upstart,Ubuntu後期針對sys-v的一個改進實現版本
  • systemd,是一套中央化系統及設定管理程式(init),包括有守護程式、程式庫以及應用軟體,相容sys-v。現代大部分桌面版都使用此實現。也是本文主要介紹的一個…emmmm…框架。是的,systemd更像一個服務管理框架。

1.2. SystemV

先說說傳統的SystemV,他其實就是利用一系列指令碼來啟動服務,之後的事就撒手不管了。

SystemV init依賴一個特定的啟動順序每次只能啟動執行一個啟動任務。

這些都是通過一個核心配置檔案tab(/etc/init)和一組啟動指令碼以及符號連結集執行的,本質上為系統提供了合理的啟動順序,
支援不同的執行級別。

他的好處是依賴關係簡單,任務之間涇渭分明的一個一個啟動,即使某個基礎服務出了錯也便於排查。但也正因為如此,他的啟動效能很不好。
服務無法並行啟動不說,而且只能按照預先規定的順序啟動服務。如果你安裝了新的硬體或者新服務,他不提供及時支援的標準方法。

time
圖1

我們把使用者空間init的服務分別叫做Job AJob B…圖1可以看到,在SysV init之下,服務必須一個接一個的順序啟動,前面的服務初始化完畢,後面才可以開始。因此,啟動時間就是所有服務啟動時間之和。

他的改進版Upstart在此基礎上就做了優化——互不相關的服務可以並行啟動,這樣啟動總時間就等於時間消耗最大的一組服務,而不是所有服務之和。systemd在並行啟動上採取了比Upstart更加激進的方案

systemd-time
圖2

圖2是systemd的並行啟動方式,他讓配置所有的服務同時啟動。如果Job Aing依賴Job B怎麼辦呢?首先兩個Job是同時啟動的,A如果先啟動,就向B傳送請求服務,B會先將請求快取起來,等到B初始化完畢之後,再處理快取的請求。
相比SysV init,這也帶來了不確定性,即你不知道此時到底哪些服務起了,哪些沒起,全依賴系統管理

1.2.1. 執行級別

執行級別的概念最早應該也是來自於SysV init.

簡單地說,執行級別定義了系統的特定狀態,這種狀態可以看成一系列服務狀態的集合。

不同的發行版有不同的執行級別,但比較公認的如下:

  • 0,關機
  • 1,單使用者模式(修復模式),如果你的系統涼涼了,這將是你的救命稻草
  • 6,重啟

以我個人的deepin15.7為例,如圖

runlevel
default

其中runlevel2/3/4都屬於同一個執行等級(multi-user),而系統的預設的執行等級為5——graphical。我們平時所用的桌面環境就是這個等級了。其實,現代大部分採用systemd的發行版都和這個大同小異。

我們使用systemctl cat graphical.target開啟graphical.target檔案,可以看到下面內容:

[Unit]
Description=Graphical Interface
Documentation=man:systemd.special(7)
Requires=multi-user.target
Wants=display-manager.service
Conflicts=rescue.service rescue.target
After=multi-user.target rescue.service rescue.target display-manager.service
AllowIsolate=yes

其中的Requires=multi-user.target表示,如果想啟動graphical.target(即執行等級5)就必須先啟動multi-user.target(執行等級3).由此可見,在systemd中,執行等級5就是在等級3基礎上,同時啟動一個display-manager服務。display-manage顧名思義,肯定是和影像顯示有關的咯。

如果你對.target檔案,和他的定義語法很迷惑,沒有關係,後面還會詳細解釋。我舉這個例子,只是想讓你瞭解systemd是相容systemV的執行等級概念的。所以,你關於SystemV的認識也是可以繼續沿用的

1.3. Systemd

在Linux中以d結尾的,表示這是一個守護程式,systemd就是這個系統的守護程式

相比於之前的版本,systemd最關鍵的特性是:

  • 延遲啟動某些服務和系統功能,等到需要他們的時候才開啟
  • 完全並行啟動

systemd architecture

systemd 架構圖

1.3.1. systemd啟動步驟

systemd的特性複雜,下面給出大致的啟動步驟,使我們有個總體觀:

  1. systemd載入配置資訊
  2. 判定啟動目標,一般是default.target
  3. 判定啟動目標的依賴關係
  4. 啟用依賴服務,啟動目標
  5. 響應系統訊息,啟用其他元件

1.3.2. 單元和單元型別

systemd不光負責處理程式和服務,同時還能掛載檔案系統、監控網路套接字等等。在systemd中
所有服務和功能都被抽象成一個個單元(Unit),根據功能不同,單元型別也不同。systemd正是通過配置這些單元
來開關、管理服務的。

1.3.2.1. 單元型別

比較常用的幾種:

  • 服務單元,傳統的守護程式(XXX.service檔案表示)
  • 掛載單元,控制檔案系統掛載(XXX.mount檔案表示)
  • 目標單元,將服務單元、掛載單元等單元組織在一起的單元,一般對應Sys-V的執行等級(XXX.target檔案表示)

上面的尤其是服務單元我們會經常打交道,而且必要時也可以自定義服務單元等。比如我們的藍芽功能就抽象成
blueteeth.service,管理磁碟的udev系統對應systemd-udevd.service檔案。如果你安裝了mysql,
還可以找到一個mysql.service檔案。

使用deepin15.7的過程中,遇到過一個bug,就是在系統長期休眠之後再重啟,藍芽模組莫名其妙的關閉了,進入[設定]皮膚也
無法找到藍芽配置選項了。這時執行systemctl restart blueteeth.service重啟藍芽模組,大概率就會修復了

除了以上幾種,還有其他型別,比如
socket單元(.socket)、系統裝置單元(.device)、交換單元(.swap)、路徑單元(.path)、定時單元(.time),
不一而足

1.3.3. systemd相關指令

1.3.3.1. 電源管理

主要涉及開關、系統重啟等,如果你是當前唯一使用者的話則不需要提權,否則需要root密碼

systemctl reboot #重啟
systemctl poweroff #關機
systemctl suspend #待機
systemctl hibernate #休眠
systemctl rescue #進入單使用者模式

1.3.3.2. 分析系統狀態

主要是檢視系統中納入systemd管理的服務的狀態

systemctl status #系統狀態
systemctl list-units #所有啟用單元列表
systemctl --failed #執行失敗單元列表

# 列出所有配置檔案
$ systemctl list-unit-files

# 列出指定型別的配置檔案
$ systemctl list-unit-files --type=service

1.3.3.3. 單元的管理

使用systemd操作單元的啟用與關閉

systemctl start <unit> #立即啟用單元
systemctl stop <unit> #立即關閉單元
sudo systemctl kill <unit> #前面的stop不好使了,就強行殺死這個單元
systemctl restart <unit> #重啟單元
systemctl status <unit> #單元狀態,這是和好用的指令,能夠看到服務單元的幾乎所有資訊

systemctl is-enabled <unit> #單元是否配置自動啟動
systemctl enable <unit># 配置自動啟動單元
systemctl disable <unit>#關閉單元自動啟動

systemctl help <unit>#單元幫助手冊,一般是服務單元

systemctl daemon-reload <unit>#掃描單元配置檔案變動,重新載入

systemctl mask <unit> #禁用單元
systemctl unmask <unit>#取消禁用

下面是我本人計算機上mysql的狀態資訊:

systemctl status mysql.service
● mysql.service - MySQL Community Server
   Loaded: loaded (/lib/systemd/system/mysql.service; enabled; vendor preset: enabled)
   Active: active (running)
  Process: 2666 ExecStart=/usr/sbin/mysqld --daemonize --pid-file=/run/mysqld/mysqld.pid (code=exited, status=0/SUCCESS)
  Process: 2602 ExecStartPre=/usr/share/mysql/mysql-systemd-start pre (code=exited, status=0/SUCCESS)
 Main PID: 2668 (mysqld)
    Tasks: 27 (limit: 4915)
   Memory: 218.1M
   CGroup: /system.slice/mysql.service
           └─2668 /usr/sbin/mysqld --daemonize --pid-file=/run/mysqld/mysqld.pid
  • loaded,單元配置檔案地址
  • active:啟用狀態
  • process:開啟服務時執行的指令
  • main Pid:主程式ID
  • memory:佔用記憶體
  • CGroup:systemd通過CGroup控制程式,這裡展示該服務的所有子程式

1.3.3.4. 單元的依賴列表

systemctl list-depandencies <xxx.service> #列出xxx.service的依賴單元

在systemd中的單元的依賴關係

1.3.3.5. 其他

一些雜七雜八的指令

systemd-analyze #系統啟動時間統計
systemd-analyze blame #檢視所有服務啟動時間列表,blame就能看出,這是要等一個背鍋位
localectl #本地化資訊
timedatectl #時區資訊
loginctl list-user #列出當前登入使用者

systemd的指令非常豐富,可以通過查詢文件獲取全部指令

1.4. systemd配置

systemd的配置檔案主要分佈在兩個地方:
系統單元目錄(全域性配置,我的是/lib/systemd/system)和系統配置目錄(區域性配置,我的是/etc/systemd/system)

你可以通過下面的指令查詢配置目錄:

pkg-config systemd --variable=systemdsystemunitdir #單元目錄
pkg-config systemd --variable=systemdsystemconfdir #配置目錄

其實配置目錄的很多檔案都是指向單元目錄的軟連結。

單元配置檔案就像一個藍圖,定義了一個單元的依賴關係、啟動順序、開啟關閉指令或者掛載點等,
systemd就是讀取這些資訊來管理單元的。

1.4.1. Service檔案

在systemd中一個.service就是一個服務型別的配置單元,同時也代表了一個服務功能。

我們使用sysctemctl cat ssh.service來檢視ssh.service檔案內容,該檔案就在/lib/systemd/system下.

注:這個Service只有在你安裝openssh-server之後才會有.

[Unit]
Description=OpenBSD Secure Shell server
After=network.target auditd.service
ConditionPathExists=!/etc/ssh/sshd_not_to_be_run

[Service]
EnvironmentFile=-/etc/default/ssh
ExecStartPre=/usr/sbin/sshd -t
ExecStart=/usr/sbin/sshd -D $SSHD_OPTS
ExecReload=/usr/sbin/sshd -t
ExecReload=/bin/kill -HUP $MAINPID
KillMode=process
Restart=on-failure
RestartPreventExitStatus=255
Type=notify
RuntimeDirectory=sshd
RuntimeDirectoryMode=0755

[Install]
WantedBy=multi-user.target
Alias=sshd.service

可以看到service檔案分為Unit/Service/Install三個區塊,我們分開解釋

1.4.1.1. [Unit]

主要描述啟動順序與依賴關係

[Unit]
Description=OpenBSD Secure Shell server
After=network.target auditd.service
ConditionPathExists=!/etc/ssh/sshd_not_to_be_run

Description,一段描述Service的資訊

After,表示ssh.servicenetwork.target auditd.service單元之後啟動。另外還有一個屬性Before
表示當前單元在列出的單元之前啟動。比如Before=bar.service,說明當前單元在bar.service之前啟動。
AfterBefore定義了單元之間啟動的順序

ConditionPathExists,表示在後面的路徑存在時返回true,這裡使用了!非運算子,應該是取反的意思。
同樣還有其他幾個路徑判斷條件——ConditionPathIsDirectoryConditionFileNotEmpty,顧名思義,他們的
意義應該不難猜吧。這些條件必須返回為true,否則該單元不會執行

1.4.1.2. [Service]

這個區塊定義如何啟動當前服務

[Service]
EnvironmentFile=-/etc/default/ssh
ExecStartPre=/usr/sbin/sshd -t
ExecStart=/usr/sbin/sshd -D $SSHD_OPTS
ExecReload=/usr/sbin/sshd -t
ExecReload=/bin/kill -HUP $MAINPID
KillMode=process
Restart=on-failure
RestartPreventExitStatus=255
Type=notify

EnvironmentFile,指定當前服務環境引數檔案,內部使用鍵值對定義,可以使用$key讀取值,比如後面的$SSHD_OPTS

ExecStartPre,定義啟動服務前執行的指令

ExecStart,定義啟動程式執行的指令

ExecReload,表示重啟服務時執行的命令。其他的諸如ExecStop等等,望文生義即可

KillMode,定義 Systemd 如何停止 sshd 服務,process表示當kill sshd服務的時候,僅殺死主程式,子程式還是留著的。
其他的kill模式還有:

  • control-group(預設值):當前控制組裡面的所有子程式,都會被殺掉
  • mixed:主程式將收到 SIGTERM 訊號,子程式收到 SIGKILL 訊號
  • none:沒有程式會被殺掉,只是執行服務的 stop 命令

Restart欄位,定義了 sshd 退出後,Systemd 的重啟方式。on-failure,表示任何意外的失敗,就將重啟sshd。
另外還有其他重啟模式定義:

  • no(預設值):退出後不會重啟
  • on-success:只有正常退出時(退出狀態碼為0),才會重啟
  • on-abnormal:只有被訊號終止和超時,才會重啟
  • on-abort:只有在收到沒有捕捉到的訊號終止時,才會重啟
  • on-watchdog:超時退出,才會重啟
  • always:不管是什麼退出原因,總是重啟

最後一個比較重要的是Type欄位,定義啟動型別。notify,表示啟動結束後會發出通知訊號,然後 Systemd 再啟動其他服務。
其他的型別如下:

  • simple(預設值):ExecStart欄位啟動的程式為主程式
  • forking:ExecStart欄位將以fork()方式啟動,此時父程式將會退出,子程式將成為主程式
  • oneshot:類似於simple,但只執行一次,Systemd 會等它執行完,才啟動其他服務
  • dbus:類似於simple,但會等待 D-Bus 訊號後啟動

1.4.1.3. [Install]

定義如何安裝這個配置檔案,即怎樣做到開機啟動

WantedBy欄位:表示該服務所在的Target。
Target的含義是服務組,表示一組服務。WantedBy=multi-user.target指的是,sshd 所在的 Target 是multi-user.target。

systemctl enable sshd.service其實就是將sshd服務的連結放在multi-user.target.wants目錄下。
同時multi-user.target是系統的預設target,在啟動該target的時候,他下面的服務都會開機啟動。
這也就是隻要掛上multi-user.target就能開機啟動的原因

1.4.2. target檔案

執行systemctl cat multi-user.target,可得:

[Unit]
Description=Multi-User System
Documentation=man:systemd.special(7)
Requires=basic.target
Conflicts=rescue.service rescue.target
After=basic.target rescue.service rescue.target
AllowIsolate=yes

target檔案只是組織一批服務,因此他沒有[service]、[mount]等定義啟動或者掛載的區塊

Requires,表示強依賴關係,即必須要求basic.target啟動,否則multi-user啟動失敗。
其他的依賴關係如下:

  • Wants,只用於啟用依賴,沒有強依賴關係,該服務沒起來也不影響當前服務
  • Conflicts,衝突關係,有我沒他,否則不能執行
  • Requisite,前置依賴,當前單元啟用前,必須啟用它,否則失敗,屬於強依賴

Wants是比較重要的依賴關係,他不會將啟動錯誤擴散給其他單元。systemd文件鼓勵我們多用Wants關係

AllowIsolate,表示允許使用systemctl isolate命令切換到multi-user.target

1.5. systemd日誌服務

systemd 自帶日誌服務 journald,該日誌服務的設計初衷是克服現有的 syslog 服務的缺點。

  • syslog 不安全,訊息的內容無法驗證
  • 資料沒有嚴格的格式,非常隨意

Systemd Journal 用二進位制格式儲存所有日誌資訊,使用者使用 journalctl 命令來檢視日誌資訊。無需自己編寫複雜脆弱的字串分析處理程式。

常見的指令如下:

# 檢視所有日誌(預設情況下 ,只儲存本次啟動的日誌)
$ sudo journalctl

# 檢視核心日誌(不顯示應用日誌)
$ sudo journalctl -k

# 檢視系統本次啟動的日誌
$ sudo journalctl -b
$ sudo journalctl -b -0

# 檢視上一次啟動的日誌(需更改設定)
$ sudo journalctl -b -1

# 檢視指定時間的日誌
$ sudo journalctl --since="2012-10-30 18:17:16"
$ sudo journalctl --since "20 min ago"
$ sudo journalctl --since yesterday
$ sudo journalctl --since "2015-01-10" --until "2015-01-11 03:00"
$ sudo journalctl --since 09:00 --until "1 hour ago"

# 顯示尾部的最新10行日誌
$ sudo journalctl -n

# 顯示尾部指定行數的日誌
$ sudo journalctl -n 20

# 實時滾動顯示最新日誌
$ sudo journalctl -f

# 檢視指定服務的日誌
$ sudo journalctl /usr/lib/systemd/systemd

# 檢視指定程式的日誌
$ sudo journalctl _PID=1

# 檢視某個路徑的指令碼的日誌
$ sudo journalctl /usr/bin/bash

# 檢視指定使用者的日誌
$ sudo journalctl _UID=33 --since today

# 檢視某個 Unit 的日誌
$ sudo journalctl -u nginx.service
$ sudo journalctl -u nginx.service --since today

# 實時滾動顯示某個 Unit 的最新日誌
$ sudo journalctl -u nginx.service -f

# 合併顯示多個 Unit 的日誌
$ journalctl -u nginx.service -u php-fpm.service --since today

# 檢視指定優先順序(及其以上級別)的日誌,共有8級
# 0: emerg
# 1: alert
# 2: crit
# 3: err
# 4: warning
# 5: notice
# 6: info
# 7: debug
$ sudo journalctl -p err -b

# 日誌預設分頁輸出,--no-pager 改為正常的標準輸出
$ sudo journalctl --no-pager

# 以 JSON 格式(單行)輸出
$ sudo journalctl -b -u nginx.service -o json

# 以 JSON 格式(多行)輸出,可讀性更好
$ sudo journalctl -b -u nginx.serviceqq
 -o json-pretty

# 顯示日誌佔據的硬碟空間
$ sudo journalctl --disk-usage

# 指定日誌檔案佔據的最大空間
$ sudo journalctl --vacuum-size=1G

# 指定日誌檔案儲存多久
$ sudo journalctl --vacuum-time=1years

1.6. 在systemd中新增單元

關於自定義單元的首要一點建議:不要更改/lib/systemd/system(系統單元目錄),他由系統維護。
我們一般在/etc/systemd/system下自定義啟動單元。

1.6.1. 寫一個小栗子

  • 建立一個名為test1.target的單元
[Unit]
Description=test 1
  • 建立test2.target,依賴與test1
[Unit]
Description=test 2
Wants=test1.target
  • 啟用test2.target,test1作為依賴也會被啟用

systemctl start test2.target

  • 驗證兩個是否都被啟用

systemctl status test1.target test2.target

注:如果單元內包含[Install]模組,需要在start前enable他.
systemctl enable <unit>

  • 刪除單元
systemctl stop <unit> #首先停止單元
systemctl disable <unit> #如果有[Install]模組,則刪除連線符號
#最後刪除單元檔案即可

1.7. systemd 的按需和資源並行啟動

這是一個很複雜的概念,最好單獨討論

1.8. 參考