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

LINUX PID 1和SYSTEMD

提供服務 檢查 網絡 pst 數字 似的 進程啟動 通訊 ade

LINUX PID 1和SYSTEMD
http://coolshell.cn/articles/17998.html


要說清 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,前者存放各種進程的啟停腳本(需要按照規範支持 start,stop子命令),後者的 X 表示不同的 runlevel 下相應的後臺進程服務,如:/etc/rc3.d 是 runlevel=3 的。 裏面的文件主要是 link 到 /etc/init.d/ 裏的啟停腳本。其中也有一定的命名規範:S 或 K 打頭的,後面跟一個數字,然後再跟一個自定義的名字,如:S01rsyslog,S02ssh。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 分成三類,signal, method 和 hooks。signal 就是異步消息,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 ()系統調用作用在 “/misc/cd/file1” 的時候,/misc/cd 尚未執行掛載操作,此時 open () 調用被掛起等待,Linux 內核通知 autofs,autofs 執行掛載。這時候,控制權返回給 open () 系統調用,並正常打開文件。
  下圖來自 Lennart 的演講裏的一頁 PPT,展示了不同 init 系統的啟動。

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

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

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

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

我們知道, 傳統 Unix/Linux 的 Daemon 服務進程的最佳實踐基本上是這個樣子的 (具體過程可參看這篇文章“SysV Daemon”)——
進程啟動時,關閉所有的打開的文件描述符(除了標準描述符0,1,2),然後重置所有的信號處理。
調用 fork () 創建子進程,在子進程中 setsid (),然後父進程退出(為了後臺執行)
再子程中,再調用一次 fork (),創建孫子進程,確定沒有交互終端。然後子進程退出。
在孫子進程中,把標準輸入標準輸出標準錯誤都連到 /dev/null 上,還要創建 pid 文件,日誌文件,處理相關信號 ……
最後才是真正開始提供服務。
在上面的這個過程中,服務進程除了兩次 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 那樣並行啟動的話,那麽是不是就像是一個微服務的玩法了?

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

LINUX PID 1和SYSTEMD