mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
4462 字
12 分钟
构建完全自主的 Linux 发行版
2020-09-12

一、什么时候需要构建自主的 Linux 发行版#

目前开源的 Linux 发行版中,有完全自由的 GNU/Linux,也有企业维护和提供支持的 Enterprise Linux(比如 RHEL)。通常在没有任何特殊要求的情况下,选择任意一个发行版都能正常上手使用以及运行自己编写的软件。但有时我们不得不考虑未来的情况,比如出现故障时是否方便排查,系统中是否存在很多不必要的软件和服务,是否存在漏洞以及如何进行修复等。

解决这些问题最简单的方式就是购买企业级 Linux 的技术支持服务。企业 Linux 发行版是专门为满足商业用户的需求而设计的,它不仅提供可靠性以及及时和专业的支持,还需要确保兼容并支持各种硬件平台和企业软件。

但如果想节省这方面的开支,我们就得自己对发行版的组成与制作知根知底,完全自主把控。通过广泛的测试确保发行版能够与企业硬件、数据库和应用程序良好兼容,并发挥出最优的性能。

二、发行版的组成#

一个典型的 Linux 发行版由以下几个部分组成:

  • Bootloader:负责在计算机启动时初始化硬件并加载内核。
  • Linux Kernel:操作系统的核心,负责管理系统资源,包括进程管理、内存管理、设备驱动、文件系统、系统调用等。
  • Libraries:系统的库文件,包括标准 C 库和其他编程接口,它们为应用程序提供了底层的系统服务和功能。比如 glibc,它封装了系统调用,提供了基本的 C 库,绝大多数 C 程序都依赖它。
  • System Utilities:系统工具和命令行程序,如文件管理、磁盘管理、网络配置等。
  • Shell:命令解释器,允许用户与操作系统交互。
  • Package Management:软件包管理系统,允许用户安装、更新和管理软件包,比如 apt 和 dnf(原 yum)。
  • System Daemons and Services:后台服务和守护进程,提供持续运行的系统功能,以下是一些典型服务:
    • Systemd:一种进程管理器,用于初始化系统服务和控制它们的生命周期。
    • D-Bus:一种进程间通信机制。
    • SSHD (sshd):安全外壳协议守护进程,提供远程登录功能。
    • Crond:定时任务守护进程。
    • Chronyd:网络时间同步守护进程,同步系统时间。
    • Udevd:设备管理守护进程,处理硬件设备的添加和移除。
    • NetworkManager:网络连接和配置的守护进程。
  • Graphical User Interface (GUI):图形用户界面,包括窗口管理器、桌面环境等。
  • Applications:预装的应用软件,包括文本编辑器、办公软件、网页浏览器等,以下是一些典型应用:
    • vim:文本编辑器
    • gcc:开发工具
    • make:构建工具
    • git:代码版本管理工具

其中,System Daemons and Services 中最重要最特殊的守护进程即 init 程序。它是系统启动时运行的第一个进程,负责启动系统上的其他进程,并按照系统初始化脚本或配置文件中的指示进行系统初始化。以前流行 System V init,目前大多使用 systemd 作为 init 进程。

三、开始自主构建#

首先需要列出需要构建的制品:

  • 操作系统内核
  • 用户根文件系统构建(包括系统库、配置和工具)
  • 安装光盘以及 Linux 安装器镜像

3.1 内核构建#

首先需要下载编译内核所需要的工具:

yum install -y gcc ncurses-devel perl-devel

在主机上下载特定分支的源码,并且进行内核和模块的配置和编译:

# 下载当前内核官网中最新的源码包
wget https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.9.3.tar.xz
# 解压并进入目录
tar -xvf linux-6.9.3.tar.xz && cd linux-6.9.3
# 生成当前架构的默认配置
make defconfig
# 通过菜单编辑内核和模块的配置
make menuconfig
# 编译内核,编译完成后会放在 arch/x86/boot/bzImage,也可以通过 make -s image_name 查看位置
make bzImage
# 编译模块
make modules
# 安装模块到 /lib/modules/<version> 目录
make modules_install

我们既可以从 GitHub 下载当前最新版本的内核源码,也可以从内核官网的镜像网站中下载长期支持或稳定的内核源码。

在配置内核时,[] 中有三种可能的值:

  • [*]:表示编译进内核;
  • [M]:表示编译为内核模块;
  • []:表示不对该模块进行编译。

使用默认配置即可满足绝大多数场景,但也可以参考 LFS 中对内核的配置和常见发行版的内核配置。内核配置文件一般可以在 /boot/config* 或者 /proc/config.gz 中找到,我们也可以使用发行版的配置:

### Debian 和 Fedora 及其衍生版:
$ cp /boot/config-"$(uname -r)" .config
### Arch Linux 及其衍生版:
$ zcat /proc/config.gz > .config
# 将旧的 .config 文件将被重命名为 .config.old 进行备份,并将新的更改写入至 .config 文件
make olddefconfig

3.2 用户根文件系统构建#

这里也就是安装一些持久到磁盘、存在于用户根文件系统中的系统工具和命令、Shell、包管理器、后台服务和守护进程以及应用的地方。安装这些软件时需要考虑和内核版本的兼容性。这个过程可以参考 LFS Project。这里为了简单起见,直接使用了 debootstrap 工具制作用户根文件系统:

# 创建用户根文件系统目录
mkdir rootfs
# include 表示需要额外安装的软件包
# components 表示需要额外安装的软件包组
# arch 表示架构
# bionic 表示发行版别名
# rootfs 表示构建输出目录
# http://mirrors.ustc.edu.cn/ubuntu/ 表示软件包源
debootstrap --include=whiptail,ca-certificates,tzdata --components=main,contrib,non-free --arch=amd64 bionic rootfs/ http://mirrors.ustc.edu.cn/ubuntu/

注 1:构建根文件系统比较方便的工具都依赖包管理器,比如 apt 系的 debootstrap,比如 rpm 系的 febootstrap。CentOS ISO 安装盘也是在 anaconda 安装器配置完成后通过包管理器安装的用户根文件系统,比如内核和 initramfs 就存在于 kernel-core 软件包中。

$ rpm -qlp BaseOS/Packages/kernel-core-6.8.0-0.rc3.20240209git1f719a2f3fa6.31.el10.x86_64.rpm
warning: BaseOS/Packages/kernel-core-6.8.0-0.rc3.20240209git1f719a2f3fa6.31.el10.x86_64.rpm: Header V4 RSA/SHA256 Signature, key ID 8483c65d: NOKEY
/boot/.vmlinuz-6.8.0-0.rc3.20240209git1f719a2f3fa6.31.el10.x86_64.hmac
/boot/System.map-6.8.0-0.rc3.20240209git1f719a2f3fa6.31.el10.x86_64
/boot/config-6.8.0-0.rc3.20240209git1f719a2f3fa6.31.el10.x86_64
/boot/initramfs-6.8.0-0.rc3.20240209git1f719a2f3fa6.31.el10.x86_64.img
/boot/symvers-6.8.0-0.rc3.20240209git1f719a2f3fa6.31.el10.x86_64.xz
/boot/vmlinuz-6.8.0-0.rc3.20240209git1f719a2f3fa6.31.el10.x86_64
/lib/modules
/lib/modules/6.8.0-0.rc3.20240209git1f719a2f3fa6.31.el10.x86_64
/lib/modules/6.8.0-0.rc3.20240209git1f719a2f3fa6.31.el10.x86_64/.vmlinuz.hmac
/lib/modules/6.8.0-0.rc3.20240209git1f719a2f3fa6.31.el10.x86_64/System.map
/lib/modules/6.8.0-0.rc3.20240209git1f719a2f3fa6.31.el10.x86_64/config
/lib/modules/6.8.0-0.rc3.20240209git1f719a2f3fa6.31.el10.x86_64/modules.builtin
/lib/modules/6.8.0-0.rc3.20240209git1f719a2f3fa6.31.el10.x86_64/modules.builtin.modinfo
/lib/modules/6.8.0-0.rc3.20240209git1f719a2f3fa6.31.el10.x86_64/symvers.xz
/lib/modules/6.8.0-0.rc3.20240209git1f719a2f3fa6.31.el10.x86_64/vmlinuz
/usr/share/licenses/kernel-core
/usr/share/licenses/kernel-core/COPYING-6.8.0-0.rc3.20240209git1f719a2f3fa6.31.el10

3.3 安装光盘以及 Linux 安装器镜像构建#

三、参考现有发行版的光盘结构以及安装流程#

制作完成一个操作系统发行版后,需要通过一些手段将它分发出去,其中 ISO 就是一种比较广泛的分发方法。首先,在了解如何制作一个 ISO 之前,可以先查看 CentOS Stream ISO 文件中一般包含哪些文件:

# 新版本 CentOS Stream 的光盘文件内容
$ mkdir -p /mnt/iso
$ mount -o loop CentOS-Stream-10-latest-x86_64-dvd1.iso /mnt/iso/
$ cd /mnt/iso
$ tree --filelimit=30
.
├── AppStream
├── Packages [4179 entries exceeds filelimit, not opening dir]
└── repodata
├── 22d8e8b515c4203ddbb2985e94c402111185584a8c5352aa5ff3a8af2d39dbc8-filelists.xml.gz
├── 4ee821bf89bc97cd7eb5c0e7dadbfc3a0117490c7efc47c01d0054500f76c8d0-other.xml.gz
├── 5f2aab7628e850f1d10a6955eb012150174e6061cc8056d1a936ad1c5ec3b28e-comps-AppStream.x86_64.xml
├── 6e1f7f087bfa38efa22c9d600685e27ee065938658fc084eedcda0222f4de3c9-comps-AppStream.x86_64.xml.gz
├── e16129a4b12ea6eea31f583760b2753afa1c30114d310c26d86f6e4d12c462aa-primary.xml.gz
└── repomd.xml
├── BaseOS
├── Packages [930 entries exceeds filelimit, not opening dir]
└── repodata
├── 15ff9c387a281faf1daa72439ee07427a8226417a05d4393cbca598bfa85b4d3-primary.xml.gz
├── 2ba8bad25a989827af7a684ee74da6f1581b10c95daae02063e90d214db53feb-filelists.xml.gz
├── 2ed22ffe47c639725b5c9c2718c77b319765ae1866ea8aa1bc4749b8f5baeae2-comps-BaseOS.x86_64.xml
├── 30e9a25f6e95cc50bb2835916bf5c3f49cabd1799a2130c77e5c811c4a7e0dab-other.xml.gz
├── d94fb53d527fd80facf145f1262663bb8deedcd98e84bac83f239e116e0afa53-comps-BaseOS.x86_64.xml.gz
└── repomd.xml
├── EFI
└── BOOT
├── BOOTX64.EFI
├── fonts
└── unicode.pf2
├── grub.cfg
├── grubx64.efi
└── mmx64.efi
├── EULA
├── LICENSE
├── boot
└── grub2
├── grub.cfg
└── i386-pc [303 entries exceeds filelimit, not opening dir]
├── extra_files.json
├── images
├── efiboot.img
├── eltorito.img
├── install.img
└── pxeboot
├── initrd.img
└── vmlinuz
└── media.repo
# 旧版本 CentOS8 的光盘内容
$ tree --filelimit=30 /mnt/iso2/
/mnt/iso2/
├── CentOS_BuildTag
├── EFI
├── BOOT
├── BOOTIA32.EFI
├── BOOTX64.EFI
├── TRANS.TBL
├── fonts
├── TRANS.TBL
└── unicode.pf2
├── grub.cfg
├── grubia32.efi
├── grubx64.efi
├── mmia32.efi
└── mmx64.efi
└── TRANS.TBL
├── EULA
├── GPL
├── LiveOS
├── TRANS.TBL
└── squashfs.img
├── Packages [448 entries exceeds filelimit, not opening dir]
├── RPM-GPG-KEY-CentOS-7
├── RPM-GPG-KEY-CentOS-Testing-7
├── TRANS.TBL
├── images
├── TRANS.TBL
├── efiboot.img
└── pxeboot
├── TRANS.TBL
├── initrd.img
└── vmlinuz
├── isolinux
├── TRANS.TBL
├── boot.cat
├── boot.msg
├── grub.conf
├── initrd.img
├── isolinux.bin
├── isolinux.cfg
├── memtest
├── splash.png
├── vesamenu.c32
└── vmlinuz
└── repodata
├── 136912ae46ca9ed27661ea6528fd544962d83095e3cdbc6149a37ddedf3a153c-primary.xml.gz
├── 57ae40f3495cc6e04d91b0eb504e740f901c3b48ec2179e3736256db08fa755c-other.xml.gz
├── 6da4a53c1231b84985efc60f9ccac32e6c33c0831d7f352d003fd4bfc39c373a-other.sqlite.bz2
├── 83b61f9495b5f728989499479e928e09851199a8846ea37ce008a3eb79ad84a0-c7-minimal-x86_64-comps.xml
├── 91cceb5edd8689502596ad298ec64bad45991dd758a8180bfe18a6d8a3281c28-filelists.sqlite.bz2
├── 95e7738c1060314a439e6970aeebe54ee6a729cc66351fc02edc057a524b0ea3-filelists.xml.gz
├── TRANS.TBL
├── a4e2b46586aa556c3b6f814dad5b16db5a669984d66b68e873586cd7c7253301-c7-x86_64-comps.xml.gz
├── b6404d2de68763bab0d9fa3f8e1d6f5bc057b2c4a1919a89cc083d5dbc6efb19-primary.sqlite.bz2
├── cca56f3cffa18f1e52302dbfcf2f0250a94c8a37acd8347ed6317cb52c8369dc-c7-x86_64-comps.xml
├── d4de4d1e2d2597c177bb095da8f1ad794d69f76e8ac7ab1ba6340fdd0969e936-c7-minimal-x86_64-comps.xml.gz
├── repomd.xml
└── repomd.xml.asc

这里需要先区分一下新旧版本的区别。新版本是通过 grub2 制作的可引导光盘,旧版本是通过 isolinux 制作的可引导光盘。可以看到新版本 ISO 文件中存在以下目录:

  • AppStream:存储了该发行版支持的应用软件包
  • BaseOS:存储了该发行版比较基础的一些软件包
  • EULA:最终用户许可协议文件,该文件中 CentOS Stream 表示不提供任何形式的保证或担保
  • LICENSE:许可证文件
  • extra_files.json:额外文件的哈希,方便做 ISO 完整性校验
  • EFI:UEFI 启动固件场景下的引导配置
  • boot:传统 BIOS 启动固件场景下的引导配置
  • images:所有的镜像文件
  • media.repo:内置的软件源配置

首先在安装 CentOS 时,在引导页面可以按 Tab 键查看引导选项:

操作系统引导选项

从以上能得知:

  • vmlinuz:表示加载的内核名称和位置
  • initrd=initrd.img:表示 initrd 镜像名称和位置
  • inst.stage2=hd=CentOS\x207\x20x86_64:内核选项,用于 dracut 中的 anaconda 模块识别磁盘标签
  • quiet:内核选项,表示静默启动,避免打印过多信息

紧接着,正式启动临时操作系统到安装界面后,可以通过 Ctrl + Alt + F2 进入 shell 环境,查看它的文件系统以及进程情况:

# 这里我只列出了关键的块设备信息
[anaconda rootlocalhost /]# lsblk
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT
sr0 11:0 1 973M 0 rom /run/install/repo
loop0 7:0 0 497.5M 1 loop
loop1 7:1 0 2G 1 loop
|-live-rw 253:0 0 2G 0 dm /
`-live-base 253:1 0 2G 1 dm /
loop2 7:2 0 512M 0 loop
`-live-rw 253:0 0 2G 0 dm /
[anaconda rootlocalhost /]# df -h
Filesystem Size Used Avail Use% Mounted on
devtmpfs 940M 0 940M - /dev
tmpfs 967M 4.0K 967M 1% /dev/shm
tmpfs 967M 17M 951M 2% /run
tmpfs 967M 0 967M 0% /sys/fs/cgroup
/dev/sr0 973M 973M 0 100% /run/install/repo
/dev/mapper/live-rw 2.0G 1.6G 427M 79% /
tmpfs 967M 5.1M 962M 1% /tmp
[anaconda root@localhost ]# ps -ef | grep anaconda
UID PID PPID C STIME TTY TIME CMD
root 1438 0 10:07 ? 00:00:00 /usr/bin/tmux -u -f /usr/share/anaconda/tmux.conf start
root 1440 1438 1 10:07 pts/0 00:00:17 /usr/bin/python /sbin/anaconda
root 1438 1438 0 10:07 pts/2 00:00:00 tail -F /tmp/anaconda.log
root 1454 0 10:07 tty1 00:00:00 /usr/bin/tmux -u attach -t anaconda
root 2881 0 10:29 tty2 00:00:00 grep anaconda

从以上可以看到这是一个完全运行于内存(tmpfs)和光盘(live-*)上的 live 操作系统,可以推导出光盘中存在一个文件形式的块设备用于支持 live 系统的读写。

至此,可以通过光盘的引导项和镜像中的内容推导得知,光盘安装的流程为:

graph TD A[插入光盘并开机] --> B[BIOS/UEFI 引导至光盘] B --> C{找到isolinux或grub引导} C -- 是 --> D[加载光盘引导程序] D --> E[启动临时的 live 操作系统] E --> F[在 anaconda 安装程序中配置语言、磁盘格式化和分区、安装软件包等] F --> G[执行安装后脚本] G --> I[重启] I --> J[从磁盘引导和加载新安装的操作系统] C -- 否 --> M[错误: 没有找到引导程序代码]

其中比较核心的工作有两个方面:

  • 通过 dracut 制作一个可识别内核参数,并正确加载临时 live 操作系统的 initramfs;
  • 携带类似 anaconda 安装程序的 live 操作系统镜像;

注:Redhat 系的镜像构建高度依赖 RPM 软件包管理机制,并且有 Lorax 工具支持。

什么是 initrd,如何制作 initrd#

已知,一个常见的 grub 引导菜单配置是这样的:

menuentry 'CentOS 7 (Core)' --class gnu-linux --class os {
linux /vmlinuz-2.6.96-1.6.el7.x86_64 root=UUID=XXXXXX ro crashkernel=auto rd.lvm.lv=centos/root rd.lvm.lv=centos/swap rhgb quiet
initrd /initramfs-2.6.96-1.6.el7.x86_64.img
}

在一个启动引导项中,可以看到 linux 指令指定了内核的位置以及一些启动选项,initrd 指令指定了 initramfs 镜像的位置。

根据内核文档 ramfs-rootfs-initramfs描述得知,initramfs 是在 Linux 内核启动过程中,当系统还未能找到或挂载真正的根文件系统时使用的临时根文件系统。它包含了启动过程中所需的程序和库,例如设备驱动程序、系统工具和脚本,以便进行硬件检测和文件系统挂载。它将启动过程中的任务转移到了用户空间,有助于实现更复杂的启动逻辑,促进了启动过程的模块化。

如果没有这个 initramfs,那么众多不同设备的驱动程序都需要直接编译到内核中,并且寻找和安装真正的根设备是一个复杂的工作——因为它可能需要跨越多个设备,可以存在于网络或可移动设备,并且它们还能被压缩、加密,这会造成内核的臃肿。当然也存在一个反例:在嵌入式 Linux 系统中,因为设备是固定的,所以会直接将驱动编译进内核,此时就不需要通过 initramfs 启动系统。

在制作 initramfs 上,我们一般使用 dracut 工具,这也是大多数发行版选择使用的工具。它主要依赖宿主机上的两个目录:

  • /usr/lib/dracut/modules.d/:为生成的 initramfs 提供模块支持,通过挂钩机制,使得各个阶段的工作可以进行定制
  • /usr/lib/modules/<kver>:为生成的 initramfs 提供特定版本内核的模块支持,实现内核不变的情况下按需动态加载必要的内核模块,方便识别新的硬件或提供新的功能

由于我们制作 initramfs 的目的是希望从光盘引导加载内核后,不依赖本地磁盘,启动一个 Live 系统,运行类似 anaconda 的 Linux 安装程序,以方便对主机上的磁盘进行格式化和分区,以及将用户真正的文件系统拷贝进去。Live 系统即是一种自包含操作系统环境,它允许用户从可移动介质(如 CD、DVD、USB 驱动器)或通过网络(如 PXE 启动)直接运行而无需依赖计算机本地硬盘的操作系统。可以通过 dracut 模块实现这个能力,生成 initramfs 镜像的命令可以参考 fedoraproject LiveOS image 文档

# 这里填写自己编译内核的版本,dracut 会从 /lib/modules/<kver> 中复制内核模块到 initramfs 中去
kver=6.9.3
# --nomdadmconf:不生成 mdadm.conf 文件。mdadm 是 Linux 的 RAID 管理工具,mdadm.conf 文件包含了 RAID 设备的配置信息
# --no-lvmconf:不生成 LVM(逻辑卷管理)相关的配置文件,如 lvm.conf 和 lvmcache
# --xz:使用 xz 压缩算法来压缩 initramfs 镜像
# --add='livenet dmsquash-live dmsquash-live-ntfs convertfs pollcdrom qemu qemu-net':添加一系列模块到 initramfs 中。这些模块包括:
# livenet:提供网络支持。
# dmsquash-live 和 dmsquash-live-ntfs:提供对 DMSquash 压缩的 Live 系统的支持
# convertfs:用于转换文件系统的类型
# pollcdrom:轮询 CD-ROM 驱动器的状态
# qemu 和 qemu-net:提供对 QEMU 虚拟化的支持,特别是网络方面
# --no-hostonly:不使用 Host-Only 模式。Host-Only 模式会根据当前主机的硬件配置来定制 initramfs,以减少其大小和依赖性
# --no-early-microcode:不包含早期微码更新。微码是处理器的低级固件,有时需要更新以修复安全问题或提高性能
# --force:强制重新生成 initramfs,即使检测到的配置没有变化。这可以确保使用最新的配置和脚本重新构建 initramfs
# --install "lsblk vim": install 选项支持在 initramfs 中新添加一些可执行文件方便调试,制作镜像时它会将它们拷贝进去并自动解决依赖
dracut --nomdadmconf --nolvmconf --xz --add 'livenet dmsquash-live dmsquash-live-ntfs convertfs pollcdrom qemu qemu-net' --no-hostonly --no-early-microcode --kver ${kver} --install "lsblk vim" --force initrd.img
dracut: Executing: /usr/bin/dracut --nomdadmconf --nolvmconf --xz --add "livenet dmsquash-live convertfs pollcdrom qemu qemu-net" --no-hostonly --no-early-microcode --kver 6.9.3 --install "lsblk vim" --force initrd.img
dracut: dracut module 'bootchart' will not be installed, because command '/sbin/bootchartd' could not be found!
dracut: dracut module 'plymouth' will not be installed, because command 'plymouth-set-default-theme' could not be found!
dracut: dracut module 'dmraid' will not be installed, because command 'dmraid' could not be found!
dracut: dracut module 'multipath' will not be installed, because command 'multipath' could not be found!
dracut: dracut module 'cifs' will not be installed, because command 'mount.cifs' could not be found!
dracut: dracut module 'fcoe' will not be installed, because command 'dcbtool' could not be found!
dracut: dracut module 'fcoe' will not be installed, because command 'fipvlan' could not be found!
dracut: dracut module 'fcoe' will not be installed, because command 'lldpad' could not be found!
dracut: dracut module 'fcoe' will not be installed, because command 'fcoemon' could not be found!
dracut: dracut module 'fcoe' will not be installed, because command 'fcoeadm' could not be found!
dracut: dracut module 'fcoe-uefi' will not be installed, because command 'dcbtool' could not be found!
dracut: dracut module 'fcoe-uefi' will not be installed, because command 'fipvlan' could not be found!
dracut: dracut module 'fcoe-uefi' will not be installed, because command 'lldpad' could not be found!
dracut: dracut module 'biosdevname' will not be installed, because command 'biosdevname' could not be found!
dracut: *** Including module: bash ***
dracut: *** Including module: dash ***
dracut: *** Including module: systemd ***
dracut: *** Including module: systemd-initrd ***
dracut: *** Including module: modsign ***
dracut: *** Including module: console-setup ***
dracut: *** Including module: convertfs ***
dracut: *** Including module: network ***
dracut: *** Including module: ifcfg ***
dracut: *** Including module: url-lib ***
/usr/lib/dracut/modules.d/45url-lib/module-setup.sh: line 33: warning: command substitution: ignored null byte in input
/usr/lib/dracut/modules.d/45url-lib/module-setup.sh: line 33: warning: command substitution: ignored null byte in input
dracut: *** Including module: bcache ***
dracut: *** Including module: btrfs ***
dracut: *** Including module: crypt ***
dracut: *** Including module: dm ***
dracut: Skipping udev rule: 10-dm.rules
dracut: Skipping udev rule: 13-dm-disk.rules
dracut: Skipping udev rule: 64-device-mapper.rules
dracut: *** Including module: dmsquash-live ***
dracut: *** Including module: kernel-modules ***
dracut: *** Including module: kernel-network-modules ***
dracut: *** Including module: livenet ***
dracut: *** Including module: lvm ***
dracut: Skipping udev rule: 11-dm-lvm.rules
dracut: Skipping udev rule: 69-dm-lvm-metad.rules
dracut: Skipping udev rule: 64-device-mapper.rules
dracut: *** Including module: mdraid ***
dracut: Skipping udev rule: 64-md-raid.rules
dracut: *** Including module: overlay-root ***
dracut: *** Including module: qemu ***
dracut: *** Including module: qemu-net ***
dracut: *** Including module: iscsi ***
dracut: *** Including module: lunmask ***
dracut: *** Including module: nbd ***
dracut: *** Including module: nfs ***
dracut: *** Including module: resume ***
dracut: *** Including module: rootfs-block ***
dracut: *** Including module: terminfo ***
dracut: *** Including module: udev-rules ***
dracut: Skipping udev rule: 40-redhat.rules
dracut: Skipping udev rule: 91-permissions.rules
dracut: Skipping udev rule: 80-drivers-modprobe.rules
dracut: *** Including module: dracut-systemd ***
dracut: *** Including module: pollcdrom ***
dracut: *** Including module: usrmount ***
dracut: *** Including module: base ***
dracut: *** Including module: fs-lib ***
dracut: *** Including module: img-lib ***
dracut: *** Including module: shutdown ***
dracut: *** Including modules done ***
dracut: *** Installing kernel module dependencies ***
dracut: *** Installing kernel module dependencies done ***
dracut: *** Resolving executable dependencies ***
dracut: *** Resolving executable dependencies done***
dracut: *** Stripping files ***
dracut: *** Stripping files done ***
dracut: *** Store current command line parameters ***
dracut: *** Creating image file '/root/iso-lab/initrd.img' ***
dracut: *** Creating initramfs image file '/root/iso-lab/initrd.img' done ***

注:如果遇到找不到模块问题可以使用 apt-get/yum install dracut-*,然后通过 dracut --list 查看已有的模块;如果只是制作从 ISO 中加载的 live 系统,只需要 dmsquash-live 模块即可。

除了 dmsquash-live 模块以外,还有 debian 系操作系统使用的 casper 模块也能实现 LiveCD 的启动,它们本质都是一样的——从不同介质中获取 squashfs 文件,解压和挂载到 /sysroot 并进行最终的 switch_root

从以上能清晰地看到 dracut 制作 initramfs 的整体流程:

  1. 加载模块
  2. 安装内核模块
  3. 解决可执行文件依赖
  4. 精简文件移除不必要的符号和调试信息
  5. 创建镜像文件

通过 dracut 制作的镜像文件一般会将模块中安装的脚本放到 /usr/lib/dracut/hooks 目录,通过 lsinitramfs 命令可以进行查看。可以先查看下 CentOS 光盘中的 initrd.img 是什么样的结构:

$ llsinitramfs /mnt/iso/images/pxeboot/initrd.img | grep hook
usr/lib/dracut/hooks
usr/lib/python3.12/site-packages/requests/hooks.py
var/lib/dracut/hooks
var/lib/dracut/hooks/cleanup
var/lib/dracut/hooks/cleanup/80-multipathd-needshutdown.sh
var/lib/dracut/hooks/cleanup/90-cleanup-fcoe.sh
var/lib/dracut/hooks/cleanup/90-cleanup-iscsi.sh
var/lib/dracut/hooks/cleanup/98-anaconda-nfsrepo-cleanup.sh
var/lib/dracut/hooks/cleanup/99-mdraid-needshutdown.sh
var/lib/dracut/hooks/cleanup/99-nfsroot-cleanup.sh
var/lib/dracut/hooks/cmdline
var/lib/dracut/hooks/cmdline/00-parse-root.sh
var/lib/dracut/hooks/cmdline/20-parse-uefifcoe.sh
var/lib/dracut/hooks/cmdline/25-parse-anaconda-options.sh
var/lib/dracut/hooks/cmdline/26-parse-anaconda-kickstart.sh
var/lib/dracut/hooks/cmdline/27-parse-anaconda-repo.sh
var/lib/dracut/hooks/cmdline/28-parse-anaconda-net.sh
var/lib/dracut/hooks/cmdline/29-parse-anaconda-dd.sh
var/lib/dracut/hooks/cmdline/29-parse-livenet.sh
var/lib/dracut/hooks/cmdline/30-parse-crypt.sh
var/lib/dracut/hooks/cmdline/30-parse-dmsquash-live.sh
var/lib/dracut/hooks/cmdline/30-parse-lunmask.sh
var/lib/dracut/hooks/cmdline/30-parse-lvm.sh
var/lib/dracut/hooks/cmdline/31-parse-iso-scan.sh
var/lib/dracut/hooks/cmdline/90-parse-iscsiroot.sh
var/lib/dracut/hooks/cmdline/90-parse-nfsroot.sh
var/lib/dracut/hooks/cmdline/91-dhcp-root.sh
var/lib/dracut/hooks/cmdline/92-parse-nvmf-boot-connections.sh
var/lib/dracut/hooks/cmdline/95-parse-virtfs.sh
var/lib/dracut/hooks/cmdline/95-parse-virtiofs.sh
var/lib/dracut/hooks/cmdline/99-nm-config.sh
var/lib/dracut/hooks/cmdline/99-parse-fcoe.sh
var/lib/dracut/hooks/emergency
var/lib/dracut/hooks/emergency/50-plymouth-emergency.sh
var/lib/dracut/hooks/initqueue
var/lib/dracut/hooks/initqueue/finished
var/lib/dracut/hooks/initqueue/online
var/lib/dracut/hooks/initqueue/online/00-anaconda-ifcfg.sh
var/lib/dracut/hooks/initqueue/online/11-fetch-kickstart-net.sh
var/lib/dracut/hooks/initqueue/online/20-fetch-driver-net.sh
var/lib/dracut/hooks/initqueue/online/80-anaconda-netroot.sh
var/lib/dracut/hooks/initqueue/online/95-fetch-liveupdate.sh
var/lib/dracut/hooks/initqueue/settled
var/lib/dracut/hooks/initqueue/settled/00-anaconda-ks-sendheaders.sh
var/lib/dracut/hooks/initqueue/settled/99-nm-run.sh
var/lib/dracut/hooks/initqueue/settled/99-pollcdrom.sh
var/lib/dracut/hooks/initqueue/timeout
var/lib/dracut/hooks/initqueue/timeout/50-anaconda-error-reporting.sh
var/lib/dracut/hooks/initqueue/timeout/99-rootfallback.sh
var/lib/dracut/hooks/mount
var/lib/dracut/hooks/mount/99-mount-virtfs.sh
var/lib/dracut/hooks/netroot
var/lib/dracut/hooks/pre-mount
var/lib/dracut/hooks/pre-mount/01-prepare-overlayfs.sh
var/lib/dracut/hooks/pre-mount/10-mdraid-waitclean.sh
var/lib/dracut/hooks/pre-mount/99-mount-virtiofs.sh
var/lib/dracut/hooks/pre-pivot
var/lib/dracut/hooks/pre-pivot/00-fips-boot.sh
var/lib/dracut/hooks/pre-pivot/01-fips-noboot.sh
var/lib/dracut/hooks/mount/01-mount-overlayfs.sh
var/lib/dracut/hooks/pre-pivot/10-mount-overlayfs.sh
var/lib/dracut/hooks/pre-pivot/20-apply-live-updates.sh
var/lib/dracut/hooks/pre-pivot/50-anaconda-copy-cmdline.sh
var/lib/dracut/hooks/pre-pivot/50-anaconda-copy-ks.sh
var/lib/dracut/hooks/pre-pivot/50-anaconda-copy-s390ccwconf.sh
var/lib/dracut/hooks/pre-pivot/50-anaconda-depmod.sh
var/lib/dracut/hooks/pre-pivot/85-write-ifcfg.sh
var/lib/dracut/hooks/pre-pivot/90-anaconda-copy-dhclient.sh
var/lib/dracut/hooks/pre-pivot/91-anaconda-copy-prefixdevname.sh
var/lib/dracut/hooks/pre-pivot/95-anaconda-set-kernel-hung-timeout.sh
var/lib/dracut/hooks/pre-pivot/99-do-convertfs.sh
var/lib/dracut/hooks/pre-pivot/99-save-initramfs.sh
var/lib/dracut/hooks/pre-shutdown
var/lib/dracut/hooks/pre-shutdown/30-mdmon-pre-shutdown.sh
var/lib/dracut/hooks/pre-shutdown/50-anaconda-pre-shutdown.sh
var/lib/dracut/hooks/pre-trigger
var/lib/dracut/hooks/pre-trigger/01-load-modsign-keys.sh
var/lib/dracut/hooks/pre-trigger/03-lldpad.sh
var/lib/dracut/hooks/pre-trigger/30-parse-md.sh
var/lib/dracut/hooks/pre-trigger/50-kickstart-genrules.sh
var/lib/dracut/hooks/pre-trigger/50-repo-genrules.sh
var/lib/dracut/hooks/pre-trigger/50-updates-genrules.sh
var/lib/dracut/hooks/pre-trigger/55-driver-updates-genrules.sh
var/lib/dracut/hooks/pre-udev
var/lib/dracut/hooks/pre-udev/01-fips-load-crypto.sh
var/lib/dracut/hooks/pre-udev/30-anaconda-modprobe.sh
var/lib/dracut/hooks/pre-udev/30-dm-pre-udev.sh
var/lib/dracut/hooks/pre-udev/30-dmsquash-live-genrules.sh
var/lib/dracut/hooks/pre-udev/30-dmsquash-liveiso-genrules.sh
var/lib/dracut/hooks/pre-udev/30-mdmon-pre-udev.sh
var/lib/dracut/hooks/pre-udev/50-ifname-genrules.sh
var/lib/dracut/hooks/pre-udev/99-nfs-start-rpc.sh
var/lib/dracut/hooks/shutdown
var/lib/dracut/hooks/shutdown-emergency
var/lib/dracut/hooks/shutdown/20-multipath-shutdown.sh
var/lib/dracut/hooks/shutdown/25-dm-shutdown.sh
var/lib/dracut/hooks/shutdown/30-md-shutdown.sh
var/lib/dracut/hooks/shutdown/40-stop-fcoe.sh

从上面可以看到,CentOS 安装光盘为了启动临时操作系统做了很多客制化的工作,在 dracut initramfs 启动的不同阶段注入了不少自己的逻辑。可以从文档中了解 dracut initramfs 的启动流程:

systemd-journal.socket
|
v
dracut-cmdline.service
|
v
dracut-pre-udev.service
|
v
systemd-udevd.service
|
v
local-fs-pre.target dracut-pre-trigger.service
| |
v v
(various mounts) (various swap systemd-udev-trigger.service
| devices...) | (various low-level (various low-level
| | | services: seed, API VFS mounts:
v v v tmpfiles, random mqueue, configfs,
local-fs.target swap.target dracut-initqueue.service sysctl, ...) debugfs, ...)
| | | | |
\_______________|____________________ | ___________________|____________________/
\|/
v
sysinit.target
|
_________________/|\___________________
/ | \
| | |
v | v
(various | rescue.service
sockets...) | |
| | v
v | rescue.target
sockets.target |
| |
\_________________ | emergency.service
\| |
v v
basic.target emergency.target
|
______________________/|
/ |
| v
| dracut-pre-mount.service
| |
| v
| sysroot.mount
| |
| v
| initrd-root-fs.target
(custom initrd services) |
| v
| dracut-mount.service
| |
| v
| initrd-parse-etc.service
| |
| v
| (sysroot-usr.mount and
| various mounts marked
| with fstab option
| x-initrd.mount)
| |
| v
| initrd-fs.target
\______________________ |
\|
v
initrd.target
|
v
dracut-pre-pivot.service
|
v
initrd-cleanup.service
isolates to
initrd-switch-root.target
|
v
______________________/|
/ |
| initrd-udevadm-cleanup-db.service
| |
(custom initrd services) |
| |
\______________________ |
\|
v
initrd-switch-root.target
|
v
initrd-switch-root.service
|
v
switch-root

上面所有的 dracut-*.service 都是由 dracut 模块实现的(包括最后的 initrd-cleanup.service),都支持进行客制化,相当灵活。特别是 cmdline hooks 支持解析内核命令行,在用户层面实现自己的逻辑。anaconda 安装程序就支持相当多的启动选项

注:如果想查看具体内容,可以通过 xz -d -c initrd.img | cpio -id 命令对其进行解压。如果是 gzip 压缩,就将 xz -d 改为 gunzip,这会把 initrd.img 解压到当前目录,所以最好在新创建的目录中执行这个命令。

如果需要在启动流程中为安装程序做其它准备,我们也可以参考 dracut 文档的模块编写章节来编写自己需要的逻辑。这里就不做过多介绍了。

制作安装光盘#

到此为止,我们已经拥有了下面三种制品:

  • 自己编译的 Linux 内核以及模块
  • 对应版本的支持 live 启动的 initramfs
  • 用户根文件系统(Live 和最终挂载的用户根文件系统都可以基于 debootstrap 生成的基础系统进行定制)

我们已经具备制作安装光盘的所有条件了,现在可以开始制作。

首先是根据 dracut 文档的 booting_live_images 章节制作符合格式要求的 LiveOS Image:

/LiveOS
# squashfs.img | SquashFS from LiveCD .iso
# !(mount)
# |- rootfs.img | Filesystem image to mount read-only
# !(mount)
# /bin | Live filesystem
# /boot |
# /dev |
# ... |
# 创建 squash-live/LiveOS/rootfs 目录,LiveOS Image 要求 squashfs.img 解压后的顶级目录是 LiveOS,并且里边有个 rootfs.img
# 子目录 rootfs 用于存放用户根文件系统
mkdir -p squash-live/LiveOS/rootfs
# 进入 LiveOS 目录工作
pushd squash-live/LiveOS
# Live 系统不需要在用户根文件系统中额外安装内核,构建用户根文件系统则需要新增 linux-image-generic 包,它包含用户操作系统需要的内核和 initramfs
PKGS=whiptail,ca-certificates,tzdata
debootstrap --include=$PKGS --components=main,contrib,non-free --arch=amd64 bionic rootfs/ http://mirrors.ustc.edu.cn/ubuntu/
# 制作一个 2G 大小的 image
dd if=/dev/zero of=rootfs.img bs=$((2048 * 1024)) count=1024 status=progress
# -j 表示开启 journaling 日志功能
# -L 表示为它设置一个 LABEL
mkfs.ext4 -j -L MYLIVE rootfs.img
# 创建一个目录,作为 rootfs.img 的挂载点,将之前准备的用户根文件系统拷贝进去
mkdir mount-rootfs
mount -o loop rootfs.img mount-rootfs/
mv rootfs/* mount-rootfs/
# 我们还可以 chroot mount-rootfs 进入这个目录,做一些自定义的操作
...
# 解除挂载和清理
umount mount-rootfs/
rm -rf rootfs/ mount-rootfs/
# 回到初始目录
popd
# 将 squashfs-live 目录制作成 squashfs.img LiveOS Image
mksquashfs squashfs-live squashfs.img

然后,再制作光盘的 grub2 引导:

#
mkdir boot/grub2
pushd boot/grub2/
# 将 grub i386-pc 架构的模块文件复制到当前目录
cp -r /usr/lib/grub/i386-pc .
# 制作 grub2 镜像
# -o i386-pc/core.img,表示镜像输出位置
# --format i386-pc,表示引导镜像为 x86 架构
# biosdisk ext2 fat iso9660....,表示支持的模块名称
grub-mkimage -o i386-pc/core.img --format i386-pc biosdisk ext2 fat iso9660 halt reboot normal minicmd ntfs chain configfile ntldr search search_fs_file search_fs_uuid search_label usb usb_keyboard extcmd echo linux vga gfxmenu gfxterm vbe -p /boot/grub2/
# 将生成的 core.img 和 cdboot.img 合并为 g2ldr,cdboot.img 用于第一阶段,cdboot.img 用于第二阶段,合并后一个文件即可包含完整的引导能力
cat i386-pc/cdboot.img i386-pc/core.img > i386-pc/g2ldr
# 编写 grub 配置文件,这包括引导菜单等
cat > grub.cfg <<EOF
set default="1"
function load_video {
insmod all_video
}
load_video
set gfxpayload=keep
insmod gzio
insmod part_gpt
insmod ext2
insmod chain
set timeout=60
### END /etc/grub.d/00_header ###
### BEGIN /etc/grub.d/10_linux ###
menuentry 'Install MYOS' --class fedora --class gnu-linux --class gnu --class os {
# 内核启动选项表示从 MYOS 卷组加载一个 live 镜像
linux /images/pxeboot/vmlinuz root=live:LABEL=MYOS rd.live.image
initrd /images/pxeboot/initrd.img
}
EOF
# 回到工作目录
popd

最后将这些文件组成完整的光盘目录结构:

# 创建光盘的基本目录结构
# image 目录用于存放镜像文件
# LiveOS 目录用于存放 squashfs.img,该目录名称是 rd.live.dir 的默认值
mkdir -p myiso/{LiveOS,image/pxeboot}
# 将 squashfs.img 文件移动到 LiveOS 目录
mv squashfs.img myiso/LiveOS
# 将刚刚制作的 boot 移动到顶级目录
mv boot myiso
# 将内核和 initramfs 移动到 image/pxeboot 目录
mv initrd.img myiso/image/pxeboot/
mv bzImage myiso/image/pxeboot/vmlinuz
# -V 表示为光盘增加一个 MYOS 的 LABEL
# 如果需要增加 uefi 支持,需要新增选项 -eltorito-alt-boot -e images/efiboot.img,efiboot.img 也可以通过 grub-mkimage 制作
genisoimage -joliet-long -V MYOS -o myos.iso -b boot/grub2/i386-pc/g2ldr -no-emul-boot -boot-load-size 4 -boot-info-table -R -J -v -cache-inodes -T -no-emul-boot myiso/

到此为止,支持 live os 的安装光盘就制作完成了。经我个人测试,通过 Hyper-V 或 VMware 都能正确进行加载和启动,进入系统后能正常使用和检测到机器上的磁盘。

3.4 之后的事#

我们经过一系列折腾,了解了 Linux 的编译、用户根文件系统的构造、安装光盘以及 live 系统的制作,距离标题的**「构建完全自主的 Linux 发行版」**似乎还有一定的距离,但已经没有技术上的难点和迷雾了。大概需要的工作包括:

  • 软件编译以及包管理:在前文我们直接借用了 debootstrap 工具,使用了 debian/ubuntu 发行版做好的 deb 包。如果要完全自主构建,这一步也需要自己进行编译和管理。这一点既可以参考无软件包管理器的 LFS,也可以参考基于 RPM 的 opencloudos-stream
  • installer 程序的编写:需要在光盘启动的 live 系统中运行一个 Linux installer,它可以是图形化的也可以是字符界面的,它可以是交互式的也可以是自动化的,但必须的是能对主机上的硬件进行检测以加载正确的驱动,以便于后面对主机上的磁盘进行格式化、分区以及安装 grub 引导和真正的用户根文件系统,并进行系统配置(时区、语言、键盘、网络、用户等)等工作;
  • 为了发行版的安全和稳定性、兼容性,需要审计该发行版提供的内核以及软件的代码,时刻关注它们的安全信息,以便于持续进行修复和更新;还需要进行交叉编译使该发行版支持不同的硬件架构,保持一致的使用体验。

从这些过程可以了解到,制作和维护一个完整的 Linux 发行版所需要的工作量是何其的庞大。好在这中间的各个过程都有工程师对其进行抽象和封装,比如前文使用的 debootstrap 以及 dracut 中的各种各样的模块。对于制作 Redhat 系的安装光盘,我们甚至可以直接使用 lorax 工具,它封装了根文件系统、initramfs、包含 anaconda 安装程序的 live os 的制作,能直接一步到位生成 ISO,我们只需要提供 RPM 软件源即可。但只有了解了这一切,才有资格和能力选择性地参与和定制自己需要的部分。

(完)

参考#

从零开始构建 Linux 内核文档 - ramfs-rootfs-initramfs initrd 和 initramfs 的区别 Linux 启动盘指南 如何从头开始创建自定义 Ubuntu Live Debian live manual dracut 文档 the-history-of-red-hat Linux 内核中根文件系统的初始化及 init 程序的运行


参考#

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

构建完全自主的 Linux 发行版
https://blog.souloss.com/posts/interview/make-os-release/
作者
Souloss
发布于
2020-09-12
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时