當前位置:
首頁 > 知識 > Linux 啟動過程分析

Linux 啟動過程分析

(點擊

上方公眾號

,可快速關注)




編譯: linux中國 /  jessie-pang  英文: Alison Chaiken


https://linux.cn/article-9437-1.html





理解運轉良好的系統對於處理不可避免的故障是最好的準備。


關於開源軟體最古老的笑話是:「代碼是自具文檔化的self-documenting」。經驗表明,閱讀源代碼就像聽天氣預報一樣:明智的人依然出門會看看室外的天氣。本文講述了如何運用調試工具來觀察和分析 Linux 系統的啟動。分析一個功能正常的系統啟動過程,有助於用戶和開發人員應對不可避免的故障。


從某些方面看,啟動過程非常簡單。內核在單核上以單線程和同步狀態啟動,似乎可以理解。但內核本身是如何啟動的呢?initrd(initial ramdisk) 和引導程序bootloader具有哪些功能?還有,為什麼乙太網埠上的 LED 燈是常亮的呢?


請繼續閱讀尋找答案。在 GitHub 上也提供了 介紹演示和練習的代碼。


啟動的開始:OFF 狀態

區域網喚醒Wake-on-LAN


OFF 狀態表示系統沒有上電,沒錯吧?表面簡單,其實不然。例如,如果系統啟用了區域網喚醒機制(WOL),乙太網指示燈將亮起。通過以下命令來檢查是否是這種情況:



# sudo ethtool <interface name>




其中 <interface name> 是網路介面的名字,比如 eth0。(ethtool 可以在同名的 Linux 軟體包中找到。)如果輸出中的 Wake-on 顯示 g,則遠程主機可以通過發送 魔法數據包MagicPacket 來啟動系統。如果您無意遠程喚醒系統,也不希望其他人這樣做,請在系統 BIOS 菜單中將 WOL 關閉,或者用以下方式:





# sudo ethtool -s <interface name> wol d





響應魔法數據包的處理器可能是網路介面的一部分,也可能是 底板管理控制器Baseboard Management Controller(BMC)。


英特爾管理引擎、平台控制器單元和 Minix


BMC 不是唯一的在系統關閉時仍在監聽的微控制器(MCU)。x86_64 系統還包含了用於遠程管理系統的英特爾管理引擎(IME)軟體套件。從伺服器到筆記本電腦,各種各樣的設備都包含了這項技術,它開啟了如 KVM 遠程控制和英特爾功能許可服務等 功能。根據 Intel 自己的檢測工具,IME 存在尚未修補的漏洞。壞消息是,要禁用 IME 很難。Trammell Hudson 發起了一個 me_cleaner 項目,它可以清除一些相對惡劣的 IME 組件,比如嵌入式 Web 伺服器,但也可能會影響運行它的系統。


IME 固件和系統管理模式System Management Mode(SMM)軟體是 基於 Minix 操作系統 的,並運行在單獨的平台控制器單元Platform Controller Hub上(LCTT 譯註:即南橋晶元),而不是主 CPU 上。然後,SMM 啟動位於主處理器上的通用可擴展固件介面Universal Extensible Firmware Interface(UEFI)軟體,相關內容 已被提及多次。Google 的 Coreboot 小組已經啟動了一個雄心勃勃的 非擴展性縮減版固件Non-Extensible Reduced Firmware(NERF)項目,其目的不僅是要取代 UEFI,還要取代早期的 Linux 用戶空間組件,如 systemd。在我們等待這些新成果的同時,Linux 用戶現在就可以從 Purism、System76 或 Dell 等處購買 禁用了 IME 的筆記本電腦,另外 帶有 ARM 64 位處理器筆記本電腦 還是值得期待的。


引導程序


除了啟動那些問題不斷的間諜軟體外,早期引導固件還有什麼功能呢?引導程序的作用是為新上電的處理器提供通用操作系統(如 Linux)所需的資源。在開機時,不但沒有虛擬內存,在控制器啟動之前連 DRAM 也沒有。然後,引導程序打開電源,並掃描匯流排和介面,以定位內核鏡像和根文件系統的位置。U-Boot 和 GRUB 等常見的引導程序支持 USB、PCI 和 NFS 等介面,以及更多的嵌入式專用設備,如 NOR 快閃記憶體和 NAND 快閃記憶體。引導程序還與 可信平台模塊Trusted Platform Module(TPM)等硬體安全設備進行交互,在啟動最開始建立信任鏈。



在構建主機上的沙盒中運行 U-boot 引導程序。


包括樹莓派、任天堂設備、汽車主板和 Chromebook 在內的系統都支持廣泛使用的開源引導程序 U-Boot。它沒有系統日誌,當發生問題時,甚至沒有任何控制台輸出。為了便於調試,U-Boot 團隊提供了一個沙盒,可以在構建主機甚至是夜間的持續集成(CI)系統上測試補丁程序。如果系統上安裝了 Git 和 GNU Compiler Collection(GCC)等通用的開發工具,使用 U-Boot 沙盒會相對簡單:



# git clone git://git.denx.de/u-boot; cd u-boot


# make ARCH=sandbox defconfig

# make; ./u-boot


=>

printenv


=>

help





在 x86_64 上運行 U-Boot,可以測試一些棘手的功能,如 模擬存儲設備 的重新分區、基於 TPM 的密鑰操作以及 USB 設備熱插拔等。U-Boot 沙盒甚至可以在 GDB 調試器下單步執行。使用沙盒進行開發的速度比將引導程序刷新到電路板上的測試快 10 倍,並且可以使用 

Ctrl + C

 恢復一個「變磚」的沙盒。


啟動內核

配置引導內核


引導程序完成任務後將跳轉到已載入到主內存中的內核代碼,並開始執行,傳遞用戶指定的任何命令行選項。內核是什麼樣的程序呢?用命令 

file /boot/vmlinuz

 可以看到它是一個 「bzImage」,意思是一個大的壓縮的鏡像。Linux 源代碼樹包含了一個可以解壓縮這個文件的工具—— extract-vmlinux:




# scripts/extract-vmlinux /boot/vmlinuz-$(uname -r) > vmlinux


# file vmlinux


vmlinux

:

ELF

64

-

bit LSB

executable

,

x86

-

64

,

version

1

(

SYSV

),

statically


linked

,

stripped



內核是一個 

可執行與可鏈接格式 Executable and Linking Format

(ELF)的二進位文件,就像 Linux 的用戶空間程序一樣。這意味著我們可以使用 

binutils

 包中的命令,如 

readelf

 來檢查它。比較一下輸出,例如:






# readelf -S /bin/date


# readelf -S vmlinux




這兩個二進位文件中的段內容大致相同。


所以內核必須像其他的 Linux ELF 文件一樣啟動,但用戶空間程序是如何啟動的呢?在 main() 函數中?並不確切。


在 main() 函數運行之前,程序需要一個執行上下文,包括堆棧內存以及 stdiostdout 和 stderr 的文件描述符。用戶空間程序從標準庫(多數 Linux 系統在用 「glibc」)中獲取這些資源。參照以下輸出:




# file /bin/date


/

bin

/

date

:

ELF

64

-

bit LSB shared

object

,

x86

-

64

,

version

1

(

SYSV

),

dynamically


linked

,

interpreter

/

lib64

/

ld

-

linux

-

x86

-

64.so.2

,

for

GNU

/

Linux

2.6.32

,


BuildID

[

sha1

]

=

14e8563676febeb06d701dbee35d225c5a8e565a

,


stripped




ELF 二進位文件有一個解釋器,就像 Bash 和 Python 腳本一樣,但是解釋器不需要像腳本那樣用 #! 指定,因為 ELF 是 Linux 的原生格式。ELF 解釋器通過調用 _start() 函數來用所需資源 配置一個二進位文件,這個函數可以從 glibc 源代碼包中找到,可以 用 GDB 查看。內核顯然沒有解釋器,必須自我配置,這是怎麼做到的呢?


用 GDB 檢查內核的啟動給出了答案。首先安裝內核的調試軟體包,內核中包含一個未剝離的unstripped vmlinux,例如 apt-get install linux-image-amd64-dbg,或者從源代碼編譯和安裝你自己的內核,可以參照 Debian Kernel Handbook 中的指令。gdb vmlinux 後加 info files 可顯示 ELF 段 init.text。在 init.text 中用 l *(address) 列出程序執行的開頭,其中 address 是 init.text 的十六進位開頭。用 GDB 可以看到 x86_64 內核從內核文件 arch/x86/kernel/head_64.S 開始啟動,在這個文件中我們找到了彙編函數 start_cpu0(),以及一段明確的代碼顯示在調用 x86_64 start_kernel() 函數之前創建了堆棧並解壓了 zImage。ARM 32 位內核也有類似的文件 arch/arm/kernel/head.S。start_kernel() 不針對特定的體系結構,所以這個函數駐留在內核的 init/main.c 中。start_kernel() 可以說是 Linux 真正的 main() 函數。


從 start_kernel() 到 PID 1


內核的硬體清單:設備樹和 ACPI 表


在引導時,內核需要硬體信息,不僅僅是已編譯過的處理器類型。代碼中的指令通過單獨存儲的配置數據進行擴充。有兩種主要的數據存儲方法:設備樹device-tree 和 高級配置和電源介面(ACPI)表。內核通過讀取這些文件了解每次啟動時需要運行的硬體。


對於嵌入式設備,設備樹是已安裝硬體的清單。設備樹只是一個與內核源代碼同時編譯的文件,通常與 

vmlinux

 一樣位於 

/boot

 目錄中。要查看 ARM 設備上的設備樹的內容,只需對名稱與 

/boot/*.dtb

 匹配的文件執行 

binutils

 包中的 

strings

 命令即可,這裡 

dtb

 是指設備樹二進位文件device-tree binary。顯然,只需編輯構成它的類 JSON 的文件並重新運行隨內核源代碼提供的特殊 

dtc

 編譯器即可修改設備樹。雖然設備樹是一個靜態文件,其文件路徑通常由命令行引導程序傳遞給內核,但近年來增加了一個 設備樹覆蓋 的功能,內核在啟動後可以動態載入熱插拔的附加設備。


x86 系列和許多企業級的 ARM64 設備使用 ACPI 機制。與設備樹不同的是,ACPI 信息存儲在內核在啟動時通過訪問板載 ROM 而創建的 

/sys/firmware/acpi/tables

 虛擬文件系統中。讀取 ACPI 表的簡單方法是使用 

acpica-tools

 包中的 

acpidump

 命令。例如:



聯想筆記本電腦的 ACPI 表都是為 Windows 2001 設置的。


是的,你的 Linux 系統已經準備好用於 Windows 2001 了,你要考慮安裝嗎?與設備樹不同,ACPI 具有方法和數據,而設備樹更多地是一種硬體描述語言。ACPI 方法在啟動後仍處於活動狀態。例如,運行 

acpi_listen

 命令(在 

apcid

 包中),然後打開和關閉筆記本機蓋會發現 ACPI 功能一直在運行。暫時地和動態地 覆蓋 ACPI 表 是可能的,而永久地改變它需要在引導時與 BIOS 菜單交互或刷新 ROM。如果你遇到那麼多麻煩,也許你應該 安裝 coreboot,這是開源固件的替代品。


從 start_kernel() 到用戶空間


init/main.c 中的代碼竟然是可讀的,而且有趣的是,它仍然在使用 1991 – 1992 年的 Linus Torvalds 的原始版權。在一個剛啟動的系統上運行 

dmesg | head

,其輸出主要來源於此文件。第一個 CPU 註冊到系統中,全局數據結構被初始化,並且調度程序、中斷處理程序(IRQ)、定時器和控制台按照嚴格的順序逐一啟動。在 

timekeeping_init()

 函數運行之前,所有的時間戳都是零。內核初始化的這部分是同步的,也就是說執行只發生在一個線程中,在最後一個完成並返回之前,沒有任何函數會被執行。因此,即使在兩個系統之間,

dmesg

的輸出也是完全可重複的,只要它們具有相同的設備樹或 ACPI 表。Linux 的行為就像在 MCU 上運行的 RTOS(實時操作系統)一樣,如 QNX 或 VxWorks。這種情況持續存在於函數 

rest_init()

 中,該函數在終止時由 

start_kernel()

 調用。



早期的內核啟動流程。


函數 

rest_init()

 產生了一個新進程以運行 

kernel_init()

,並調用了 

do_initcalls()

。用戶可以通過將 

initcall_debug

 附加到內核命令行來監控 

initcalls

,這樣每運行一次 

initcall

 函數就會產生 一個 

dmesg

 條目。

initcalls

 會歷經七個連續的級別:early、core、postcore、arch、subsys、fs、device 和 late。

initcalls

 最為用戶可見的部分是所有處理器外圍設備的探測和設置:匯流排、網路、存儲和顯示器等等,同時載入其內核模塊。

rest_init()

 也會在引導處理器上產生第二個線程,它首先運行 

cpu_idle()

,然後等待調度器分配工作。


kernel_init()

 也可以 設置對稱多處理(SMP)結構。在較新的內核中,如果 

dmesg

 的輸出中出現 「Bringing up secondary CPUs…」 等字樣,系統便使用了 SMP。SMP 通過「熱插拔」 CPU 來進行,這意味著它用狀態機來管理其生命周期,這種狀態機在概念上類似於熱插拔的 U 盤一樣。內核的電源管理系統經常會使某個核core離線,然後根據需要將其喚醒,以便在不忙的機器上反覆調用同一段的 CPU 熱插拔代碼。觀察電源管理系統調用 CPU 熱插拔代碼的 BCC 工具 稱為 

offcputime.py


請注意,

init/main.c

 中的代碼在 

smp_init()

 運行時幾乎已執行完畢:引導處理器已經完成了大部分一次性初始化操作,其它核無需重複。儘管如此,跨 CPU 的線程仍然要在每個核上生成,以管理每個核的中斷(IRQ)、工作隊列、定時器和電源事件。例如,通過 

ps -o psr

 命令可以查看服務每個 CPU 上的線程的 softirqs 和 workqueues。




# ps -o pid,psr,comm $(pgrep ksoftirqd)  


PID PSR

COMMAND


  

7

  

0

ksoftirqd

/

0


  

16

  

1

ksoftirqd

/

1


  

22

  

2

ksoftirqd

/

2


  

28

  

3

ksoftirqd

/

3


 


# ps -o pid,psr,comm $(pgrep kworker)


PID  PSR

COMMAND


  

4

  

0

kworker

/

0

:

0H


  

18

  

1

kworker

/

1

:

0H


  

24

  

2

kworker

/

2

:

0H


  

30

  

3

kworker

/

3

:

0H


[

.

.

.

]




其中,PSR 欄位代表「處理器processor」。每個核還必須擁有自己的定時器和 

cpuhp

 熱插拔處理程序。


那麼用戶空間是如何啟動的呢?在最後,

kernel_init()

 尋找可以代表它執行 

init

 進程的 

initrd

。如果沒有找到,內核直接執行 

init

 本身。那麼為什麼需要 

initrd

 呢?


早期的用戶空間:誰規定要用 initrd?


除了設備樹之外,在啟動時可以提供給內核的另一個文件路徑是 

initrd

 的路徑。

initrd

 通常位於 

/boot

 目錄中,與 x86 系統中的 bzImage 文件 vmlinuz 一樣,或是與 ARM 系統中的 uImage 和設備樹相同。用 

initramfs-tools-core

 軟體包中的 

lsinitramfs

 工具可以列出 

initrd

 的內容。發行版的 

initrd

 方案包含了最小化的 

/bin

/sbin

 和 

/etc

 目錄以及內核模塊,還有 

/scripts

 中的一些文件。所有這些看起來都很熟悉,因為 

initrd

 大致上是一個簡單的最小化 Linux 根文件系統。看似相似,其實不然,因為位於虛擬內存檔中的 

/bin

 和 

/sbin

 目錄下的所有可執行文件幾乎都是指向 BusyBox 二進位文件 的符號鏈接,由此導致 

/bin

 和 

/sbin

 目錄比 glibc 的小 10 倍。


如果要做的只是載入一些模塊,然後在普通的根文件系統上啟動 

init

,為什麼還要創建一個 

initrd

 呢?想想一個加密的根文件系統,解密可能依賴於載入一個位於根文件系統 

/lib/modules

 的內核模塊,當然還有 

initrd

 中的。加密模塊可能被靜態地編譯到內核中,而不是從文件載入,但有多種原因不希望這樣做。例如,用模塊靜態編譯內核可能會使其太大而不能適應存儲空間,或者靜態編譯可能會違反軟體許可條款。不出所料,存儲、網路和人類輸入設備(HID)驅動程序也可能存在於 

initrd

 中。

initrd

 基本上包含了任何掛載根文件系統所必需的非內核代碼。

initrd

 也是用戶存放 自定義ACPI 表代碼的地方。



救援模式的 shell 和自定義的 initrd 還是很有意思的。


initrd

 對測試文件系統和數據存儲設備也很有用。將這些測試工具存放在 

initrd

 中,並從內存中運行測試,而不是從被測對象中運行。


最後,當 

init

 開始運行時,系統就啟動啦!由於第二個處理器現在在運行,機器已經成為我們所熟知和喜愛的非同步、可搶佔、不可預測和高性能的生物。的確,

ps -o pid,psr,comm -p 1

 很容易顯示用戶空間的 

init

 進程已不在引導處理器上運行了。


總結


Linux 引導過程聽起來或許令人生畏,即使是簡單嵌入式設備上的軟體數量也是如此。但換個角度來看,啟動過程相當簡單,因為啟動中沒有搶佔、RCU 和競爭條件等撲朔迷離的複雜功能。只關注內核和 PID 1 會忽略了引導程序和輔助處理器為運行內核執行的大量準備工作。雖然內核在 Linux 程序中是獨一無二的,但通過一些檢查 ELF 文件的工具也可以了解其結構。學習一個正常的啟動過程,可以幫助運維人員處理啟動的故障。


要了解更多信息,請參閱 Alison Chaiken 的演講——Linux: The first second,已於 1 月 22 日至 26 日在悉尼舉行。參見 linux.conf.au。


感謝 Akkana Peck 的提議和指正。





看完本文有收穫?請轉

發分享給更多人


關注「P

ython開發者」,提升Python技能


喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 Python開發者 的精彩文章:

2018,怎樣成為搶手的機器學習人才

TAG:Python開發者 |