從 init 系統說起
linux 操作系統的啟動首先從 BIOS 開始,接下來進入 boot loader,由 bootloader 載入內核,進行內核初始化。內核初始化的最后一步就是啟動 PID 為 1 的 init 進程。這個進程是系統的第一個進程。它負責產生其他所有的用戶進程。init 進程以守護進程(也就是服務)的方式存在,是所有其他進程的祖先。init 進程非常獨特,能夠完成其他進程無法完成的任務。
init 系統能夠定義、管理和控制 init 進程的行為。它負責組織和運行許多獨立的或相關的初始化工作(因此被稱為 init 系統),從而讓計算機系統進入某種用戶預定義的運行模式,比如命令行模式或圖形界面模式 。
對於一個操作系統而言,僅僅將內核運行起來是毫無實際用途的,必須由 init 系統將操作系統初始化為可操作的狀態。比如啟動 shell 后,便有了人機交互,這樣就可以讓計算機執行一些程序完成有實際意義的任務。或者啟動 X 圖形系統以便提供更佳的人機界面,更加高效的完成任務。這里,字符界面的 shell 或者 X 系統都是一種預設的運行模式。
隨着計算機系統軟硬件的發展,init 系統也在不斷的發展變化之中。大體上的演進路線為 sysvinit -> upstart -> systemd。雖然本文的目的是要介紹 systemd,但是筆者覺得如果能從歷史發展的角度觀察 init 系統的演進,將會幫助我們更好的理解、使用 systemd。
sysvinit
sysvinit 就是 System V 風格的 init 系統,顧名思義,它源於 System V 系列的 UNIX。最初的 linux 發行版幾乎都是采用 sysvinit 作為 init 系統。sysvinit 用術語 runlevel 來定義 "預訂的運行模式"。比如 runlevel 3 是命令行模式,runlevel 5 是圖形界面模式,runlevel 0 是關機,runlevel 6 是重啟。sysvinit 會按照下面的順序按部就班的初始化系統:
- 激活 udev 和 selinux
- 設置定義在 /etc/sysctl.conf 中的內核參數
- 設置系統時鍾
- 加載 keymaps
- 啟用交換分區
- 設置主機名(hostname)
- 根分區檢查和 remount
- 激活 RAID 和 LVM 設備
- 開啟磁盤配額
- 檢查並掛載所有文件系統
- 清除過期的 locks 和 PID 文件
- 最后找到指定 runlevel 下的腳本並執行,其實就是啟動服務。
除了負責初始化系統,sysvinit 還要負責關閉系統,主要是在系統關閉是為了保證數據的一致性,需要小心地按照順序進行任務的結束和清理工作。另外,sysvinit 還提供了很多管理和控制系統的命令,比如 halt、init、mesg、shutdown、reboot 等等。
sysvinit 的優點是概念簡單。特別是服務(service)的配置,只需要把啟動/停止服務的腳本鏈接接到合適的目錄就可以了。
sysvinit 的另一個重要優點是確定的執行順序,腳本嚴格按照順序執行(sysvinit 靠腳本來初始化系統),一個執行完畢再執行下一個,這非常有益於錯誤排查。
同時,完全順序執行任務也是 sysvinit 最致命的缺陷。如果 linux 系統只用於服務器系統,那么漫長的啟動過程可能並不是什么問題,畢竟我們是不會經常重啟服務器的。但是現在 linux 被越來越多的用在了桌面系統中,漫長的啟動過程對桌面用戶來說是不能接受的。除了啟動慢,sysvinit 還有一些其它的缺陷,比如不能很好的處理即插即用的設備,對網絡共享磁盤的掛載也存在一定的問題,於是 init 系統開始了它的進化之旅。
upstart
由於 sysvinit 系統的種種弊端,ubuntu 的開發人員決定重新設計和開發一個全新的 init 系統,即 upstart 。upstart 是第一個被廣泛應用的新一代 init 系統。
upstart 基於事件機制,比如 U 盤插入 USB 接口后,udev 得到內核通知,發現該設備,這就是一個新的事件。upstart 在感知到該事件之后觸發相應的等待任務,比如處理 /etc/fstab 中存在的掛載點。采用這種事件驅動的模式,upstart 完美地解決了即插即用設備帶來的新問題。采用事件驅動機制也帶來了一些其它有益的變化,比如加快了系統啟動時間。sysvinit 運行時是同步阻塞的。一個腳本運行的時候,后續腳本必須等待。這意味着所有的初始化步驟都是串行執行的,而實際上很多服務彼此並不相關,完全可以並行啟動,從而減小系統的啟動時間。
upstart 的特點
upstart 解決了之前提到的 sysvinit 的缺點。采用事件驅動模型的 upstart 可以:
- 更快地啟動系統
- 當新硬件被發現時動態啟動服務
- 硬件被拔除時動態停止服務
這些特點使得 upstart 可以很好地應用在桌面或者便攜式系統中,處理這些系統中的動態硬件插拔特性。
主角 systemd 登場
systemd 是 linux 系統中最新的初始化系統(init),它主要的設計目標是克服 sysvinit 固有的缺點,提高系統的啟動速度。systemd 和 ubuntu 的 upstart 是競爭對手,但是時至今日 ubuntu 也采用了 systemd,所以 systemd 在競爭中勝出,大有一統天下的趨勢。其實,systemd 的很多概念都來源於蘋果 Mac OS 操作系統上的 launchd。
systemd 的優點是功能強大,使用方便,缺點是體系龐大,非常復雜,下圖展示了 systemd 的架構(此圖來自互聯網):
systemd 能夠在與 upstart 的競爭中勝出自然有很多過人之處,接下來讓我們介紹一些 systemd 的主要優點。
兼容性
systemd 提供了和 sysvinit 兼容的特性。系統中已經存在的服務和進程無需修改。這降低了系統向 systemd 遷移的成本,使得 systemd 替換現有初始化系統成為可能。
啟動速度
systemd 提供了比 upstart 更激進的並行啟動能力,采用了 socket / D-Bus activation 等技術啟動服務。一個顯而易見的結果就是:更快的啟動速度。為了減少系統啟動時間,systemd 的目標是:
- 盡可能啟動更少的進程
- 盡可能將更多進程並行啟動
同樣地,upstart 也試圖實現這兩個目標。下圖展示了 upstart 相對於 sysvinit 在並發啟動這個方面的改進(此圖來自互聯網):
upstart 增加了系統啟動的並行性,從而提高了系統啟動速度。但是在 upstart 中,有依賴關系的服務還是必須先后啟動。比如任務 A,B,(C,D)因為存在依賴關系,所以在這個局部,還是串行執行。
systemd 能夠更進一步提高並發性,即便對於那些 upstart 認為存在相互依賴而必須串行的服務,比如 Avahi 和 D-Bus 也可以並發啟動。從而實現如下圖所示的並發啟動過程(此圖來自互聯網):
在 systemd 中,所有的任務都同時並發執行,總的啟動時間被進一步降低為 T1。可見 systemd 比 upstart 更進一步提高了並行啟動能力,極大地加速了系統啟動時間。
systemd 提供按需啟動能力
當 sysvinit 系統初始化的時候,它會將所有可能用到的后台服務進程全部啟動運行。並且系統必須等待所有的服務都啟動就緒之后,才允許用戶登錄。這種做法有兩個缺點:首先是啟動時間過長,其次是系統資源浪費。
某些服務很可能在很長一段時間內,甚至整個服務器運行期間都沒有被使用過。比如 CUPS,打印服務在多數服務器上很少被真正使用到。您可能沒有想到,在很多服務器上 SSHD 也是很少被真正訪問到的。花費在啟動這些服務上的時間是不必要的;同樣,花費在這些服務上的系統資源也是一種浪費。
systemd 可以提供按需啟動的能力,只有在某個服務被真正請求的時候才啟動它。當該服務結束,systemd 可以關閉它,等待下次需要時再次啟動它。
這有點類似於以前系統中的 inetd,並且有很多文章介紹如何把過去 inetd 管理的服務遷移到 systemd。
采用 linux 的 cgroups 跟蹤和管理進程的生命周期
systemd 利用了 Linux 內核的特性即 cgroups 來完成跟蹤的任務。當停止服務時,通過查詢 cgroups ,systemd 可以確保找到所有的相關進程,從而干凈地停止服務。
cgroups 已經出現了很久,它主要用來實現系統資源配額管理。cgroups 提供了類似文件系統的接口,使用方便。當進程創建子進程時,子進程會繼承父進程的 cgroups 。因此無論服務如何啟動新的子進程,所有的這些相關進程都會屬於同一個 cgroups ,systemd 只需要簡單地遍歷指定的 cgroups 即可正確地找到所有的相關進程,將它們一一停止即可。
啟動掛載點和自動掛載的管理
傳統的 linux 系統中,用戶可以用 /etc/fstab 文件來維護固定的文件系統掛載點。這些掛載點在系統啟動過程中被自動掛載,一旦啟動過程結束,這些掛載點就會確保存在。這些掛載點都是對系統運行至關重要的文件系統,比如 HOME 目錄。和 sysvinit 一樣,Systemd 管理這些掛載點,以便能夠在系統啟動時自動掛載它們。systemd 還兼容 /etc/fstab 文件,您可以繼續使用該文件管理掛載點。
有時候用戶還需要動態掛載點,比如打算訪問 DVD 或者 NFS 共享的內容時,才臨時執行掛載以便訪問其中的內容,而不訪問光盤時該掛載點被取消(umount),以便節約資源。傳統地,人們依賴 autofs 服務來實現這種功能。
systemd 內建了自動掛載服務,無需另外安裝 autofs 服務,可以直接使用 systemd 提供的自動掛載管理能力來實現 autofs 的功能。
實現事務性依賴關系管理
系統啟動過程是由很多的獨立工作共同組成的,這些工作之間可能存在依賴關系,比如掛載一個 NFS 文件系統必須依賴網絡能夠正常工作。systemd 雖然能夠最大限度地並發執行很多有依賴關系的工作,但是類似"掛載 NFS"和"啟動網絡"這樣的工作還是存在天生的先后依賴關系,無法並發執行。對於這些任務,systemd 維護一個"事務一致性"的概念,保證所有相關的服務都可以正常啟動而不會出現互相依賴,以至於死鎖的情況。
日志服務
systemd 自帶日志服務 journald,該日志服務的設計初衷是克服現有的 syslog 服務的缺點。比如:
- syslog 不安全,消息的內容無法驗證。每一個本地進程都可以聲稱自己是 Apache PID 4711,而 syslog 也就相信並保存到磁盤上。
- 數據沒有嚴格的格式,非常隨意。自動化的日志分析器需要分析人類語言字符串來識別消息。一方面此類分析困難低效;此外日志格式的變化會導致分析代碼需要更新甚至重寫。
systemd journal 用二進制格式保存所有日志信息,用戶使用 journalctl 命令來查看日志信息。無需自己編寫復雜脆弱的字符串分析處理程序。
systemd journal 的優點如下:
簡單性:代碼少,依賴少,抽象開銷最小。
零維護:日志是除錯和監控系統的核心功能,因此它自己不能再產生問題。舉例說,自動管理磁盤空間,避免由於日志的不斷產生而將磁盤空間耗盡。
移植性:日志文件應該在所有類型的 Linux 系統上可用,無論它使用的何種 CPU 或者字節序。
性能:添加和瀏覽日志非常快。
最小資源占用:日志數據文件需要較小。
統一化:各種不同的日志存儲技術應該統一起來,將所有的可記錄事件保存在同一個數據存儲中。所以日志內容的全局上下文都會被保存並且可供日后查詢。例如一條固件記錄后通常會跟隨一條內核記錄,最終還會有一條用戶態記錄。重要的是當保存到硬盤上時這三者之間的關系不會丟失。syslog 將不同的信息保存到不同的文件中,分析的時候很難確定哪些條目是相關的。
擴展性:日志的適用范圍很廣,從嵌入式設備到超級計算機集群都可以滿足需求。
安全性:日志文件是可以驗證的,讓無法檢測的修改不再可能。
在了解了 systemd 的種種優勢之后讓我們開始認識它的一些基本概念。
unit(單元)
系統初始化需要做的事情非常多。需要啟動后台服務,比如啟動 ssh 服務;需要做配置工作,比如掛載文件系統。這個過程中的每一步都被 systemd 抽象為一個配置單元,即 unit。可以認為一個服務是一個配置單元,一個掛載點是一個配置單元,一個交換分區的配置是一個配置單元等等。systemd 將配置單元歸納為以下一些不同的類型。然而,systemd 正在快速發展,新功能不斷增加。所以配置單元類型可能在不久的將來繼續增加。下面是一些常見的 unit 類型:
service :代表一個后台服務進程,比如 mysqld。這是最常用的一類。
socket :此類配置單元封裝系統和互聯網中的一個套接字 。當下,systemd 支持流式、數據報和連續包的 AF_INET、AF_INET6、AF_UNIX socket 。每一個套接字配置單元都有一個相應的服務配置單元 。相應的服務在第一個"連接"進入套接字時就會啟動(例如:nscd.socket 在有新連接后便啟動 nscd.service)。
device :此類配置單元封裝一個存在於 Linux 設備樹中的設備。每一個使用 udev 規則標記的設備都將會在 systemd 中作為一個設備配置單元出現。
mount :此類配置單元封裝文件系統結構層次中的一個掛載點。Systemd 將對這個掛載點進行監控和管理。比如可以在啟動時自動將其掛載;可以在某些條件下自動卸載。Systemd 會將 /etc/fstab 中的條目都轉換為掛載點,並在開機時處理。
automount :此類配置單元封裝系統結構層次中的一個自掛載點。每一個自掛載配置單元對應一個掛載配置單元 ,當該自動掛載點被訪問時,systemd 執行掛載點中定義的掛載行為。
swap:和掛載配置單元類似,交換配置單元用來管理交換分區。用戶可以用交換配置單元來定義系統中的交換分區,可以讓這些交換分區在啟動時被激活。
target :此類配置單元為其他配置單元進行邏輯分組。它們本身實際上並不做什么,只是引用其他配置單元而已。這樣便可以對配置單元做一個統一的控制。這樣就可以實現大家都已經非常熟悉的運行級別概念。比如想讓系統進入圖形化模式,需要運行許多服務和配置命令,這些操作都由一個個的配置單元表示,將所有這些配置單元組合為一個目標(target),就表示需要將這些配置單元全部執行一遍以便進入目標所代表的系統運行狀態。 (例如:multi-user.target 相當於在傳統使用 SysV 的系統中運行級別 5)
timer:定時器配置單元用來定時觸發用戶定義的操作,這類配置單元取代了 atd、crond 等傳統的定時服務。
snapshot :與 target 配置單元相似,快照是一組配置單元。它保存了系統當前的運行狀態。
path:文件系統中的一個文件或目錄。
scope:用於 cgroups,表示從 systemd 外部創建的進程。
slice:用於 cgroups,表示一組按層級排列的單位。slice 並不包含進程,但會組建一個層級,並將 scope 和 service 都放置其中。
每個配置單元都有一個對應的配置文件,系統管理員的任務就是編寫和維護這些不同的配置文件,比如一個 MySQL 服務對應一個 mysql.service 文件。這種配置文件的語法非常簡單,用戶不需要再編寫和維護復雜的系統腳本了。
依賴關系
雖然 systemd 將大量的啟動工作解除了依賴,使得它們可以並發啟動。但還是存在有些任務,它們之間存在天生的依賴,不能用"套接字激活"(socket activation)、D-Bus activation 和 autofs 三大方法來解除依賴。比如:掛載必須等待掛載點在文件系統中被創建;掛載也必須等待相應的物理設備就緒。為了解決這類依賴問題,systemd 的配置單元之間可以彼此定義依賴關系。Systemd 用配置單元定義文件中的關鍵字來描述配置單元之間的依賴關系。比如:unit A 依賴 unit B,可以在 unit B 的定義中用"require A"來表示。這樣 systemd 就會保證先啟動 A 再啟動 B。
systemd 事務
systemd 能保證事務完整性。Systemd 的事務概念和數據庫中的有所不同,主要是為了保證多個依賴的配置單元之間沒有環形引用。比如存在 unit A、B、C,假如它們的依賴關系如下(此圖來自互聯網):
存在循環依賴,那么 systemd 將無法啟動任意一個服務。此時 systemd 將會嘗試解決這個問題,因為配置單元之間的依賴關系有兩種:required 是強依賴;want 則是弱依賴,systemd 將去掉 wants 關鍵字指定的依賴看看是否能打破循環。如果無法修復,systemd 會報錯。systemd 能夠自動檢測和修復這類配置錯誤,從而極大地減輕了管理員的排錯負擔。
target 和運行級別
systemd 用目標(target)替代了運行級別的概念,提供了更大的靈活性,如您可以繼承一個已有的目標,並添加其它服務,來創建自己的目標。下表列舉了 systemd 中的 target 和 sysvinit 中常見的 runlevel 的對應關系:
sysvinit runlevel | systemd target | 描述 |
0 | poweroff.target | 關閉系統。 |
1,s,single | rescue.target | 單用戶模式。 |
2,4 | multi-user.target | 用戶定義/域特定運行級別。默認等同於 3。 |
3 | multi-user.target | 多用戶,非圖形化。用戶可以通過多個控制台或網絡登錄。 |
5 | graphical.target | 多用戶,圖形化。通常為所有運行級別 3 的服務外加圖形化登錄。 |
6 | reboot.target | 重啟。 |
emergency | emergency.target | 緊急 Shell。 |
總結
本文簡要的介紹了 init 系統的發展歷史,並概要的介紹了 systemd 的基本概念。由於相比其它的 init 系統優勢巨大,所以 systemd 已經被各大 linux 版本接受,並有望在 linux init 系統中一統天下。
參考:
淺析 Linux 初始化 init 系統:sysvinit
淺析 Linux 初始化 init 系統:upstart
淺析 Linux 初始化 init 系統:systemd