1. 程式人生 > >LINUX PID 1 和 SYSTEMD

LINUX PID 1 和 SYSTEMD

要說清 Systemd,得先從Linux作業系統的啟動說起。Linux 作業系統的啟動首先從 BIOS 開始,然後由 Boot Loader 載入核心,並初始化核心。核心初始化的最後一步就是啟動 init 程序。這個程序是系統的第一個程序,PID 為 1,又叫超級程序,也叫根程序。它負責產生其他所有使用者程序。所有的程序都會被掛在這個程序下,如果這個程序退出了,那麼所有的程序都被 kill 。如果一個子程序的父程序退了,那麼這個子程序會被掛到 PID 1 下面。(注:PID 0 是核心的一部分,主要用於內進換頁,參看:Process identifier

SysV Init

PID 1 這個程序非常特殊,其主要就任務是把整個作業系統帶入可操作的狀態。比如:啟動 UI – Shell 以便進行人機互動,或者進入 X 圖形視窗。傳統上,PID 1 和傳統的 Unix System V 相相容的,所以也叫 sysvinit

,這是使用得最悠久的 init 實現。Unix System V 於1983年 release。

在 sysvint 下,有好幾個執行模式,又叫 runlevel。比如:常見的 3 級別指定啟動到多使用者的字元命令列介面,5 級別指定啟起到圖形介面,0 表示關機,6 表示重啟。其配置在 /etc/inittab 檔案中。

 

與此配套的還有 /etc/init.d/ 和 /etc/rc[X].d,前者存放各種程序的啟停指令碼(需要按照規範支援 startstop子命令),後者的 X 表示不同的 runlevel 下相應的後臺程序服務,如:/etc/rc3.d

 是 runlevel=3 的。 裡面的檔案主要是 link 到  /etc/init.d/ 裡的啟停指令碼。其中也有一定的命名規範:S 或 K 打頭的,後面跟一個數字,然後再跟一個自定義的名字,如:S01rsyslogS02ssh。S 表示啟動,K表示停止,數字表示執行的順序。

UpStart

Unix 和 Linux 在 sysvint 運作多年後,大約到了2006年的時候,Linux核心進入2.6時代,Linux有了很多更新。並且,Linux開始進入桌面系統,而桌面系統和伺服器系統不一樣的是,桌面系統面臨頻繁重啟,而且,使用者會非常頻繁的使用硬體的熱插拔技術。於是,這些新的場景,讓 sysvint

 受到了很多挑戰。

比如,印表機需要CUPS等服務程序,但是如果使用者沒有打機印,啟動這個服務完全是一種浪費,而如果不啟動,如果要用印表機了,就無法使用,因為sysvint 沒有自動檢測的機制,它只能一次性啟動所有的服務。另外,還有網路盤掛載的問題。在 /etc/fstab中,負責硬碟掛載,有時候還有網路硬碟(NFS 或 iSCSI)在其中,但是在桌面機上,有很可能開機的時候是沒有網路的, 於是網路硬碟都不可以訪問,也無法掛載,這會極大的影響啟動速度。sysvinit 採用 netdev 的方式來解決這個問題,也就是說,需要使用者自己在 /etc/fstab 中給相應的硬碟配置上 netdev 屬性,於是 sysvint 啟動時不會掛載它,只有在網路可用後,由專門的 netfs 服務程序來掛載。這種管理方式比較難以管理,也很容易讓人掉坑。

所以,Ubuntu 開發人員在評估了當時幾個可選的 init 系統後,決定重新設計這個系統,於是,這就是我們後面看到的 upstart 。 upstart 基於事件驅動的機制,把之前的完全序列的同步啟動服務的方式改成了由事件驅動的非同步的方式。比如:如果有U盤插入,udev 得到通知,upstart 感知到這個事件後觸發相應的服務程式,比如掛載檔案系統等等。因為使用一個事件驅動的玩法,所以,啟動作業系統時,很多不必要的服務可以不用啟動,而是等待通知,lazy 啟動。而且事件驅動的好處是,可以並行啟動服務,他們之間的依賴關係,由相應的事件通知完成。

upstart 有著很不錯的設計,其中最重要的兩個概念是 Job 和 Event。

Job 有一般的Job,也有service的Job,並且,upstart 管理了整個 Job 的生命週期,比如:Waiting, Starting, pre-Start, Spawned, post-Start, Running, pre-Stop, Stopping, Killed, post-Stop等等,並維護著這個生命週期的狀態機。

Event 分成三類,signalmethod 和 hookssignal 就是非同步訊息,method 是同步阻塞的。hooks 也是同步的,但介於前面兩者之間,發出hook事件的程序必須等到事件完成,但不檢查是否成功。

但是,upstart 的事件非常複雜,也非常紛亂,各種各樣的事件(事件沒有歸好類)導致有點凌亂。不過因為整個事件驅動的設計比之前的 sysvinit 來說好太多,所以,也深得歡迎。

Systemd

直到2010的有一天,一個在 RedHat工作的工程師 Lennart Poettering 和 Kay Sievers ,開始引入了一個新的 init 系統—— systemd。這是一個非常非常有野心的專案,這個專案幾乎改變了所有的東西,systemd 不但想取代已有的 init 系統,而且還想幹更多的東西。

Lennart 同意 upstart 乾的不錯,程式碼質量很好,基於事件的設計也很好。但是他覺得 upstart 也有問題,其中最大的問題還是不夠快,雖然 upstart 用事件可以達到一定的啟動並行度,但是,本質上來說,這些事件還是會讓啟動過程序列在一起。  如:NetworkManager 在等 D-Bus 的啟動事件,而 D-Bus 在等 syslog 的啟動事件。

Lennart 認為,實現上來說,upstart 其實是在管理一個邏輯上的服務依賴樹,但是這個服務依賴樹在表現形式上比較簡單,你只需要配置——“啟動 B好了就啟動A”或是“停止了A後就停止B”這樣的規則。但是,Lennart 說,這種簡單其實是有害的(this simplification is actually detrimental)。他認為,

  • 從一個系統管理的角度出來,他一開始會設定好整個系統啟動的服務依賴樹,但是這個系統管理員要人肉的把這個本來就非常乾淨的服務依整樹給翻譯成計算機看的懂的 Event/Action 形式,而且 Event/Action 這種配置方式是執行時的,所以,你需要執行起來才知道是什麼樣的。
  • Event邏輯從頭到腳到處都是,這個事件擴大了運維的複雜度,還不如之前的 sysvint。 也就是說,當用戶配置了 “啟動 D-Bus 後請啟動 NetworkManager”, 這個 upstart 可以幹,但是反過來,如果,使用者啟動 NetworkManager,我們應該先去啟動他的前置依賴 D-Bus,然而你還要配置相應的反向 Event。本來,我只需要配置一條依賴的,結果現在我要配置很多很多情況下的Event。
  • 最後,upstart 裡的 Event 的並不標準,很混亂,沒有良好的定義。比如:既有,程序啟動,執行,停止的事件,也有USB裝置插入、可用、拔出的事件,還有檔案系統裝置being mounted、 mounted 和 umounted 的事件,還有AC電源線連線和斷開的事件。你會發現,這程序啟停的、USB的、檔案系統的、電源線的事件,看上去長得很像, 但是沒有被標準化抽像出來掉,因為絕大多數的事件都是三元組:start, condition, stop 。這種概念設計模型並沒有在 upstart 中出現。因為 upstart 被設計為單一的事件,而忽略了邏輯依賴。

當然,如果 systemd 只是解決 upstart 的問題,他就改造 upstart 就好了,但是 Lennart 的野心不只是想幹個這樣的事,他想幹的更多。

首先,systemd 清醒的認識到了 init 程序的首要目標是要讓使用者快速的進入可以操作OS的環境,所以,這個速度一定要快,越快越好,所以,systemd 的設計理念就是兩條:

  • To start less.
  • And to start more in parallel.

也就是說,按需啟動,能不啟動就不啟動,如果要啟動,能並行啟動就並行啟動,包括你們之間有依賴,我也並行啟動。按需啟動還好理解,那麼,有依賴關係的並行啟動,它是怎麼做到的?這裡,systemd 借鑑了 MacOS 的 Launchd 的玩法(在Youtube上有一個分享——Launchd: One Program to Rule them All,在蘋果的開源網站上也有相關的設計文件——About Daemons and Services

要解決這些依賴性,systemd 需要解決好三種底層依賴—— Socket, D-Bus ,檔案系統。

  • Socket依賴。如果服務C依賴於服務S的socket,那麼就要先啟動S,然後再啟動C,因為如果C啟動時找不到S的Socket,那麼C就會失敗。systemd 可以幫你在S還沒有啟動好的時候,建立一個socket,用來接收所有的C的請求和資料,並快取之,一旦S全部啟動完成,把systemd替換好的這個快取的資料和Socket描述符替換過去。

 

  • D-Bus依賴D-Bus 全稱 Desktop Bus,是一個用來在程序間通訊的服務。除了用於使用者態程序和核心態程序通訊,也用於使用者態的程序之前。現在,很多的現在的服務程序都用 D-Bus 而不是Socket來通訊。比如:NetworkManager 就是通過 D-Bus和其它服務程序通訊的,也就是說,如果一個程序需要知道網路的狀態,那麼就必需要通過 D-Bus 通訊。D-Bus 支援 “Bus Activation”的特性。也就是說,A要通過 D-Bus 服務和B通訊,但是B沒有啟動,那麼 D-Bus 可以把B起來,在B啟動的過程中,D-Bus 幫你快取資料。systemd 可以幫你利用好這個特性來並行啟動 A 和 B。

 

  • 檔案系統依賴。系統啟動過程中,檔案系統相關的活動是最耗時的,比如掛載檔案系統,對檔案系統進行磁碟檢查(fsck),磁碟配額檢查等都是非常耗時的操作。在等待這些工作完成的同時,系統處於空閒狀態。那些想使用檔案系統的服務似乎必須等待檔案系統初始化完成才可以啟動。systemd 參考了 autofs 的設計思路,使得依賴檔案系統的服務和檔案系統本身初始化兩者可以併發工作。autofs 可以監測到某個檔案系統掛載點真正被訪問到的時候才觸發掛載操作,這是通過核心 automounter 模組的支援而實現的。比如一個 open() 系統呼叫作用在某個檔案系統上的時候,而這個檔案系統尚未執行掛載,此時 open() 呼叫被核心掛起等待,等到掛載完成後,控制權返回給 open() 系統呼叫,並正常開啟檔案。這個過程和 autofs 是相似的。

 

下圖來自 Lennart 的演講裡的一頁PPT,展示了不同 init 系統的啟動。

除此之外,systemd 還在啟動時管理好了一些下面的事。

用C語言取代傳統的指令碼式的啟動。前面說過,sysvint 用 /etc/rcX.d 下的各種指令碼啟動。然而這些指令碼中需要使用 awksedgrepfindxargs 等等這些作業系統的命令,這些命令需要生成程序,生成程序的開銷很大,關鍵是生成完這些程序後,這個程序就幹了點屁大的事就退了。換句話說就是,我作業系統幹了那麼多事為你拉個程序起來,結果你就把個字串轉成小寫就退了,把我作業系統當什麼了?

在正常的一個 sysvinit 的腳本里,可能會有成百上千個這樣的命令。所以,慢死。因此,systemd 全面用 C 語言全部取代了。一般來說,sysvinit 下,作業系統啟動完成後,用 echo $$ 可以看到,pid 被分配到了上千的樣子,而 systemd 的系統只是上百。

另外,systemd 是真正一個可以管住服務程序的——可以跟蹤上服務程序所fork/exec出來的所有程序。

  • 我們知道, 傳統 Unix/Linux 的 Daemon 服務程序的最佳實踐基本上是這個樣子的 (具體過程可參看這篇文章“SysV Daemon”)——
    1. 程序啟動時,關閉所有的開啟的檔案描述符(除了標準描述符0,1,2),然後重置所有的訊號處理。
    2. 呼叫 fork() 建立子程序,在子程序中 setsid(),然後父程序退出(為了後臺執行)
    3. 在子程序中,再呼叫一次 fork(),建立孫子程序,確定沒有互動終端。然後子程序退出。
    4. 在孫子程序中,把標準輸入標準輸出標準錯誤都連到 /dev/null 上,還要建立 pid 檔案,日誌檔案,處理相關訊號 ……
    5. 最後才是真正開始提供服務。

 

  • 在上面的這個過程中,服務程序除了兩次 fork 外還會 fork 出很多很多的子程序(比如說一些Web服務程序,會根據使用者的請求連結來 fork 子程序),這個程序樹是相當難以管理的,因為,一旦父程序退出來了,子程序就會被掛到 PID 1下,所以,基本上來說,你無法通過服務程序自已給定的一個pid檔案來找到所有的相關程序(這個對開發者的要求太高了),所以,在傳統的方式下用指令碼啟停服務是相當相當的 Buggy 的,因為無法做對所有的服務生出來的子子孫孫做到監控。

 

  • 為了解決這個問題,upstart 通過變態的 strace 來跟蹤程序中的 fork() 和 exec() 或 exit() 等相關的系統呼叫。這種方法相當笨拙。 systemd 使用了一個非常有意思的玩法來 tracking 服務程序生出來的所有程序,那就是用 cgroup (我在 Docker 的基礎技術“cgroup篇”中講過這個東西)。cgroup主要是用來管理程序組資源配額的事,所以,無論服務如何啟動新的子程序,所有的這些相關程序都會同屬於一個 cgroup,所以,systemd 只需要簡單的去遍歷一下相應的 cgroup 的那個虛檔案系統目錄下的檔案,就可以正確的找到所有的相關程序,並將他們一一停止。

 

另外,systemd 簡化了整個 daemon 開發的過程:

  • 不需要兩次 fork(),只需要實現服務本身的主邏輯就可以了。
  • 不需要 setsid()systemd 會幫你幹
  • 不需要維護 pid檔案systemd 會幫處理。
  • 不需要管理日誌檔案或是使用syslog,或是處理HUP的日誌reload訊號。把日誌打到 stderr 上,systemd 幫你管理。
  • 處理 SIGTERM 訊號,這個訊號就是正確退出當前服務,不要做其他的事。
  • ……

除此之外,systemd 還能——

  • 自動檢測啟動的服務間有沒有環形依賴。
  • 內建 autofs 自動掛載管理功能。
  • 日誌服務。systemd 改造了傳統的 syslog 的問題,採用二進位制格式儲存日誌,日誌索引更快。
  • 快照和恢復。對當前的系統執行的服務集合做快照,並可以恢復。
  • ……

還有好多好多,他接管很多很多東西,於是就讓很多人不爽了,因為他在幹了很多本不屬於 PID 1 的事。

Systemd 爭論和八卦

於是 systemd 這個東西成了可能是有史以來口水戰最多的一個開源軟體了。systemd 飽受各種爭議,最大的爭議就是他破壞了 Unix 的設計哲學(相關的哲學可以讀一下《Unix程式設計藝術》),幹了一個大而全而且相當複雜的東西。當然,Lennart 並不同意這樣的說法,他後來又寫一篇blog “The Biggest Myths”來解釋 systemd 並不是這樣的,大家可以前往一讀。

這個爭議大到什麼樣子呢?2014 年,Debian Linux 因為想準備使用 systemd 來作為標準的 init 守護程序來替換 sysvinit 。而圍繞這個事的爭論達到了空前的熱度,爭論中充滿著仇恨,systemd 的支持者和反對者都在互相辱罵,導致當時 Debian 陣營開始分裂。還有人給 Lennart 發了死亡威脅的郵件,用比特幣僱凶買殺手,揚言要取他的性命,在Youbute上傳了侮辱他的歌曲,在IRC和各種社交渠道上給他發下流和侮辱性的訊息。這已經不是爭議了,而是一種不折不扣的仇恨!

於是,Lennart 在 Google Plus 上發了貼子,批評整個 Linux 開源社群和 Linus 本人。他大意說,

這個社群太病態了,全是 ass holes,你們不停用各種手段在各種地方用不同的語言和方式來侮辱和漫罵我。我還是一個年輕人,我從來沒有經歷過這樣的場面,但是今天我已經對這種場面很熟悉了。我有時候說話可能不準確,但是我不會像他樣那樣說出那樣的話,我也沒有被這些事影響,因為我臉皮夠厚,所以,為什麼我可以在如何大的反對聲面前讓 systemd 成功,但是,你們 Linux 社群太可怕了。你們裡面的有精神病的人太多了。另外,對於Linus Torvalds,你是這個社群的 Role Model,但可惜你是一個 Bad Role Model,你在社群裡的刻薄和侮辱性的言行,基本從一定程度上鼓勵了其它人跟你一樣,當然,並不只是你一個人的問題,而是在你周圍聚集了一群和你一樣的這樣乾的人。送你一句話—— A fish rots from the head down !一條魚是從頭往下腐爛的……

這篇契文很長,喜歡八卦的同學可以前往一讀。感受一下 Lennart 當時的心態(我覺得能算上是非常平穩了)。

Linus也在被一媒體問起 systemd 這個事來(參看“Torvalds says he has no strong opinions on systemd”),Linus在採訪裡說,

我對 systemd 和 Lennart 的貼子沒有什麼強烈的想法。雖然,傳統的 Unix 設計哲學—— “Do one thing and Do it well”,很不錯,而且我們大多數人也實踐了這麼多年,但是這並不代表所有的真實世界。在歷史上,也不只有systemd 這麼幹過。但是,我個人還是 old-fashioned 的人,至少我喜歡文字式的日誌,而不是二進位制的日誌。但是 systemd 沒有必要一定要有這樣的品味。哦,我說細節了……

今天,systemd 佔據了幾乎所有的主流的 Linux 發行版的預設配置,包括:Arch Linux、CentOS、CoreOS、Debian、Fedora、Megeia、OpenSUSE、RHEL、SUSE企業版和 Ubuntu。而且,對於 CentOS, CoreOS, Fedora, RHEL, SUSE這些發行版來說,不能沒有 systemd。(Ubuntu 還有一個不錯的wiki – Systemd for Upstart Users 闡述瞭如何在兩者間切換)

 

其它

還記得在《快取更新的套路》一文中,我說過,如果你要做好架構,首先你得把計算機體系結構以及很多老古董的基礎技術吃透了。因為裡面會有很多可以借鑑和相通的東西。那麼,你是否從這篇文章裡看到了一些有分散式架構相似的東西?

比如:從 sysvinit 到 upstart 再到 systemd,像不像是服務治理?Linux系統下的這些服務程序,是不是很像分散式架構中的微服務?還有那個D-Bus,是不是很像SOA裡的ESB?而 init 系統是不是很像一個控制系統?甚至像一個服務編排(Service Orchestration)系統?

分散式系統中的服務之間也有很多依賴,所以,在啟動一個架構的時候,如果我們可以做到像 systemd 那樣並行啟動的話,那麼是不是就像是一個微服務的玩法了?

嗯,你會發現,技術上的很多東西是相通的,也是互相有對方的影子,所以,其實技術並不多。關鍵是我們學在了表面還是看到了本質。

 

延伸閱讀

(全文完)