前言
OpenShift 4.X 版本要求安裝在操作系統為 CoreOS 的機器上,因此 官方文檔 給出了使用 PXE 或 IPXE 引導 CoreOS 系統的方法。我們可以參考其操作流程,將一台 CentOS 7.X 的機器改寫為 CoreOS 系統,步驟如下:
-
從 鏡像下載頁 下載安裝所需版本的 kernel、initramfs 和 rootfs 文件,並將 rootfs 和點火文件(*.ign)上傳到自建的 HTTP 服務器上;
-
將 kernel 和 initramfs 文件拷貝到 CentOS 7.X 機器的 /boot 目錄下;
-
根據需求修改 /boot/grub2 目錄下的 grub.cfg 文件;
-
重啟機器。
對於操作系統初學者(比如我)來說,很難想象僅依靠添加和修改文件就能改變一台計算機的操作系統。為了解其實現原理,我們將對 Linux 的啟動流程進行討論,並從中說明上述操作是如何影響操作系統的。
Linux 啟動流程
啟動一台 Linux 機器的過程可以分為兩個部分:Boot 和 Startup。其中,Boot 起始於計算機啟動,在內核初始化完成且 systemd 進程開始加載后結束。緊接着, Startup 接管任務,使計算機達到一個用戶可操作的狀態。
Boot 階段
如上圖所示,Boot 階段又可以細分為三個部分:
- BIOS POST
- Boot Loader
- 內核初始化
BIOS POST
開機自檢(Power On Self Test,POST)是 基本輸入輸出系統(Basic I/O System,BIOS)的一部分,也是啟動 Linux 機器的第一個步驟。其工作對象是計算機硬件,因此對於任何操作系統都是相同的。POST 檢查硬件的基本可操作性,若失敗則 Boot 過程將會被終止。
POST 檢查完畢后會發出一個 BIOS 中斷調用 INT 13H,它將在任何可連接且可引導的磁盤上搜索含有有效引導記錄的引導扇區(Boot Sector),通常是 主引導扇區。引導扇區中的主引導記錄(Master Boot Record,MBR)將被加載到 RAM 中,然后控制權就會轉移到其手中。
Boot Loader
大多數 Linux 發行版使用三種 Boot Loader 程序:GRUB1、GRUB2 和 LILO,其中 GRUB2 是最新且使用最為廣泛的。GRUB2 代表“GRand Unified Bootloader, version 2”,它能夠定位操作系統內核並將其加載到內存中。GRUB2 還允許用戶選擇從幾種不同的內核中引導計算機,如果更新的內核版本出現兼容性問題,我們就可以恢復到先前內核版本。
GRUB1 的引導過程可以分為三個階段:stage 1、stage 1.5 和 stage 2。雖然 GRUB2 中並沒有 stage 的概念,但兩者的工作方式基本相同。為了方便說明,我們在討論 GRUB2 時將沿用 GRUB1 中 stage 的說法。
stage 1
上文提到,BIOS 中斷調用會定位主引導扇區,其結構如下圖所示:
主引導記錄首部的引導代碼便是 stage 1 文件 boot.img,它和 stage 1.5 文件 core.img 均位於 /boot/grub2/i386-pc 目錄下:
[root@bastion ~]# du -b /boot/grub2/i386-pc/*.img
512 /boot/grub2/i386-pc/boot.img
26664 /boot/grub2/i386-pc/core.img
它的作用是檢查分區表是否正確,然后定位和加載 stage 1.5 文件。446 字節的 boot.img 放不下能夠識別文件系統的代碼,只能通過計算扇區的偏移量來尋找,因此 core.img 必須位於主引導記錄和驅動器的第一個分區(partition)之間。第一個分區從扇區 63 開始,與位於扇區 0 的主引導記錄之間有 62 個扇區(每個 512 字節),有足夠的空間存儲大小不足 30000 字節的 core.img 文件。當 core.img 文件加載到 RAM 后,控制權也隨之轉移。
stage 1.5
相比於只能讀取原始扇區的 LILO,GRUB1 和 GRUB2 均可識別文件系統,這依賴於 stage 1.5 文件中內置的文件系統驅動程序。如果你擁有一台仍然使用 GRUB1 引導的 CentOS 6.X 機器,那么便可以在 /boot/grub/ 目錄下找到這些適配不同文件系統的 stage 1.5 文件:
[root@centos6.5 ~]# du -b /boot/grub/* | grep stage1_5
13380 /boot/grub/e2fs_stage1_5
12620 /boot/grub/fat_stage1_5
11748 /boot/grub/ffs_stage1_5
11756 /boot/grub/iso9660_stage1_5
13268 /boot/grub/jfs_stage1_5
11956 /boot/grub/minix_stage1_5
14412 /boot/grub/reiserfs_stage1_5
12024 /boot/grub/ufs2_stage1_5
11364 /boot/grub/vstafs_stage1_5
13964 /boot/grub/xfs_stage1_5
GRUB2 中的 core.img 不僅整合了上述文件系統驅動,還新增了菜單處理等模塊,這也是其優於 GRUB1 的地方。我們可以在 GNU GRUB Manual 2.06: Images 中找到對各種 GRUB 鏡像文件的詳細介紹。
既然 core.img 文件可以識別文件系統,那么它就能夠根據安裝時確定的系統路徑定位和加載 stage 2 文件。同樣,當 stage 2 文件加載到 RAM 后,控制權也隨之轉移。
stage 2
stage 2 文件並非是一個 .img 的鏡像,而是一些運行時內核模塊:
[root@bastion ~]# ls /boot/grub2/i386-pc/ | grep .mod | head
acpi.mod
adler32.mod
affs.mod
afs.mod
ahci.mod
all_video.mod
aout.mod
appendedsig.mod
appended_signature_test.mod
archelp.mod
它們的任務是根據 grub.cfg 文件的配置定位和加載內核文件,然后將控制權轉交給 Linux 內核。grub.cfg 文件存放在 /boot/grub2 目錄下:
[root@bastion ~]# head /boot/grub2/grub.cfg -n 5
#
# DO NOT EDIT THIS FILE
#
# It is automatically generated by grub2-mkconfig using templates
# from /etc/grub.d and settings from /etc/default/grub
通過該文件的注釋我們可以知道,它實際上是由 grub2-mkconfig 命令使用 /etc/grub.d 目錄下的一些模板文件並根據 /etc/default/grub 文件中的設置生成的:
[root@bastion ~]# ls /etc/grub.d/
00_header 00_tuned 01_users 10_linux 20_linux_xen 20_ppc_terminfo 30_os-prober 40_custom 41_custom README
40_custom 和 41_custom 文件常用於用戶對 GRUB2 配置的修改,實際上我們對機器的操作也是從這里開始的。為了讓 GRUB2 在機器啟動時選擇 CoreOS 系統內核而非默認的 CentOS,需要在原始 40_custom 文件末尾添加如下內容:
menuentry 'coreos' {
set root='hd0,msdos1'
linux16 /rhcos-live-kernel-x86_64 coreos.inst=yes coreos.inst.install_dev=vda rd.neednet=1 console=tty0 console=ttyS0 coreos.live.rootfs_url=http://{{HTTP-Server-Path}}/rhcos-live-rootfs.x86_64.img coreos.inst.ignition_url=http://{{HTTP-Server-Path}}/master.ign ip=dhcp
initrd16 /rhcos-live-initramfs.x86_64.img
}
所示的 Menuentry 由三條 Shell 命令組成:
set root='hd0,msdos1'
linux16 /rhcos-live-kernel-x86_64 ...
initrd16 /rhcos-live-initramfs.x86_64.img
第一條命令指定了 GRUB2 的根目錄,也就是 /boot 所在分區在計算機硬件上的位置。既然我們已經將內核文件拷貝到了 /boot 目錄下,那么能夠識別文件系統的 GRUB2 便可以定位和加載它。本例中hd
代表硬盤(hard drive),0
代表第一塊硬盤,mosdos
代表分區格式,1
代表第一個分區。詳細的硬件命名規范見 Naming Convention。
第二條命令將從rhcos-live-kernel-x86_64
(CoreOS 系統的內核文件)中以 16 位模式加載 Linux 內核映像,並通過coreos.live.rootfs_url
和coreos.inst.ignition_url
參數指定根文件系統(rootfs)的鏡像文件和點火文件的下載鏈接。ip=dhcp
代表該計算機網絡將由 DHCP 服務器動態配置,也可以按ip={{HostIP}}::{{Gateway}}:{{Genmask}}:{{Hostname}}::none nameserver={{DNSServer}}
的格式寫入靜態配置。
第三條命令將從rhcos-live-initramfs.x86_64.img
中加載 RAM Filesystem。GRUB2 讀取的內核文件實際上只包含了內核的核心模塊,缺少硬件驅動模塊的它無法完成 rootfs 的掛載。然而這些硬件驅動模塊位於 /lib/modules/$(uname -r)/kernel/ 目錄下,必須在 rootfs 掛載完畢后才能被識別和加載。為了解決這一問題,initramfs(前身為 initrd)應運而生。它是一個包含了必要驅動模塊的臨時 rootfs,內核可以從中加載所需的驅動程序。待真正的 rootfs 掛載完畢后,它便會從內存中移除。
除此之外我們還需要將 /etc/default/grub 文件中的 GRUB_DEFAULT=saved 修改為 GRUB_DEFAULT="coreos",使其與 40_custom 文件中的menuentry 'coreos'
對應。最后使用命令grub2-mkconfig -o /boot/grub2/grub.cfg
來重新生成一份 grub.cfg 文件,這樣計算機重啟后 GRUB2 就會根據我們的配置去加載 CoreOS 系統的內核了。
至此我們已經明白了為什么“僅依靠添加和修改文件就能改變一台計算機的操作系統”,但計算機想要達到用戶可操作狀態還遠不止於此。讓我們再來看看內核被加載到內存后發生了什么。
內核初始化
不同內核及其相關文件位於 /boot 目錄中,均以 vmlinuz 開頭:
[root@bastion ~]# ls /boot/ | grep vmlinuz
vmlinuz-0-rescue-20210623110808105647395700239158
vmlinuz-4.18.0-305.12.1.el8_4.x86_64
vmlinuz-4.18.0-305.3.1.el8.x86_64
內核通過壓縮自身來節省存儲空間,所以當選定的內核被加載到內存中后,它首先需要進行解壓縮(extracting)。一旦解壓完成,內核便會開始加載 systemd 並將控制權移交給它。
Startup 階段
systemd 是所有進程之父,它負責使計算機達到可以完成生產工作的狀態。其功能比過去的 init 程序要豐富得多,包括掛載文件系統、啟動和管理計算機所需的系統服務。當然你也可以將一些應用(如 Docker)以 systemd 的方式啟動,但它們與 Linux 的啟動無關,因此不在本文的討論范圍之內。
首先,systemd 根據 /etc/fstab 文件中的配置掛載文件系統。然后讀取 /etc 目錄下的配置文件,包括其自身的配置文件 /etc/systemd/system/default.target。該文件指定了 systemd 需要引導計算機到達的最終目標和狀態,實際上是一個軟鏈接:
[root@bastion ~]# ls /etc/systemd/system/default.target -l
lrwxrwxrwx. 1 root root 37 Oct 17 2019 /etc/systemd/system/default.target -> /lib/systemd/system/multi-user.target
在我使用的 bastion 服務器上,它指向的是 multi-user.target;對於帶有圖形化界面的桌面工作站,它通常指向 graphics.target;而對於單用戶模式的機器,它將指向 emergency.target。target 等效於過去 SystemV 中的 運行級別(Runlevel),它提供了別名以實現向后兼容性:
SystemV Runlevel | systemd target | systemd target alias | Description |
---|---|---|---|
halt.target | 在不關閉電源的情況下中止系統。 | ||
0 | poweroff.target | runlevel0.target | 中止系統並關閉電源。 |
s | emergency.target | 單用戶模式。 沒有服務正在運行,也未掛載文件系統。僅在主控制台上運行一個緊急 Shell,供用戶與系統交互。 | |
1 | rescue.target | runlevel1.target | 一個基本系統。文件系統已掛載,只運行最基本的服務和主控制台上的緊急 Shell。 |
2 | runlevel2.target | 多用戶模式。雖然還沒有網絡連接,但不依賴網絡的所有非 GUI 服務都已運行。 | |
3 | multi-user.target | runlevel3.target | 所有服務都在運行,但只能使用命令行界面(CLI)。 |
4 | runlevel4.target | 用戶自定義 | |
5 | graphical.target | runlevel5.target | 所有服務都在運行,並且可以使用圖形化界面(GUI)。 |
6 | reboot.target | runlevel6.target | 重啟系統 |
每個 target 都在其配置文件中指定了一組依賴,由 systemd 負責啟動。這些依賴是 Linux 達到某個運行級別所必須的服務(service)。換句話說,當一個 target 配置文件中的所有 service 都已成功加載,那么系統就達到了該 target 對應的運行級別。
下圖展示了 systemd 啟動過程中各 target 和 service 實現的一般順序:
cryptsetup-pre.target veritysetup-pre.target
|
(various low-level v
API VFS mounts: (various cryptsetup/veritysetup devices...)
mqueue, configfs, | |
debugfs, ...) v |
| cryptsetup.target |
| (various swap | | remote-fs-pre.target
| devices...) | | | |
| | | | | v
| v local-fs-pre.target | | | (network file systems)
| swap.target | | v v |
| | v | remote-cryptsetup.target |
| | (various low-level (various mounts and | remote-veritysetup.target |
| | services: udevd, fsck services...) | | remote-fs.target
| | tmpfiles, random | | | /
| | seed, sysctl, ...) v | | /
| | | local-fs.target | | /
| | | | | | /
\____|______|_______________ ______|___________/ | /
\ / | /
v | /
sysinit.target | /
| | /
______________________/|\_____________________ | /
/ | | | \ | /
| | | | | | /
v v | v | | /
(various (various | (various | |/
timers...) paths...) | sockets...) | |
| | | | | |
v v | v | |
timers.target paths.target | sockets.target | |
| | | | v |
v \_______ | _____/ rescue.service |
\|/ | |
v v |
basic.target rescue.target |
| |
________v____________________ |
/ | \ |
| | | |
v v v |
display- (various system (various system |
manager.service services services) |
| required for | |
| graphical UIs) v v
| | multi-user.target
emergency.service | | |
| \_____________ | _____________/
v \|/
emergency.target v
graphical.target
如上圖所示,想要到達到某個 target,其依賴的所有 target 和 service 就必須已完成加載。如實現 sysinit.target,需要先掛載文件系統(local-fs.target)、設置交換文件(swap.target)、初始化 udev (various low-level services)和設置加密服務(cryptsetup.target)等。不過,同一個 target 的不同依賴項可以並行執行。
當計算機達到 multi-user.target 或 graphical.target 時,它的漫漫啟動之路就走到了盡頭。但為了滿足用戶多樣的需求,它所面臨的挑戰其實才剛剛開始。
Future Work
雖然本文已對 Linux 的啟動流程進行了較為深入地討論,但仍有一些 Topic 值得我們繼續探索:
- 前言提到 RedHat 官方給出了 IPXE/PXE 引導 CoreOS 系統的方法,那么這項技術又是什么呢?
- MBR 只有 446 個字節,可為什么 boot.img 文件卻有 512 個字節?
- 目前已經有越來越多的計算機使用 UEFI 和 GPT 來代替 BIOS 和 MBR,其優勢體現在哪?
- 我們該如何理解 systemd 的配置文件?如何使用 systemd 部署我們的應用?
參考文獻
Creating Red Hat Enterprise Linux CoreOS (RHCOS) machines by PXE or iPXE Booting