本文的主题是Linux根文件系统,在进入正文之前,我们先来思考一个问题:什么是Linux根文件系统?根文件系统被认为是Linux内核启动后挂载的第一个文件系统,是Linux文件树的根,是所有绝对路径的起点。笔者认为,这种表达并不够精准。接下来,我们从内核视角出发,深入了解Linux根文件系统。Linux用户接触到的根文件系统只是挂载在根目录(“/”)下的一个真实的文件系统 ,我们习惯把它称为真实根文件系统。内核初始化时,会挂载一个rootfs文件系统(伪文件系统,只存在内存中),rootfs是Linux内核启动后第一个挂载的文件系统,所以它才是Linux的根文件系统。 首先,我们来看rootfs伪文件系统的挂载流程,rootfs文件系统定义如下:structfile_system_typerootfs_fs_type = { .name = "rootfs", .init_fs_context = rootfs_init_fs_context, .kill_sb = kill_litter_super,};
我们展开rootfs_init_fs_context函数:staticintrootfs_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脚本。
initramfs并不是真正的文件系统,它是压缩(如lz4、gzip、zstd等压缩格式)了的cpio归档文件,cpio(Copy In and Out)是一种在Unix和Linux系统中广泛使用的归档工具,用于将多个文件和目录打包成一个单独的归档文件,同时保留文件的元数据(如权限、所有者、时间戳等)。 cpio支持多种归档格式:bin、odc、newc、crc、tar等。initramfs采用的是newc格式,如图2所示。 initramfs由一条条文件记录构成,每条文件记录格式为: 文件头包含文件的元数据,固定大小为110字节,其格式见表1。 为了加深大家对newc格式的理解,我们创建一个最小initramfs并打包成newc格式,测试脚本如下:#!/bin/bash#创建文件树mkdir bin conf etc libtouch 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 < initramfs.cpiobinconfetc/test.txtetclibinit
执行hexdump -C initramfs.cpio查看归档文件记录,输出结果如下: 最终cpio中的文件记录将一条条被解析,并存储在rootfs文件系统中。 /init脚本是initramfs的核心,它将完成以下关键任务:- 挂载
/proc, /sys, /dev等虚拟文件系统,并创建必要的设备节点。 - 将系统根目录切换到真实根文件系统,并启动/sbin/init进程(1号进程)。
实际的/init脚本一般都比较复杂,为了便于讲解,我们只关注根文件系统挂载相关的内容,如图3所示。 内核调用run_init_process("/init")函数将会执行init脚本,run_init_process函数主要任务是执行用户空间的第一个进程,从而完成从内核态到用户态的切换。 真实根文件系统(如ext4)未挂载之前,系统执行文件相关的操作都是在rootfs文件系统中进行。/init脚本首先会挂载伪文件系统(proc、sysfs、devtmpfs等)至rootfs。接着,内核会读取块设备,并挂载块设备中的真实根文件系统挂载至rootfs(挂载点由用户自行定义)。最后,将已挂载的伪文件系统移动至真实根文件系统,以及执行initramfs中的switch_root命令将运行环境切换至真实根文件系统。switch_root 命令的核心功能包括:
- 将新根目录设置为系统的根文件系统。
- 执行新根文件系统中的init程序(通常是
/sbin/init)。
switch_root [-c /dev/console] NEW_ROOT NEW_INIT [ARGUMENTS_TO_INIT]
真实根文件系统启动流程分为三步:首先,挂载rootfs文件系统,创建根目录,解决文件系统从0到1的问题;接着,解析initramfs并将initramfs中的文件保存至rootfs文件系统;最后,执行initramfs中的init脚本,init脚本将完成真实根文件系统挂载。