前言

这篇博客主要介绍磁盘中的两个重要概念——文件系统分区。并通过这些概念,理解grub构建启动盘的原理

分区

出于某些原因(如需要多重引导等),磁盘需要被分割成多个独立的分区进行管理

则这些分区信息(被称为分区表)需要被存储在磁盘的固定位置(往往是磁盘起始地址),从而方便其余设备识别分区信息

现在常用的分区表格式有Master Boot Record(MBR)Globally-Unique-Identifiers Partition Table(gpt)

MBR

MBR位于磁盘的第一个扇区中,其基本格式如下所示
MBR布局

GPT

GPT位于磁盘的前34个扇区,其基本格式如下所示
GPT布局

可以看到,其兼容MBR

文件系统

xv6中有一句经典的描述

文件系统又被称为on-disk数据结构

因此,文件系统中,数据在磁盘上组织逻辑就是整个文件系统的重点之一,其一般布局如下所示
文件系统布局

创建一个文件系统,就是创建该文件系统的诸如superblock等的metadata

分区和文件系统

实际上,分区文件系统的关系并不是非常紧密。分区是解析磁盘的方式,主要解析partition table;而文件系统是解析磁盘数据的方式,主要解析相关的superblock

所以,superblock的管理和partition table的管理,实际上关联不大。换句话说,即使没有磁盘分区,仍然能创建文件系统,如下所示
创建文件系统

在挂载的loop设备上,没有分区,仍然可以正常创建文件系统

内核实现

源码之下,了无秘密

这里通过分析Linux内核中,关于分区文件系统部分的相关代码,更深入了解分区文件系统的关系

分区

Linux内核中,磁盘分区struct gendiskstruct block_device相关,其关系如下所示
分区结构示意图

当内核需要解析磁盘内容时,会尝试使用check_partition()函数解析分区,调用栈如下所示
解析SCSI磁盘分区调用栈

文件系统

Linux内核中,文件系统struct file_system_typestruct super_block(in-memory数据结构,不同的文件系统有各自的on-disk super block)struct mount相关,其关系如下所示
文件系统结构示意图(参考[醉卧沙场](https://zhuanlan.zhihu.com/p/67831794))

当内核需要解析文件系统时,也就是需要进行mount()时,会尝试使用vfs_get_tree()函数,调用回调函数fc->ops->get_tree()去解析文件系统,流程如内核文档所示,调用栈如下所示
解析ext4文件系统调用栈

总结

通过Linux内核的相关代码,可以非常清楚的看到,分区文件系统没有直接关系。

内核会将磁盘和通过blk_add_partitions()解析出的磁盘的分区,都抽象成struct block_device

当内核执行mount()解析文件系统时,内核通过vfs_get_tree(),调用相关的fc->ops->get_tree()回调函数,从对应的struct block_device抽象的设备中,读取对应文件系统约定的block,完成super block数据的读取和解析

可以看到,由于内核将磁盘和磁盘的分区,都统一抽象成struct block_device,并且之后的数据交互都基于struct block_device暴露的接口进行。自然的,文件系统分区并无直接关系

操作系统启动

这里以常用的grub2(Grand Unified Bootloader2)为例,参考grub官方文档

操作系统的启动逻辑可以抽象成非常简单的一个过程——CPU跳转到固定地址,执行对应的flat binary。其中,flat binary会完成必要的初始化工作(如加载内核镜像等),并执行内核代码

当然,对于不同的启动模式,具体细节差距会很大,其基本可以分为BIOS(Basic Input/Output System)UEFI(Unified Extensible Firmware Interface)两种模式

BIOS

BIOS下,操作系统的启动流程如下所示
BIOS启动流程

CPU上电后,会从0xffff0物理地址处,开始执行位于ROMBIOS代码。

之后,BIOS会尝试寻找bootable device,即含有MBR分区的设备。

找到符合条件的设备后,载入磁盘的第一个扇区(即Master Boot Record)到0x07C00物理地址处,开始执行MBR,即BootLoader

BIOS中,该BootLoader的大小受到严格限制,因此其作用就是装载剩余的grub image至内存,而该grub image会加载必要的模块(如磁盘分区模块、文件系统模块)等,从而读取磁盘中的设置文件,并根据该设置文件加载内核并启动即可

UEFI

UEFI下,操作系统的启动流程如下所示
UEFI启动流程

CPU上电后,会执行位于ROMUEFI代码,其会在Driver Execution Environment阶段(UEFI执行的某一个阶段)加载必要的模块(诸如磁盘分区模块、文件系统模块等)

在完成UEFI代码执行后,不同于BIOS去查找bootable device,其会直接装载指定路径的BootLoader(UEFI此时可以识别磁盘分区和文件系统):如果NVRAM中存储有,则直接读取该信息并加载相关路径下的;否则加载默认路径{efi_system_partition}/EFI/BOOT/BOOT{machine_type}BootLoader即可

UEFI中,BootLoader没有大小限制,则其可以直接加载必要的模块(如磁盘分区模块、文件系统模块)等,从而读取磁盘中的设置文件,并根据该设置文件加载内核并启动即可

构建启动盘

BIOS

根据前面的分析,实际上在磁盘上需要安装BootLoadergrub image和相关的配置文件,即可完成启动盘的构建

  1. 磁盘分区
    虽然这一步并不是必须的,但是平时使用时,我们都会将磁盘进行分区,如下所示
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    hawk@xiaoxin:~/Desktop$ sudo fdisk /dev/loop11

    Welcome to fdisk (util-linux 2.34).
    Changes will remain in memory only, until you decide to write them.
    Be careful before using the write command.


    Command (m for help): o
    Created a new DOS disklabel with disk identifier 0x6df869f1.

    Command (m for help): n
    Partition type
    p primary (0 primary, 0 extended, 4 free)
    e extended (container for logical partitions)
    Select (default p):

    Using default response p.
    Partition number (1-4, default 1):
    First sector (2048-4194303, default 2048):
    Last sector, +/-sectors or +/-size{K,M,G,T,P} (2048-4194303, default 4194303): +1G

    Created a new partition 1 of type 'Linux' and of size 1 GiB.

    Command (m for help): n
    Partition type
    p primary (1 primary, 0 extended, 3 free)
    e extended (container for logical partitions)
    Select (default p):

    Using default response p.
    Partition number (2-4, default 2):
    First sector (2099200-4194303, default 2099200):
    Last sector, +/-sectors or +/-size{K,M,G,T,P} (2099200-4194303, default 4194303):

    Created a new partition 2 of type 'Linux' and of size 1023 MiB.

    Command (m for help): w
    The partition table has been altered.
    Calling ioctl() to re-read partition table.
    Syncing disks.
  2. 初始化文件系统
    这里在两个分区上分别初始化为FAT32文件系统和EXT2文件系统,从而存储启动设置等文件
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    hawk@xiaoxin:~/Desktop$ sudo mkfs.vfat -F 32 /dev/loop11p1 
    mkfs.fat 4.1 (2017-01-24)

    hawk@xiaoxin:~/Desktop$ sudo mkfs.ext2 /dev/loop11p2
    mke2fs 1.45.5 (07-Jan-2020)
    Creating filesystem with 261888 4k blocks and 65536 inodes
    Filesystem UUID: b7f11d0d-d3be-435d-ba3c-1572ecf26771
    Superblock backups stored on blocks:
    32768, 98304, 163840, 229376

    Allocating group tables: done
    Writing inode tables: done
    Writing superblocks and filesystem accounting information: done
  3. 安装BootLoadergrub image
    即调用grub-install,命令如下所示
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    hawk@xiaoxin:~/Desktop$ sudo mount -o rw /dev/loop11p1 mnt/

    hawk@xiaoxin:~/Desktop$ sudo grub-install --target=i386-pc --boot-directory=mnt /dev/loop11
    Installing for i386-pc platform.
    Installation finished. No error reported.

    hawk@xiaoxin:~/Desktop$ tree -d mnt/
    1 [error opening dir]
    mnt/
    └── grub
    ├── fonts
    └── i386-pc

    3 directories
    这里简单解释一下grub-install命令中的部分参数
    • —boot-directory=mnt: 即grub设置文件被安装的位置,这部分数据是grub已经加载完相关模块(如磁盘分区模块、文件系统模块)后加载的,因此参数值是文件系统的目录项名称
    • /dev/loop11: 即BootLoader和部分grub image将被安装的磁盘。由于BootLoader和部分grub image会被安装到磁盘的起始扇区处,和分区无关,因此参数值是磁盘设备,而非磁盘分区
  4. 设置配置文件
    grub在启动时,会读取{boot_directory}/grub/grub.cfg的配置文件,从而执行相应的操作
    因此,我们需要在相关的路径下,参考grub官方文档初始化一个grub.cfg,如下所示

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    hawk@xiaoxin:~/Desktop$ lsblk -o +UUID
    NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT UUID
    loop11 7:11 0 2G 0 loop
    ├─loop11p1
    │ 259:5 0 1G 0 part /home/hawk A6E8-C20A
    └─loop11p2
    259:6 0 1023M 0 part b7f11d0d-d3be-435d-ba3c-1572ecf26771

    hawk@xiaoxin:~/Desktop$ cat mnt/grub/grub.cfg
    menuentry "v6.0-rc6" {
    insmod /grub/i386-pc/ext2.mod
    search --fs-uuid --set=root b7f11d0d-d3be-435d-ba3c-1572ecf26771
    linux /v6.0-rc6/bzImage
    }

    可以看到,grub.cfg有一个菜单项,其会首先加载相关的模块(以{boot_directory}作为根目录),然后尝试切换根目录到指定uuid文件系统实例,加载相应路径下的内核。而相关的文件系统实例内容如下所示

    1
    2
    3
    4
    5
    6
    7
    8
    hawk@xiaoxin:~/Desktop$ sudo mount -o rw /dev/loop11p2 disk
    hawk@xiaoxin:~/Desktop$ tree disk
    disk
    ├── lost+found [error opening dir]
    └── v6.0-rc6
    └── bzImage

    2 directories, 1 file

    此时,一个启动盘已经构建好了,可以参考QEMU官方文档,使用如下所示的QEMU启动脚本进行测试

    1
    2
    3
    4
    5
    6
    7
    hawk@xiaoxin:~/Desktop$ cat start.sh 
    #!/bin/sh

    qemu-system-x86_64 \
    -drive file=disk.img,index=0,media=disk

    hawk@xiaoxin:~/Desktop$ ./start.sh

    BIOS下qemu测试

  5. 创建根文件系统
    如果没有指定根文件系统,则Linux内核无法正常加载文件系统模块,如下所示
    未指定根文件系统

    这里使用busybox,构建一个简单的根文件目录,并更新相关的设置文件即可,如下所示

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    hawk@xiaoxin:~/Desktop/busybox-1.35.0/_install$ ls
    bin init linuxrc sbin usr

    hawk@xiaoxin:~/Desktop/busybox-1.35.0/_install$ cat init
    #!/bin/sh
    exec /bin/sh

    hawk@xiaoxin:~/Desktop/busybox-1.35.0/_install$ sudo sh -c 'find . | cpio -o --format=newc > /home/hawk/Desktop/disk/v6.0-rc6/rootfs.cpio'
    5390 blocks

    hawk@xiaoxin:~/Desktop/busybox-1.35.0/_install$ cat ~/Desktop/mnt/grub/grub.cfg
    menuentry "v6.0-rc6" {
    insmod /grub/i386-pc/ext2.mod
    search --fs-uuid --set=root b7f11d0d-d3be-435d-ba3c-1572ecf26771
    linux /v6.0-rc6/bzImage
    initrd /v6.0-rc6/rootfs.cpio
    }

    此时,可以正常进入Linuxbash,如下图所示
    包含根文件系统

UEFI

类似BIOS,实际上在磁盘上需要安装BootLoader和相关的配置文件,即可完成启动盘的构建

  1. 磁盘分区
    UEFI启动时必须需要有EFI分区,因此将磁盘进行如下分区
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    hawk@xiaoxin:~/Desktop$ sudo losetup -fP disk.img 

    hawk@xiaoxin:~/Desktop$ sudo losetup -a | grep "disk.img"
    /dev/loop11: [66306]:1852129 (/home/hawk/Desktop/disk.img)

    hawk@xiaoxin:~/Desktop$ sudo fdisk /dev/loop11

    Welcome to fdisk (util-linux 2.34).
    Changes will remain in memory only, until you decide to write them.
    Be careful before using the write command.


    Command (m for help): g
    Created a new GPT disklabel (GUID: 17082A27-61F6-0A4B-8D3C-2A685B71BA7C).
    The old dos signature will be removed by a write command.

    Command (m for help): n
    Partition number (1-128, default 1):
    First sector (2048-4194270, default 2048):
    Last sector, +/-sectors or +/-size{K,M,G,T,P} (2048-4194270, default 4194270): +256M

    Created a new partition 1 of type 'Linux filesystem' and of size 256 MiB.
    Partition #1 contains a vfat signature.

    Do you want to remove the signature? [Y]es/[N]o: y

    The signature will be removed by a write command.

    Command (m for help): n
    Partition number (2-128, default 2):
    First sector (526336-4194270, default 526336):
    Last sector, +/-sectors or +/-size{K,M,G,T,P} (526336-4194270, default 4194270):

    Created a new partition 2 of type 'Linux filesystem' and of size 1.8 GiB.

    Command (m for help): t
    Partition number (1,2, default 2): 1
    Partition type (type L to list all types): 1

    Changed type of partition 'Linux filesystem' to 'EFI System'.

    Command (m for help): t
    Partition number (1,2, default 2):
    Partition type (type L to list all types): 24

    Changed type of partition 'Linux filesystem' to 'Linux root (x86-64)'.

    Command (m for help): w
    The partition table has been altered.
    Calling ioctl() to re-read partition table.
    Syncing disks.
  2. 初始化文件系统
    这里在两个分区上初始化为FAT32文件系统和EXT2文件系统,从而存储启动设置等文件
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    hawk@xiaoxin:~/Desktop$ sudo mkfs.vfat -F 32 /dev/loop11p1 
    mkfs.fat 4.1 (2017-01-24)

    hawk@xiaoxin:~/Desktop$ sudo mkfs.ext2 /dev/loop11p2
    mke2fs 1.45.5 (07-Jan-2020)
    Discarding device blocks: done
    Creating filesystem with 458491 4k blocks and 114688 inodes
    Filesystem UUID: d59e6ba7-6bce-4cbe-ab6f-44d778c3fbe6
    Superblock backups stored on blocks:
    32768, 98304, 163840, 229376, 294912

    Allocating group tables: done
    Writing inode tables: done
    Writing superblocks and filesystem accounting information: done
  3. 安装BootLoader
    即调用grub-install,命令如下所示
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    hawk@xiaoxin:~/Desktop$ sudo mount -o rw /dev/loop11p1 mnt/

    hawk@xiaoxin:~/Desktop$ sudo grub-install --efi-directory=mnt --boot-directory=mnt --removable
    Installing for x86_64-efi platform.
    Installation finished. No error reported.

    hawk@xiaoxin:~/Desktop$ tree -d mnt/
    1 [error opening dir]
    mnt/
    ├── EFI
    │   └── BOOT
    └── grub
    ├── fonts
    └── x86_64-efi

    5 directories
    这里简单解释一下grub-install命令中的部分参数
    • —boot-directory=mnt: 即grub设置文件被安装的位置,这部分数据是grub已经加载完相关模块(如磁盘分区模块、文件系统模块)后加载的,因此参数值是文件系统的目录项名称
    • —efi-directory=mnt: 即BootLoader被安装的路径。不同于BIOSUEFI可以识别磁盘分区文件系统,因此参数值是BootLoader所在的文件系统的目录项名称
  4. 设置配置文件
    grub在启动时,会读取{boot_directory}/grub/grub.cfg的配置文件,从而执行相应的操作
    因此,我们需要在相关的路径下,参考grub官方文档初始化一个grub.cfg,如下所示

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    hawk@xiaoxin:~/Desktop$ lsblk -o +UUID
    NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT UUID
    loop11 7:9 0 2G 0 loop
    ├─loop11p1 259:5 0 256M 0 part /home/hawk/Desktop/mnt C573-58EB
    └─loop11p2 259:6 0 1.8G 0 part d59e6ba7-6bce-4cbe-ab6f-44d778c3fbe6

    hawk@xiaoxin:~/Desktop$ cat mnt/grub/grub.cfg
    menuentry "v6.0-rc6" {
    insmod /grub/x86_64-efi/ext2.mod
    search --fs-uuid --set=root d59e6ba7-6bce-4cbe-ab6f-44d778c3fbe6
    linux /v6.0-rc6/bzImage
    }

    可以看到,grub.cfg有一个菜单项,其会首先加载相关的模块(以{boot_directory}作为根目录),然后尝试切换根目录到指定uuid文件系统实例,加载相应路径下的内核。而相关的文件系统实例内容如下所示

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    hawk@xiaoxin:~/Desktop$ sudo mount -o rw /dev/loop11p2 disk

    hawk@xiaoxin:~/Desktop$ tree disk
    disk
    ├── lost+found [error opening dir]
    └── v6.0-rc6
    └── bzImage

    2 directories, 1 file
    hawk@xiaoxin:~/Desktop$

    这里需要特别说明,需要按照Linux官方文档,编译支持UEFILinux内核。此时,一个启动盘已经构建好了,可以参考QEMU官方文档UEFI固件项目,使用如下所示的QEMU启动脚本进行测试

    1
    2
    3
    4
    5
    6
    7
    8
    9
    hawk@xiaoxin:~/Desktop$ cat start.sh 
    #!/bin/sh

    qemu-system-x86_64 \
    -bios /usr/share/ovmf/OVMF.fd \
    -m 4G \
    -drive file=disk.img,index=0,media=disk

    hawk@xiaoxin:~/Desktop$ ./start.sh

    UEFI下qemu测试

  5. 创建根文件系统
    如果没有指定根文件系统,则Linux内核无法正常加载文件系统模块,如下所示
    未指定根文件系统

    这里使用busybox,构建一个简单的根文件目录,并更新相关的设置文件即可,如下所示

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    hawk@xiaoxin:~/Desktop/busybox-1.35.0/_install$ ls
    bin init linuxrc sbin usr

    hawk@xiaoxin:~/Desktop/busybox-1.35.0/_install$ cat init
    #!/bin/sh
    exec /bin/sh

    hawk@xiaoxin:~/Desktop/busybox-1.35.0/_install$ sudo sh -c 'find . | cpio -o --format=newc > /home/hawk/Desktop/disk/v6.0-rc6/rootfs.cpio'
    5390 blocks

    hawk@xiaoxin:~/Desktop/busybox-1.35.0/_install$ cat ~/Desktop/mnt/grub/grub.cfg
    menuentry "v6.0-rc6" {
    insmod /grub/i386-pc/ext2.mod
    search --fs-uuid --set=root d59e6ba7-6bce-4cbe-ab6f-44d778c3fbe6
    linux /v6.0-rc6/bzImage
    initrd /v6.0-rc6/rootfs.cpio
    }

    此时,可以正常进入Linuxbash,如下图所示
    包含根文件系统