乾貨分享 | Systemd 技術原理&實踐(上)

優麒麟的程序員小哥在研究如何優化系統資源模塊時,查閱了許許多多的資料,發現沒有一篇能詳細把 systemd 的優勢與原理說得很清楚的中文介紹文章,於是自己動手下載了 systemd 的源碼,對照資料彙總了一份有關 systemd 的詳解文章,希望能對研究 systemd 的優客有所幫助。

systemd 介紹

1 systemd 的起源

關於 systemd 的起源,首先要從 Linux 的 init 程序說起。Linux 系統在啓動過程中,內核完成初始化以後,由內核第一個啓動的程序便是 init 程序,路徑爲 /sbin/init(爲一個軟連接,鏈接到真實的 init 進程),其 PID 爲1,它爲系統裏所有進程的“祖先”,Linux 中所有的進程都由 init 進程直接或間接進行創建並運行,init 進程以守護進程的方式存在,負責組織與運行系統的相關初始化工作,讓系統進入定義好的運行模式,如命令行模式或圖形界面模式。

init 程序的發展,大體上可分爲三個階段:sysvinit->upstart->systemd,根據 init 進程的發展特性,可以簡單理解爲如下:

sysvinit:init 系統通過 shell 腳本以串行的方式啓動系統服務,下一個進程必須等待上一個進程啓動完成後才能開始啓動,因此係統啓動的過程比較慢。

upstart:在 sysvinit 的基礎上,把一些沒有關聯的程序並行啓動,以提高啓動的速度,但是存在依賴關係的程序仍然爲串行啓動。

systemd:通過套接字激活的機制,讓所有無論有無依賴關係的程序全部並行啓動,並且僅按照系統啓動的需要啓動相應的服務,最大化提高開機啓動速度。

目前優麒麟操作系統使用的就爲 systemd。systemd 的意思爲 system daemon,意爲系統守護進程,由 Lennart Poettering 帶頭開發,採用更加優秀的服務框架,並且與老的 sysvinit 兼容,其設計目的就是克服 sysvinit 與 upstart 的缺點,進一步地提高啓動速度。目前主流的系統中,systemd 的守護進程主要分爲系統態(system)與用戶態(user),可以在 ps -ef 中看到 systemd 的守護進程,如下:

PID 爲1的進程/sbin/init 即是 system 態的 systemd,它爲一個軟鏈接,指向真實的 systemd 路徑,在優麒麟操作系統中一般放在/lib/systemd/目錄:

systemd 爲進程服務集合的總稱,它包含許多的進程,負責控制、管理系統的資源,其中包括 systemd-login,負責用戶登錄相關信息的創建、修改與刪除;systemd-sleep 控制系統的休眠、睡眠狀態切換等等。在優麒麟操作系統下,它們主要集中在/lib/systemd/文件目錄:

每個進程的主要用途可以閱讀 freedesktop systemd 手冊:https://www.freedesktop.org/software/systemd/man/

目前 systemd 佔據 init 程序的主導,有統一天下的趨勢。

2 systemd 的主要功能

  1. systemd 採用並行的啓動方式,並提供按需啓動的方式:systemd 在設計之初最關注兩件事情:更少的開始,更多的並行。更少的開始,意味着在開機啓動階段,systemd 僅啓動系統啓動時必要的一些服務,更多其他的服務推遲啓動,直到真正需要它的時候,例如優麒麟的藍牙 bluetooth 與截圖相關的服務,開機的時候系統不會用到它;優麒麟的 U 盤啓動器相關的服務,可以等到接入 U 盤的時候再啓動;如果系統未連接到網絡,那那些需要用到網絡的相關服務也可以無需啓動,直到網絡連通後的第一次連接再啓動即可。更多的並行,意味着服務的啓動不需要像 sysvinit 一樣序列化啓動,而是同時運行所有需要的服務,以便系統 cpu 資源利用的最大化,因此總的啓動時間最小化,後面會介紹 systemd 是如何實現所有服務並行啓動。

  2. 採用 cgroup 跟蹤管理進程的生命週期:cgroup 爲控制組,是一個層級結構,類似與文件管理系統的結構。當一個進程創建了子進程,子進程會繼承父進程的 cgroup,就好比子進程創建在父進程的目錄下,當子進程又創建一個子進程時,這個子進程會繼承上一個子進程的 cgroup,也就相當與繼承了父進程的 cgroup,好比這個子進程創建在上一個子進程的目錄下,也就在父進程的目錄下,通過這一機制就可以把父進程與所有的子進程關聯起來並進行跟蹤,當停止父進程時,可以通過查詢 cgroup 找到所有關聯的子進程,從而確保乾淨的停止所有相關服務。

  3. 啓動掛載與自動掛載:在系統啓動過程中,systemd 在初始化時會自身進行一些掛載,如/sys 目錄與/run 目錄的掛載,這些都是系統啓動時至關重要的文件系統。systemd 還能實現動態掛載點的自動掛載,例如不需要經常使用的光盤、U 盤的掛載,只在這些設備接入時,systemd 啓動相應的服務並對其進行臨時的掛載以便訪問其中的內容,當這些設備拔出時,這些掛載點將被取消以便節約資源。

  4. 事務的依賴關係管理:系統有很多的服務存在依賴關係,例如麒麟軟件商店需要等待網絡服務的啓動,lightdm 與 systemd 交互需要等待 D-Bus 的啓動,大多數服務也需要等待 syslog 的完全啓動與初始化。systemd 採用 Unit(配置單元)管理這些服務的依賴關係,維護一個事務的一致性,並保證所有的相關服務不會出現相互依賴而產生死鎖的情況,後面會對 Unit 進行詳細介紹。

  5. 日誌:systemd 自帶 journalctl 命令來查看系統保存的所有日誌信息,並且可以支持通過一些參數來對日誌進行過濾,方便用戶進行日誌分析。

  6. 其他:systemd 經過幾代的更新,實現的功能已經十分的多了,甚至有人覺得 systemd 管得太多了,導致一些服務都沒有了存在的必要。例如 systemd 添加了許多 systemctl 的命令,可以實現系統電源的管理;systemd 還添加了看門狗機制,其他守護進程需要定期 ping systemd 進程,否則會視爲失敗而重啓它等等。詳情可以去閱讀設計師的博客 http://0pointer.de/blog/projects。

3 systemd 如何實現服務的並行

systemd 的設計理念就是希望讓所有的服務並行的啓動,以最大化利用硬件資源,提高啓動的時間。但是我們知道服務之間存在依賴關係,客戶端需要等待服務端的啓動纔可以建立連接,例如前面提到的,在優麒麟操作系統中,所有的服務都需要等待 syslog 服務的啓動,那 systemd 是如何擺脫這同步和序列化過程的呢?

systemd 的設計師認爲,對於傳統的守護進程,他們真正實際等待另一個守護進程提供的是套接字的準備,需要的是一個文件系統的 socket 套接字描述符,這是它們唯一等待的,因此是否可以設法讓這些套接字描述符可以更早的創建用於連接,從而不用等待整個守護進程完整的啓動?答案是可以的。

在 C 語言中,一個進程啓動另一個進程時,一般是執行系統調用 exec(),systemd 在調用 exec()來啓動服務之前,先創建與該服務關聯的監聽套接字並激活,然後在 exec()啓動服務期間把套接字傳遞給它,因此在服務還在啓動的時候,套接字就已經處於可用的狀態。通過這一方式,systemd 可以在第一步中爲所有的服務創建套接字,然後第二步一次運行所有的服務,如果一個服務需要依賴於另一個服務,由於套接字已經準備好,服務之間可以直接進行連接並繼續執行啓動,如果遇到了需要同步的請求,不得不等待阻塞的情況,那阻塞的也將只會是一個服務,並且只是一個服務的一個請求,不會影響其他服務的啓動,由此實現服務之間不需要再進行序列化的啓動。Linux 內核提供了套接字緩衝區功能,幫助 systemd 實現了最大的並行化,還是拿 syslog 服務來說,優麒麟操作系統上大多數服務在啓動初期都會先進行日誌相關的初始化配置,如果同時啓動 syslog 服務與各種 syslog 的客戶端服務,由於 syslog 相關套接字在 systemd 執行 exec()啓動 syslog 之前已經創建並準備好,客戶端可以直接連接到 syslog 的套接字上,如果遇到 syslog 啓動比較慢,客戶端向 syslog 發送請求消息,syslog 還無法處理的情況,通過內核 socket 緩衝區的機制,請求的消息將會傳到 syslog 套接字的緩衝區之中,只要緩衝區未滿,客戶端就不需要等待並繼續往下執行;一旦 syslog 服務完全啓動,它就會使所有消息出列並處理他們;當出現另一種情況,緩衝區已滿,或者需要同步消息請求的情況,雖然這個時候客戶端不得不阻塞等待,但是也只有一個客戶端的一個請求被阻塞,並且直到服務端趕上並處理爲止。

因此 systemd 先進行套接字的激活,然後開始服務的創建,使得所有的服務可以並行啓動,依賴的管理也變得多餘,至少可以說是次要的,因爲從服務的角度看,只要套接字是激活的,另一個服務有沒有啓動都沒有區別,這樣一種方式也使得程序更加地健壯,因爲不論服務可用或不可用,甚至是崩潰,套接字都處於可用的狀態,不會讓客戶端注意到丟失連接。

4 systemd 執行單元--Unit 介紹

Unit 是 systemd 管理服務與資源的基本單元,可以簡單理解爲 systemd 啓動後每次需要執行的服務,每次需要處理的資源,都被抽象爲一個配置單元 Unit,保存在一個 Unit 文件裏面。例如,當用戶登錄到優麒麟操作系統時,systemd 會執行 systemd-login.service 這個 Unit 文件來啓動 login 服務,持續跟蹤用戶的會話、進程、空閒狀態,爲用戶分配一個 slice 單元;當用戶執行睡眠操作時,systemd 會執行 systemd-suspend.service 文件的 Unit,來啓動 systemd-sleep 服務執行系統睡眠操作。Unit 文件可以根據其後綴名分爲12種不同的類型,systemd 內部給這12種類型的 Unit 定義了不同的全局模板,因此 systemd 的執行流程爲:

首先找到對應的 Unit 文件,然後根據 Unit 文件的類型匹配對應的全局模板,再然後根據這個模板解析 Unit 文件,最後執行 Unit 文件裏的操作。接下來簡單介紹一下12種 Unit 文件類型:

(1)service:這是最明顯的單元類型,代表一個後臺守護進程,可以啓動、停止、重新啓動、重新加載守護進程,是最常用的一類 Unit 文件。

(2)socket:這個單元在文件系統或互聯網上封裝了一個套接字。目前 systemd 支持流、數據報和順序包類型的 AF_INET、AF_INET6、AF_UNIX 套接字。還支持經典的 FIFO 作爲傳輸。每個套接字單元都有一個匹配的服務單元,相應的服務在第一個連接進入套接字時就會啓動,例如:nscd.socket 在傳入連接上啓動 nscd.service。

(3)device:這個單元封裝了 Linux 設備樹中的一個設備。如果設備通過 udev 規則爲此標記,它將在 systemd 中作爲設備單元公開。使用 udev 設置的屬性可用作配置源來設置設備單元的依賴關係。

(4)mount:這個單元封裝了文件系統層次結構中的一個掛載點。systemd 監控所有掛載點,也可用於掛載或卸載掛載點。systemd 會將/etc/fstab 中的條目都轉換爲掛載點,並在開機時處理。

(5)automount:這個單元類型在文件系統層次結構中封裝了一個自動掛載點。每個自動掛載單元都有一個匹配的掛載單元,當該自動掛載點被訪問時,systemd 就會執行掛載點中定義的掛載行爲。

(6)target:這種單元類型用於單元的邏輯分組:它本身並不做任何事情,它只是引用其他單元,從而可以一起控制。比如:想讓系統進入圖形化模式,需要運行許多服務和配置命令,這些操作都由一個個的配置單元表示,將所有這些配置單元組合爲一個目標(target),就表示需要將這些配置單元全部執行一遍以便進入目標所代表的系統運行狀態。

(7)snapshot:類似於 target 單元,snapshot 本身實際上不做任何事情,它們的唯一目的是引用其他單元。快照可用於保存或回滾 init 系統的所有服務和單元的狀態。比如允許用戶臨時進入特定狀態,例如“緊急外殼”,終止當前服務,並提供一種簡單的方法返回之前的狀態。

(8)swap:和掛載配置單元類似,交換配置單元用來管理交換分區。用戶可以使用交換配置單元來定義系統中的交換分區,可以讓這些交換分區在啓動時被激活。

(9)timer:定時器配置單元用來定時觸發用戶定義的服務操作,是一種基於定時器的服務激活,這類配置單元取代了 atd、crond 等傳統的定時服務。

(10)path:這類配置單元用來監控指定目錄或者文件的變化,根據變化觸發其他配置單元服務的運行。

(11)scope:這個單元主要表示從 systemd 外部創建的進程。

(12)slice:此單元主要用於封裝管理一組進程資源佔用的控制組的 slice 單元,也就是主要用於 cgroup,它通過在 cgroup 中創建一個節點實現資源的控制,一般包含 scope 與 service 單元。

下面通過藍牙服務 bluetooth.service 介紹一下 Unit 文件的結構。

Unit 文件主要分爲以下三個大的部分:

  1. Unit 段:此部分所有 Unit 文件通用,用來定義 Unit 的元數據、配置以及與其他 Unit 的關係,Description 描述 Unit 文件信息,Documentation 表示指定服務的文檔,Condition 表示服務啓動的條件,有些 Unit 還包含 wants、before、after、require 字段,這些表示服務的一個依賴關係。

  2. Install 段:此部分所有 Unit 文件通用,通常指定運行目標的 target,使得服務在系統啓動時自動運行。Wantedby、RequiredBy 與 Unit 段 Wants 字段類似,表示依賴關係,Alias 字段表示啓動運行時使用的別名。

  3. service 段:服務(Service)類型的 Unit 文件特有的字段,用於定義服務的具體管理和執行動作。其中包括 Type 字段,定義進程的行爲,例如使用 fork()啓動,使用 dbus 啓動等等;ExecStart、ExecStartPre、ExecStartPos、ExecReload、ExecStop 分別表示啓動當前服務執行的命令、啓動當前服務之前執行的命令、啓動當前服務之後啓動的命令、重啓當前服務時執行的命令、停止當前服務時執行的命令。以上圖爲例,啓動藍牙服務所需要執行的命令爲/usr/lib/bluetooth/bluetoothd。

相關字段更詳細的描述可以參考 freedesktop 的 man 手冊: https://www.freedesktop.org/software/systemd/man/systemd.unit.html https://www.freedesktop.org/software/systemd/man/systemd.service.html

以優麒麟操作系統爲例,Unit 文件主要的存儲路徑如下:

system:

/etc/systemd/system/*

/run/systemd/system/*

/lib/systemd/system/*

user:

~/.config/systemd/user/*

/etc/systemd/user/*

$XDG_RUNTIME_DIR/systemd/user/*

/run/systemd/user/*

~/.local/share/systemd/user/*

/usr/lib/systemd/user/*

考慮篇幅問題,本期先帶來 systemd 的基礎介紹,下期將帶大家詳細瞭解開機啓動的過程中 systemd 的作用機制。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章