嵌入式Linux系統主要特點在於使用Bootloader替代了案頭系統的BIOS,同時對系統進行了規模上的裁剪,但硬體上的劣勢往往導致系統啟動速度較慢,而嵌入式產品使用者又對系統的開機速度比較敏感,樣就產生了對於提高嵌入式Linux系統啟動速度的需求。本文對系統啟動時執行哪些階段的操作,以及縮短這些操作時間的方法進行了探討。
1 嵌入式Linux系統啟動時序
目前,嵌入式系統的硬體平台和應用方向區別很大,但總體啟動流程一致的。這裡的系統啟動是指從使用者執行上電/複位操作,到系統開始提供使用者可接收的服務水平所需要的過程。典型的上電/複位時序如表1所列。
表1 嵌入式Linux系統啟動時序
2 Linux快速啟動方法
目前,一些Linux的發行版本已經對啟動速度進行了最佳化。如果利用標準Linux進行開發,則啟動速度的提高主要是通過核心配置和各種補丁包來實現的。下面分析快速啟動的一些關鍵技術。
2.1 Firmware和Bootloader階段
目標板一旦確定,Firmware啟動並執行時間就無法改變了,Flash和RAM的讀寫速度也就隨之確定了。但如果複位時能夠繞過Firmware和Bootloader,即允許運行中的核心載入以及運行另一個核心,可以縮短啟動的時間。典型的實現有Kexec,它有2個組件,即使用者空間組件kexectools和核心補丁。另外一種辦法是在核心命令列中加入reboot=soft數,同樣可以跳過Firmware,但是缺點在於無法從使用者空間調用。
對於正常啟動,可以選擇速度比較快的Bootloader,並對核心進行小型化處理;還可以使用高速的映像複製技術(如DMA2RAM),從而縮短複製的時間。為了縮短解壓消耗的時間,可尋求比較高效的壓縮演算法。但一般情況下,壓縮比越高,演算法越複雜,解壓速度就越慢,從而造成複製時間(與壓縮比成反比)和解壓時間(一般與壓縮比成正比)之間的矛盾。
2.2 核心階段
核心初始化時要對RealTime Clock (RTC)進行同步。此過程要佔用1s的時間,可去掉以節約時間,但這樣CPU會與正確的時間有1s的偏差,如果關機時CPU時鐘又要儲存在RTC中,偏差就會不斷累積。但對於使用外部時鐘源進行同步的系統,則可安全地跳過這個階段。
Preset LPJ可以用來縮短每次啟動時調用calibrate_delay()來校準loops_per_jiffy消耗的時間。這個時間開銷與CPU頻率無關,在典型的嵌入式硬體環境下會消耗300ms左右。LPJ值對於固定硬體平台應該是一致的,可以只計算一次,在後續的啟動中就可以在啟動參數中強制指定LPJ值,而跳過實際的計算過程。具體方法是:在正常啟動後記錄下核心啟動資訊中的"Calibrating Delay"數值,在啟動參數中以"lpj=xxxxxx"的形式強制指定。
啟動過程預設開啟控制台輸出啟動訊息,但是控制台尤其是基於幀緩衝的控制台會減慢啟動速度。因此在嵌入式Linux產品中,將啟動過程中的控制台設為靜默狀態,方法是在核心啟動參數中加入"quiet"。
裝置搜尋和驅動安裝是比較耗時的操作,因此要在編譯核心時確定需要安裝哪些驅動模組,以免系統搜尋那些根本不存在的裝置,尤其是多餘的IDE裝置。對於啟動時暫時不用安裝的裝置,盡量將驅動編譯成模組,在以後空閑時或者使用裝置時載入,而不是全部放在啟動階段。
2.3 使用者空間階段
傳統Linux的初始化指令碼是由bash執行的,在核心引導後啟動init進程(/sbin/init)。它使用一個ASCII檔案(/etc/inittab)來改變運行層級,這個檔案中又會調用RCSript,由RCSript尋找/etc/rc.d/rc5.d/並啟動相應連結指向的系統服務。
消費電子類Linux系統需要啟用圖形介面等必要的服務,未經最佳化的系統在這個過程中會預設啟動很多根本用不到或者當前用不到的系統服務,這一部分會花去較大的時間開銷。最簡單的最佳化辦法就是根據實際需要,通過改寫服務組態檔定製系統服務。另外,init指令碼的執行是串列的,在指令碼量大時會導致引導過程非常,因此可以考慮並行運行各種服務以加快啟動的速度。現在已經出現了一些初始化程式來替代init進程,下面介紹initng和upstart。
initng(init nextgerneration)能夠並行啟動服務從而快速完成初始化工作。initng認為滿足了依賴關係的服務就可以啟動。在從外存載入一個指令碼或等待硬體裝置啟動的同時,可以運行另一個指令碼來啟動別的服務,使系統在CPU 和 I/O 之間實現較好的平衡。作為一個基於依賴關係的解決方案,initng使用自己的初始化指令碼集,它們對服務和守護進程的依賴性進行了編碼。如果某個服務依賴(使用 need關鍵字定義)於其他服務,則要保證啟動時它所依賴的所有服務均可用。無依賴關係的服務立即並行啟動,具有依賴關係的服務則要等待以安全啟動。
upstart與 initng的區別在於: upstart基於事件,任務/服務的啟動/停止都取決於它所等待的事件是否發生。upstart對事件的定義非常靈活,分為3類:edge (simple) events, level (value) events和temporal events。使用start/stop、事件名以及它所期待的值(可選)組成條目對觸發事件進行描述。事件依賴有兩種辦法:一種是任務自身導致事件發生,不管任務何時啟動/結束都會有事件發生,對於啟動時要執行的基本任務,這種辦法比較有效;而對於較複雜的依賴關係,則可使用任務的Shell指令碼工具。
2.4 預讀取和預連結 預讀取(Readahead)可以將檔案(程式和庫檔案)在使用之前積極式載入到RAM緩衝中,這樣就不用在使用時為讀取這個檔案而訪問I/O。如果知道下一步操作要訪問哪些檔案,就可以提前將它們全部
/部分讀取到緩衝區,從而加快執行速度。嵌入式系統很多場合下對於下一步操作都是可預測的,比如系統啟動時總是以同樣的順序訪問同樣的可執行/資料檔案,檔案塊的訪問往往是順序的,應用程式啟動時總是訪問同樣的程式檔案段、共用庫、資源或者輸入檔案。這樣使用預讀取有很強的針對性,從而提高程式執行速度。 ELF(Excutable and Linkable File)是目前Linux中的標準二進位格式,其啟動需要以下步驟:將共用庫映射到虛擬位址空間;解析符號引用;初始化每個ELF檔案。由於共用庫是位置無關的,要在運行時完成部分重定位處理和符號尋找的工作,才能跳到程式的進入點,因此在帶來靈活性的同時,也造成ELF檔案的啟動速度緩慢,尤其是解析符號引用要消耗大量的時間,對於使用多個共用庫的大型程式更是如此。但在很多嵌入式系統中,可執行檔和共用庫極少變化,而且每次程式運行時連結工作完全相同。 預連結(Prelink)利用這一點,修改ELF共用庫和二進位檔案,將連結資訊加入到可執行檔中以簡化動態連結重定位,從而使程式啟動加快。預連結首先搜集要預連結的ELF二進位檔案及其所依賴的共用庫,為每個庫分配唯一的虛擬空間位置,並將共用庫重新連結到這個基準位置(動態連結器要載入這個庫時,只要虛擬空間地址未被佔用,它就會將庫映射到指定位置);然後預連結解析二進位或者庫中的所有重定位,並將重定位資訊存放到ELF對象,還要將所有依賴庫的列表及校正和添加到二進位檔案或庫中。對於二進位檔案,還需列出所有的衝突(在共用庫的自然搜尋範圍內對符號的解析不相同)。在運行時,動態連結器先檢查是否所有依賴的庫都已經映射到指定的位置,而且庫檔案沒有變化,只考慮衝突而不用處理每個庫的重定位,這樣大大提高了程式啟動的速度。使用時要注意的是,若共用庫發生了改變,則使用它的所有程式都要重新連結,否則程式仍要進行耗時的正常重定位。 3 XIP和檔案系統最佳化 3.1 代碼執行方式 嵌入式系統中代碼的執行方式主要有3種: ① 完全映射(fully shadowed)。嵌入式系統程式運行時,將所有的代碼從非易失儲存空間(Flash、ROM等)複製到RAM中運行。 ② 按需分頁(demand paging)。只複製部分代碼到RAM中。這種方法對RAM中的頁進行匯入/匯出管理,如果訪問位於虛存中但不在物理RAM中會產生頁錯誤,這時才將代碼和資料對應到RAM中。 ③ eXecute In Place (XIP)。在系統啟動時,不將代碼複製到RAM,而是直接在非易失性儲存位置執行。RAM中只存放需要不斷變化的資料部分,1所示。如果非易失性儲存空間的讀取速度與RAM相近,則XIP可以節省複製和解壓的時間。NOR Flash和ROM的讀取速度比較快(約100 ns),適合XIP;而NAND Flash的讀操作是基於扇區的,速度相對很慢(μs級),因此不宜實現XIP。 圖1 完全映射和XIP的比較 |
XIP可以分為以下2種: ① 核心XIP。直接在Flash/ROM中運行核心,可以節省複製和映像解壓的時間。Linux 2.6.10核心已經包含了XIP支援。 ② 應用程式XIP。直接從應用程式代碼的儲存位置執行,而不用將它載入到RAM中,這樣應用程式的第一次執行速度會比較快。要使用應用程式XIP,應該基於支援它的檔案系統。 3.2 XIP檔案系統 目前XIP檔案系統的實現主要有2種: Linear XIP CRAMFS和Advanced XIP File System(AXFS)。 CRAMFS是一個壓縮的唯讀檔案系統,本來用於案頭Linux系統的啟動,但CRAMFS經過修改後可以支援嵌入式系統並支援XIP。Linear XIP CRAMFS用一個sticky bit對它管理的檔案進行區分,標記為壓縮(按需分頁)或者未壓縮(XIP)。如果檔案標記為XIP,則所有頁都不壓縮,而且要在Flash中連續儲存。在載入XIP檔案時,直接對所有頁地址進行映射;而按需分頁的檔案則在發生頁錯誤時,將相應頁解壓到RAM中。 要建立Linear XIP CRAMFS檔案系統映像,必須確定可執行檔和庫檔案的使用頻率,頻繁使用的檔案適合於XIP,而其他檔案應該進行壓縮。現在有一些工具(如RAMUST和CFSST)可以協助判斷哪些檔案需要XIP,而哪些不需要。下面就可以給XIP檔案加上標記並製作根檔案系統,以使用mkfs.cramfs工具為例: chmod +t filenames mkfs.cramfs-x rootfs rootfs.bin 另外,還要修改核心配置參數以支援XIP:在啟動選項中向預設核心命令字串中加入 rootfstype=cramfs,選擇核心XIP並設定XIP核心物理地址;在驅動程式中加入MTD對XIP的支援;在檔案系統中加入對Linear XIP CRAMFS的支援。接下來就可以產生XIP映像了。 Linear XIP CRAMFS的一個缺陷在於它是基於檔案的,即一個檔案中的所有頁要麼全部採用XIP,要麼全部採用壓縮/按需分頁,但事實上同一檔案中不同頁的使用頻率區別也很大。AXFS是Intel公司開發的一個新的唯讀檔案系統,它從Linear XIP CRAMFS中繼承了許多方法,同時也進行了一些改進。AXFS的XIP粒度是基於頁的,並且內建工具來判斷哪些頁需要XIP,哪些頁需要壓縮,從而更好地在速度和RAM/Flash的使用上取得平衡。
3.3 非XIP檔案系統 XIP一般基於NOR Flash,成本相對較高。對於使用者資料量大的應用,往往還要使用基於NAND Flash的,非XIP的檔案系統常用的有JFFS2/YAFFS。 JFFS2是一種基於壓縮的檔案系統。在多媒體應用中,如果圖片、音視頻已經經過壓縮,則使用JFFS2無疑會給CPU帶來雙重的壓縮/解壓負擔,訪問速度也會受到影響。因此,在這類應用比較密集的應用中,採用不壓縮的檔案系統(如YAFFS/YAFFS2)可以加快系統速度。 YAFFS/YAFFS2是專為嵌入式系統使用NAND Flash設計的記錄檔系統。與JFFS2相比,減少了一些功能(例如不支援資料壓縮),所以速度更快,掛載時間很短,對記憶體的佔用較小。YAFFS/YAFFS2內建NAND晶片的驅動,使用者可以不使用MTD和VFS,直接對檔案系統操作。YAFFS與YAFFS2的主要區別在於:前者僅支援小頁(512位元組) NAND Flash;後者則可支援大頁(2 KB) NAND Flash,同時在記憶體使用量、記憶體回收、訪問速度等方面有所改進。 結語 快速啟動對於嵌入式Linux系統是比較迫切的要求之一。本文通過分析嵌入式系統的引導過程和關鍵時延因素,提出了相應的解決辦法,並對XIP檔案系統進行了介紹。由於啟動速度非常依賴於硬體平台,而且有的方法互斥,因此在具體應用時需要綜合考慮和選擇。 參考文獻 [1] Tim Bird R. Methods to Improve Bootup Time in Linux [R]. Proceedings of the Linux Symposium, Ottawa,2004. [2] Karim Yaghmour. 構建嵌入式Linux系統[M]. 北京:中電力出版社, 2004: 49-66. [3] 陳莉君. 深入分析Linux核心原始碼[M]. 北京:民郵電出版社, 2001: 477-499. [4] 左大全,吳剛. 嵌入式Linux快速啟動與XIP應用[J]. 電腦工程與科學,2006(12). |
|