Linux 內核裁剪框架初探
大約是在2000年的時候,老碼農還很年輕,當時希望將Linux 作為手機的操作系統, 于是才有了進行內核裁剪的想法并輔助實踐,效果尚好,已經能在PDA上執行手機的功能了。一晃20多年過去了,Linux 已經有了太大的變化,內核裁剪的技術和方式也有了較大的不同。
Linux 的內核裁剪是為了減少目標應用中不需要的內核代碼,在安全性和高性能(快速啟動時間和減少內存占用)方面有著顯著的好處。但是,現有的內核裁剪技術有其局限性,有沒有內核裁剪的框架化方法呢?
1. 關于內核裁剪
近年來,Linux操作系統在復雜性和規模上都在增長。然而,一個應用程序通常只需要一部分 OS 功能,眾多的應用需求導致了Linux內核的膨脹。操作系統的內核膨脹同樣導致了安全性隱患、啟動時間變長和內存使用的增加。
隨著服務化和微服務的流行,進一步提出了對內核裁剪的需求。在這些場景中,虛擬機運行小型應用程序,每個應用程序往往是“微型”的,內核占用較小,一些虛擬化技術要為目標應用程序提供最簡單的 Linux 內核。
鑒于操作系統的復雜性,通過手工挑選內核特性來裁剪內核有些不切實際。例如,Linux 有超過14,000+個配置選項(截至 v4.14) ,每年都會引入數百個新選項。內核配置器(例如 KConfig)只提供用于選擇配置選項的用戶界面。鑒于糟糕的可用性和文檔的不完整性,用戶很難選擇最小且實用的內核配置。
現有的內核裁剪技術一般遵循三個步驟:
- 運行目標應用程序的工作負載并跟蹤在應用程序運行期間執行的內核代碼;
- 分析跟蹤并確定目標應用程序所需的內核代碼,
- 組裝一個只包含應用程序所需代碼的內核裁剪。
配置驅動的是內核裁剪的一般方法,大多數現有的工具使用配置驅動技術,因為它們是為數不多的可以產生穩定內核的技術之一。配置驅動的內核重載根據功能特性減少了內核代碼,配置選項對應于內核的功能,裁剪后的內核只包含用于支持目標應用程序工作負載的功能。
然而,盡管內核裁剪技術在安全性和性能方面非常吸引人,但在實踐中并沒有得到廣泛采用。這并不是因為缺乏需求,實際上,許多云供應商手工編寫 Linux 內核來減少代碼,但一般不如內核裁剪技術有效。
2. 現有內核裁剪技術的限制
現有內核裁剪技術有五個主要的局限性。
在引導階段不可見?,F有技術只能在內核引導后啟動,依賴于 ftrace,因此無法觀察在引導階段加載了哪些內核代碼。如果內核中缺少關鍵模塊,內核通常無法啟動,而大量的內核功能特性只能通過觀察引導階段來捕獲。此外,關于性能和安全性同樣只在引導時加載(例如,用于多核支持的 CONFIGSCHEDMC 和 CONFIGSECURITYNETWORK) ,導致了性能和安全性降低。
缺乏對應用程序部署的快速支持。使用現有的工具,面向內核裁剪來部署一個新的應用程序需要完成跟蹤、分析和組裝這三個步驟。這個過程非常耗時,有可能需要幾個小時甚至幾天,阻礙了應用部署的敏捷性。
粒度較粗。使用ftrace 只能在函數級跟蹤內核代碼,粒度太粗,無法跟蹤影響函數內代碼的配置選項。
覆蓋不完全。因為使用動態跟蹤,所以需要應用程序工作負載來驅動內核的代碼執行,以最大限度地擴大覆蓋范圍。然而,基準測試覆蓋是具有挑戰性的,而且,如果應用程序有在跟蹤期間沒有觀察到的內核代碼,那么裁剪后的內核可能會在運行時崩潰。
沒有區分執行依賴,可能存在冗余。即使實際上可能并不需要執行的代碼,也可能包含在了內核功能特性中,例如,可能初始化了第二個文件系統。
前三個限制是可以克服的,可以通過改進設計和工具加以解決,而后兩個限制是在所難免,需要在具體的技術之外作出努力。
3. Linux 的內核配置
3.1配置選項
內核配置由一組配置選項組成。一個內核模塊可以有多個選項,每個選項都控制哪些代碼將包含在最終的內核二進制文件中。
配置選項控制內核代碼的不同粒度,例如由 C 預處理器實現的語句和函數,以及基于 Makefile 實現的對象文件。C 預處理器根據 #ifdef/#ifndef 選擇代碼塊,配置選項用作宏定義,以確定是否在編譯后的內核中包含這樣條件的代碼塊,可以是語句粒度或者函數粒度。Makefile 用于確定是否在編譯后的內核中包含某些對象文件,例如, CONFIG_CACHEFILES 就是 Makefile 中的配置選項。
語句級配置選項不能通過現有內核裁剪工具所使用的函數級跟蹤來識別。事實上,Linux 4.14 中30%左右 的 C 預處理器是語句級選項。
隨著內核代碼和功能特性的快速增長,內核中的配置選項數量也在迅速增加,以 Linux內核3.0以上版本都有1萬多個配置選項。
3.2. 配置語言
Linux內核使用KConfig 配置語言來指示編譯器在編譯后的內核中包含哪些代碼,允許定義配置選項以及它們之間的依賴關系。
KConfig 中配置選項的值可能是 bool、 tristate 或 constant。bool 意味著代碼要么被靜態編譯成內核二進制文件,要么被排除在外,而 tristate 允許代碼被編譯成一個可載入核心模組,即一個可以在運行時加載的獨立對象。constant可以為內核代碼變量提供字符串或數值。一個選項可以依賴于另一個選項,KConfig 使用了一個遞歸過程,通過遞歸選擇和取消依賴項。最終的內核配置具有有效的依賴關系,但可能與用戶輸入不同。
3.3. 配置模板
Linux 內核附帶了許多手工制作的配置模板。但是,由于配置模板的硬編碼特性并且需要人工干預,它們不能適應不同的硬件平臺,也不了解應用程序的需求。例如,由 tinyconfig 構建的內核不能在標準硬件上啟動,更不用說支持其他應用了。有些工具將 localmodconfig 視為最小化的配置,但是,localmodconfig 與靜態配置模板具有相同的局限性,它不會啟動控制語句級或函數級 C 預處理器的配置選項,也不會處理可加載的內核模塊。
kvmconfig 和 xenconfig 模板是為在 KVM 和 Xen 上運行的內核而定制的。它們提供例如底層虛擬化和硬件環境的領域知識。
3.4. 云中的 Linux 內核配置
Linux 是云服務中占主導地位的操作系統內核,云供應商都在一定程度上放棄了普通的 Linux 內核。云廠商的定制通常是通過直接刪除可加載的內核模塊來完成的,手工修剪內核模塊二進制文件的問題是可能會違反依賴關系。重要的是,基于應用程序需求可以進一步裁剪內核。例如,Amazon FireCracker 內核是一個專門用于函數即服務的微型虛擬機,使用 HTTPD 作為目標應用程序,在保證功能和性能提升的同時,使內核裁剪實現了更大程度的最小化。
4. 內核裁剪的思考
針對局限一,是否可以使用來自 QEMU 的指令級跟蹤來實現引導階段的可見性呢?這樣,就可以跟蹤內核代碼并將其映射到內核配置選項。既然引導階段對于生成可引導內核至關重要,使用 hypervisor 提供的跟蹤特性來獲得端到端的可觀察性并生成穩定的內核。
針對局限二,根據在NLP深度學習中的經驗,可以使用離線和在線結合的方法,給定一組目標應用程序,可以直接離線生成的App 配置,再和基線配置組合成完整的內核配置,從而生成一個裁剪后的內核。這種可組合性能夠通過重用應用配置和以前構建的文件(例如內核模塊)來增量地構建新內核。如果目標應用程序的配置已知,就可以在幾十秒內完成內核裁剪。
針對局限三,使用指令級跟蹤可以解決控制函數內部功能特性的內核配置選項,指令級跟蹤的開銷對于運行測試套件和性能基準來說是可以接受的。
針對局限四,使用基于動態跟蹤的一個基本限制是測試套件和基準的不完善,許多開源應用程序測試套件的代碼覆蓋率較低。組合不同的工作負載來驅動應用程序可以在一定程度上減輕這種限制。
針對局限五,通過刪除在基線內核中執行但在實際部署運行時不需要的內核模塊,可以使用特定于領域的信息進一步加載內核。以 Xen 和 KVM 為例,可以基于 xenconfig 和 kvmconfig 配置模板進一步減少內核大小。面向應用程序的內核裁剪可以進一步減少內核大小甚至廣泛地定制的內核代碼。
5 內核裁剪框架初探
內核裁剪框架的原理沒有變,仍然是跟蹤目標應用工作負載的內核占用情況,以確定所需的內核選項。
5.1 內核裁剪框架的核心特性
內核裁剪框架大概可以具備以下特性:
- 端到端的可見性。利用虛擬機監控程序的可見性來實現端到端的觀察,可以跟蹤內核引導階段和應用程序工作負載,可以嘗試在QEMU 的基礎上建造Linux內核的裁剪框架。
- 可組合性。一個核心思想是通過將內核配置劃分為若干組配置集,使內核配置可以組合,用于在給定的部署環境上引導內核,也可以用于目標應用程序所需的配置選項。配置集分為兩種:基線配置和應用配置?;€配置不一定是在特定硬件上引導所需的最小配置集,而是在引導階段跟蹤的一組配置選項。基線配置可以與一個或多個應用配置組合在一起,以生成最終的內核配置。
- 可重用性?;€配置和應用配置都可以存儲在數據庫中,并且只要部署環境和應用程序的二進制文件不變就可以重用。這種可重用性避免了重復跟蹤工作負載的運行,使得配置集的創建成為一次性的工作。
- 支持快速應用部署。給定一個部署環境和目標應用程序,內核裁剪框架可以有效地檢索基線配置和 應用配置,并將它們組合成所需的內核配置,然后使用生成的配置構建廢棄的內核。
- 細粒度配置跟蹤,基于程序計數器的跟蹤來識別基于低級代碼模式的配置選項。
5.2 內核裁剪框架的體系結構
內核裁剪框架應該同時具備離/在線系統,體系結構如下圖所示:
通過離線系統, 配置跟蹤器用于跟蹤部署環境和應用程序所需的配置選項,并記錄下來。配置生成器將這些選項處理成基線配置和應用配置選項,并將它們存儲在配置數據庫中。
通過在線系統,配置組合器使用基線配置和應用配置來生成目標內核配置,然后,內核構建器生成裁剪后的Linux內核.
5.3 內核裁剪框架的實現可行性
配置跟蹤
內核裁剪框架的配置跟蹤器在目標應用程序驅動的內核執行期間跟蹤配置選項,使用 PC 寄存器捕獲正在執行的指令的地址。為了確保被跟蹤的 PC 屬于目標應用程序,而不是其他進程(例如,后臺服務) ,可以使用了一個定制的 init 腳本,該腳本不啟動任何其他應用程序,只掛載文件系統/tmp、/proc 和/sys ,啟用網絡接口(lo 和 eth0) ,最后在內核引導后直接啟動應用程序。
同時,可能需要禁用內核位址空間配置隨機載入 ,以便能夠正確地將地址映射到源代碼,但在裁剪后的內核中仍然可以使用。然后,將 PC 映射到源代碼語句。可加載的內核模塊需要額外的處理,可以使用/proc/module 獲取每個加載的內核模塊的起始地址,將這些 PC 映射到內核模塊二進制中的語句。另一種方法是利用 localmodconfig,但是,localmodconfig 只提供模塊粒度級別的信息。
最后,將語句歸屬于配置。對于基于 C 預處理器的模式 ,分析 C 源文件以提取預處理器指令,然后檢查這些指令中的語句是否被執行。對于基于 Makefile 的模式 ,確定是否應該在對象文件的粒度上選擇配置選項。例如,如果使用了任何相應的文件(bind.o、 achefiles.o 或 daemon.o) ,則需要選擇 CONFIG_CACHEFILES。
配置生成
基線配置和應用配置是在離線系統中生成的。如何判斷啟動階段結束呢?可以使用 mmap 將一個空的存根函數映射到一個預定義地址段,上述的初始化腳本在運行目標應用程序之前調用調用存根函數,因此,可能根據 PC 跟蹤中的預定義地址來識別引導階段的結束。
內核裁剪框架從應用程序中獲取配置選項,并過濾掉在引導階段觀察到的與硬件相關的選項。這些硬件特性是根據它們在內核源代碼中的位置定義的。不排除這樣的可能性,即與硬件相關的選項只能在應用程序執行期間觀察到,例如,它根據需要加載新的設備驅動程序。
配置組裝
將基線配置與一個或多個應用配置組合在一起,可以以生成用于構建內核的最終配置。首先,將所有 配置選項并入一個初始配置,然后使用SAT求解器解決它們之間的依賴關系。嘗試將配置依賴性建模為一個布爾可滿足性問題,有效配置是指滿足配置選項之間所有指定依賴性的配置。因為 KConfig 并不確保包含所有選定的選項,而是取消選擇未滿足的依賴項,所以才要基于 SAT 求解器對內核配置進行建模。
內核構建
使用于Linux的KBuild基于組裝后的配置選項構建裁剪內核,利用現代make的增量構建可以優化構建時間,也可以緩存以前的構建結果(例如,目標文件和內核模塊) ,以避免冗余的編譯和鏈接。當發生配置更改時,只有對配置選項進行更改的模塊重新構建,而其他文件可以重用。
6. 小結
由于操作系統內核的不穩定性、時效性較差、完整性問題以及需要人工干預等原因,Linux內核裁剪技術沒有得到廣泛的應用。了解了現有技術的局限性,嘗試提出一個Linux內核裁剪框架,或許可以解決這些問題。