前些时间为了给笔记本上的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) 等工具,避免手动复制字节或设置分区表。
附注
【2024年10月18日更新】关于移动 Windows 恢复分区
克隆 Windows 恢复分区到新位置,并删除原有 Windows 恢复分区后,GPT 表项中存储的 GUID 和 attributes 可能不会被复制到新表项中,因此 Windows 可能无法识别并显示出新位置上的分区是恢复分区。为此,需要手动设置。在 Windows 管理员命令行中,运行 diskpart,进入其交互环境,然后:
list disk
select disk X
list partition
select partition Y 注:恢复分区新位置的分区号
set id="de94bba4-06d1-4d40-a16a-bfd50179d6ac" 注释1
gpt attributes=0x8000000000000001 注释2
exit
注释1,这一行设置了新分区在 GPT 表项中的 PartitionType 变量,这个以 de94bb 开头的 id 是一个 GUID,含义为 “本类别为 Windows 恢复分区”,见 (1) (2)
注释2,这一行设置了新分区的 EFI attribute,开头的 8 表示不要分配盘符,结尾的 1 表示这个分区是计算机运行所必须的。参考链接同上。
验证
然后,可以在管理员命令行中检查配置是否成功(或者在 Windows 自带磁盘管理工具中检查)
reagentc /disable
reagnetc /enable
reagentc /info
info 输出的分区应该对应新分区,磁盘管理工具应该能够标志出新分区是恢复分区。
移除盘符
如果电脑挂载了新恢复分区,并为其分配了驱动器盘符(如 “F:”),可以在 diskpart 环境中通过如下方法移除盘符:
list volume 找出所有卷,卷(volume)是一个或多个分区
select volume <volume_number>
remove letter=<drive_letter> 一个驱动器(drive)就是一个分配了字母代号的卷
Leave a Reply