大家好,这里是物联网心球。
本文的主题是Linux根文件系统,在进入正文之前,我们先来思考一个问题:什么是Linux根文件系统?
根文件系统被认为是Linux内核启动后挂载的第一个文件系统,是Linux文件树的根,是所有绝对路径的起点。笔者认为,这种表达并不够精准。接下来,我们从内核视角出发,深入了解Linux根文件系统。
注意:本文基于内核6.10版本讲解。
1.根文件系统从何而来?
Linux用户接触到的根文件系统只是挂载在根目录("/")下的一个真实的文件系统 ,我们习惯把它称为真实根文件系统。内核初始化时,会挂载一个rootfs文件系统(伪文件系统,只存在内存中),rootfs是Linux内核启动后第一个挂载的文件系统,所以它才是Linux的根文件系统。
从rootfs到真实根文件系统需要经历三个环节:
- 挂载rootfs伪文件系统。
- 解析initramfs至rootfs。
- 挂载真实根文件系统(如ext4)。
图1 真实根文件系统挂载流程
真实根文件系统挂载流程如图1所示。
首先,我们来看rootfs伪文件系统的挂载流程,rootfs文件系统定义如下:
struct file_system_type rootfs_fs_type = {
.name = "rootfs",
.init_fs_context = rootfs_init_fs_context,
.kill_sb = kill_litter_super,
};
我们展开rootfs_init_fs_context函数:
static int rootfs_init_fs_context(struct fs_context *fc)
{
if (IS_ENABLED(CONFIG_TMPFS) && is_tmpfs)
return shmem_init_fs_context(fc);
return ramfs_init_fs_context(fc);
}
发现rootfs实际上是一个**tmpfs** 或**ramfs文件系统** 实例。ramfs 和tmpfs都是 Linux系统中基于内存的文件系统。它们将数据直接存储在RAM(物理内存)中,因此读写速度极快,但数据在系统重启后会丢失。
内核启动过程会调用start_kernel函数,该函数会执行一些列初始化工作,rootfs的初始化路径为:start_kernel()->vfs_caches_init()->mnt_init()->init_mount_tree()。init_mount_tree函数将会创建rootfs挂载实例和rootfs根目录,并将这些信息记录在内核。文件系统挂载过程这里不过多展开,对文件系统挂载感兴趣的同学可以阅读这篇文章:从ext4文件系统到Linux文件树。
rootfs解决的是Linux文件树从0到1的问题,rootfs的根目录就是Linux文件树的根,后续的真实根文件系统需要挂载在rootfs的根目录。
有了根文件系统后,接下来,内核会解析initramfs至rootfs。initramfs是Linux启动过程中使用的临时文件系统,在真实根文件系统挂载前提供必要的驱动和工具。initramfs经常被当做Linux文件系统,其实它并不是真正的文件系统,内核中并没有定义该类型的文件系统。initramfs本质是一个cpio归档文件,包含了挂载真实根文件系统需要用到的所有文件。内核会对initramfs进行解析,并将解析出来的文件一个个保存在rootfs文件系统中(注意此处并不是挂载,可以理解为文件拷贝)。initramfs解析对应的初始化路径为:start_kernel()->arch_call_rest_init()->rest_init()->kernel_init()->......->do_populate_rootfs()。
initramfs解析完毕后,rootfs根目录下会有一个init脚本,init脚本将会完成真实文件系统挂载,并将系统运行环境也切换至真实文件系统。对应的初始化路径为:start_kernel()->vfs_caches_init()->rest_init()->kernel_init()->run_init_process("/init")。run_init_process函数将会执行init脚本。
2.initramfs详解
initramfs并不是真正的文件系统,它是压缩(如lz4、gzip、zstd等压缩格式)了的cpio归档文件,cpio(Copy In and Out)是一种在Unix和Linux系统中广泛使用的归档工具,用于将多个文件和目录打包成一个单独的归档文件,同时保留文件的元数据(如权限、所有者、时间戳等)。
cpio支持多种归档格式:bin、odc、newc、crc、tar等。initramfs采用的是newc格式,如图2所示。
图2 initramfs格式
initramfs由一条条文件记录构成,每条文件记录格式为:
- 文件头(110字节)
- 文件名
- 0-3字节填充
- 文件数据
- 0-3字节填充
文件头包含文件的元数据,固定大小为110字节,其格式见表1。
表1 newc文件头
为了加深大家对newc格式的理解,我们创建一个最小initramfs并打包成newc格式,测试脚本如下:
bash
#!/bin/bash
#创建文件树
mkdir bin conf etc lib
touch etc/test.txt
#创建init脚本
echo "#!/bin/sh" > init
#打包为newc格式
find bin conf etc lib init -depth | cpio -o -H newc > initramfs.cpio
执行测试脚本后将生成一个initramfs.cpio归档文件,执行以下命令验证文件清单:
cpio -t < demo.cpio
文件清单如下:
# cpio -t < initramfs.cpio
bin
conf
etc/test.txt
etc
lib
init
执行hexdump -C initramfs.cpio查看归档文件记录,输出结果如下:
最终cpio中的文件记录将一条条被解析,并存储在rootfs文件系统中。
3./init脚本
/init脚本是initramfs的核心,它将完成以下关键任务:
- 挂载
/proc,/sys,/dev等虚拟文件系统,并创建必要的设备节点。 - 加载访问存储设备和文件系统所需的内核模块。
- 挂载真实根文件系统。
- 将系统根目录切换到真实根文件系统,并启动/sbin/init进程(1号进程)。
实际的/init脚本一般都比较复杂,为了便于讲解,我们只关注根文件系统挂载相关的内容,如图3所示。
图3 /init脚本工作原理
内核调用run_init_process("/init")函数将会执行init脚本,run_init_process函数主要任务是执行用户空间的第一个进程,从而完成从内核态到用户态的切换。
真实根文件系统(如ext4)未挂载之前,系统执行文件相关的操作都是在rootfs文件系统中进行。/init脚本首先会挂载伪文件系统(proc、sysfs、devtmpfs等)至rootfs。接着,内核会读取块设备,并挂载块设备中的真实根文件系统挂载至rootfs(挂载点由用户自行定义)。最后,将已挂载的伪文件系统移动至真实根文件系统,以及执行initramfs中的switch_root命令将运行环境切换至真实根文件系统。
switch_root 命令的核心功能包括:
- 将新根目录设置为系统的根文件系统。
- 执行新根文件系统中的init程序(通常是
/sbin/init)。 - 清理并释放initramfs占用的内存空间。
switch_root语法格式如下:
switch_root [-c /dev/console] NEW_ROOT NEW_INIT [ARGUMENTS_TO_INIT]
-
NEW_ROOT:已经挂载好的真正根文件系统挂载点,例如 /newroot。
-
NEW_INIT:真正根文件系统中的init程序路径,通常是/sbin/init(软链接,指向init或systemd)。
-
-c:可选,重定向新系统的控制台设备。
总结:
真实根文件系统启动流程分为三步:首先,挂载rootfs文件系统,创建根目录,解决文件系统从0到1的问题;接着,解析initramfs并将initramfs中的文件保存至rootfs文件系统;最后,执行initramfs中的init脚本,init脚本将完成真实根文件系统挂载。