初识固件接口和磁盘分区:计算机是如何启动的?

前些时间为了给笔记本上的Arch Linux系统增加存储空间,折腾了一下磁盘分区,借此了解了磁盘分区、固件接口等等与计算机启动过程相关的底层结构,记录如下。

固件与固件接口 (Firmware Interface)

在操作系统之下,直接操作硬件的底层控制软件称为固件(Firmware)。顾名思义,“固”件就是固定在硬件中的软件,按照这个理解,的确有很多固件被存储于ROM、EEPROM之类的只读存储器上,很难变更。然而也有一些固件被存储在磁盘上,没有被定死,一个典型的例子就是引导程序 bootloader。事实上,固件只要存储在非易失性存储器上就可以了。

固件接口定义了计算机启动时操作系统和固件(也就是嵌入硬件当中的软件)之间的交互过程,是二者之间的桥梁。常见的固件接口有两个,分别是当下常用的较新的标准 UEFI (Unified Extensible Firmware Interface) 和较旧的遗留的标准 BIOS (Basic Input/Output System)。

既然要负责计算机的启动,固件接口必须解决从哪里加载 bootloader,怎样搜索要启动的操作系统的问题。如前所述,两者都在磁盘上。但具体在磁盘的哪里呢?这就与磁盘的格式有关了。

磁盘分区格式

为了方便管理磁盘,通常把磁盘划分为若干个分区来管理。正如文件系统的第一个块是 Superblock 一样,磁盘的特殊位置也要有相应的结构来记录磁盘的区域划分,这个结构就是分区表 (partition table)。以两种常见的磁盘分区格式 MBR (Master Boot Record) 和 GPT (GUID Partition Table) 为例,MBR的分区表在磁盘的第一个扇区(sector)上,而 GPT 的分区表在磁盘的第一个和最后一个扇区。

GPT 是一种较新的标准,MBR 是一种较旧的标准。UEFI 通常支持 GPT 磁盘分区标准,如果需要使用 MBR,需要打开 UEFI 的 legacy support。

以 UEFI + GPT 的组合为例,GPT 的每一个表项会记录分区的起止位置、分区类型和一个独属于每个分区的 GUID (globally unique identifier)。在计算机启动的过程中,会去寻找 EFI System Partition 类型的分区并从中加载引导程序,这就解决了“引导程序在哪里”的问题。如果分区包含了可供启动的操作系统,这会在分区类型上体现出来,于是可以轻松列出磁盘上有哪些可供启动的操作系统。

在 GPT 分区方案里,每个分区的 GUID 是随机生成而互不相同的,方便了分区的识别和管理。相似地,ext4 等文件系统也会使用相同的机制生成 128位的 UUID,存储在相应文件系统的 superblock 里,方便文件系统的识别和管理。磁盘上所有文件系统的 UUID 等信息可以通过如 `lsblk -o name,mountpoint,label,uuid,fstype` 等命令来获得,而且常被 fstab 配置文件、fdisk 工具使用;而 GUID 则不容易查询得到了,也很少被直接使用。

实例:Linux kernel 初始化时的挂载过程

如果使用 GRUB,那么可以通过观察其配置脚本来验证相应的启动过程:

首先,通过 UUID 来搜索 EFI System Partition 并设置成一个临时的根目录:`search –fs-uuid –set=root XXXX-…-XXXX`

然后,找到并调用存在于 EFI System Partition 中的 linux kernel,通过 UUID 指定目标的分区,作为调用的参数,分区将被挂载到启动过程结束后的根目录:`linux /vmlinuz-linux root=UUID=XXXX-…-XXXX`

脚本的最后,建立一个初始的文件系统:`initrd /initramfs-linux.img`

在成功挂载真正的根文件系统之后,操作系统可以进一步初始化了,例如读取根文件系统中的 /etc/fstab 文件,并据此挂载更多的分区。

应用:分区的移动和扩大

假想有如下情形:你有一个 25GB 的磁盘,磁盘的前 5GB 未分配,后 20 GB 是一个分区,分区里是一个 ext4 文件系统,它是一个操作系统的根文件系统。现在假设该文件系统占用了将近全部 20GB 的空间,你想要给它扩容,如何操作?

首先,既然想给文件系统扩容,必要的条件是扩大分区。然而,扩大分区还不够-文件系统的 superblock 在磁盘的 5GB 处,而受 superblock 这一“左边界”的制约,文件系统只能“向右”扩张,难以“向左”扩张,所以磁盘左边的 5GB 存储空间难以被利用。

一个解决方法是复制(如逐字节复制)整个文件系统,使得 superblock 移动到磁盘的 0GB 处。这个操作可以通过危险的 dd 命令来完成。之所以说这个操作危险,是因为如下情况:如果我们把复制的方向倒转,把区间 [0, 20] 的内容复制到 [5, 25],由于 dd 默认从“左”到“右”复制且 blocksize 一般不会超过 GB 的量级,原先的 [5, 20] 区间的内容会被复制的内容覆盖,我们最终会得到 5 份原先的 [0, 5] 区间内的数据,造成数据丢失。所以,使用 dd 时要尽量避免“源”区间与“目的”区间重叠的情况,否则可能出现预期之外的结果。

逐字节复制数据后,我们还需要修改分区表,否则由于分区表中的内容和实际硬盘中的字节不一致,我们无法通过这个磁盘启动。这可以通过在 fdisk 命令中删除旧分区并创建新分区来完成,创建的新分区可以利用全部 25GB 的空间。

最后,使用 resize2fs 等命令,扩大 ext4 文件系统,使其利用新分区的全部空间。

令人惊奇的是,我们不需要修改启动使用的 GRUB 脚本或者 fstab 文件,就可以启动分区中的操作系统了。这是因为两者都使用 UUID 来识别文件系统或分区,而这个 UUID 是存储在 ext4 文件系统的 superblock 中的,被 dd 命令被原样复制到了新的地方。

注:为了避免损坏系统丢失数据,建议在虚拟机中进行上述实验。在真机上操作分区可以使用 GParted (on Linux) 或者 DiskGenius (on Windows) 等工具,避免手动复制字节或设置分区表。


Posted

in

by

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *