[toc]
init/initramfs.c 初始RAM文件系统(Initial RAM Filesystem) 内核启动的早期用户空间
历史与背景
这项技术是为了解决什么特定问题而诞生的?
initramfs
(Initial RAM Filesystem)的诞生是为了解决一个经典的**“鸡生蛋,蛋生鸡”问题,即内核在启动过程中如何加载那些访问真正根文件系统所必需的驱动程序**。
具体来说,它解决了以下核心问题:
- 模块化的挑战:现代Linux内核是高度模块化的。为了保持内核镜像(vmlinuz)的小巧,大量驱动程序(如SATA/SCSI/NVMe控制器驱动、LVM/RAID逻辑卷驱动、网络驱动、加密模块等)都被编译成了独立的内核模块(
.ko
文件)。 - 根文件系统访问:内核启动后,其首要任务之一是挂载根文件系统(
/
)。但如果根文件系统位于一个SATA硬盘上,而SATA驱动又是一个内核模块,那么内核在没有加载这个模块之前,根本无法识别和访问这个硬盘,也就无法挂载根文件系统,启动过程陷入死锁。 - 早期用户空间的需求:在挂载真正的根文件系统之前,可能需要执行一些复杂的初始化任务,例如:
- 通过网络(NFS, iSCSI)挂载根文件系统,这需要加载网络驱动、配置IP地址。
- 解密一个加密的根分区,这需要加载加密模块并向用户索要密码。
- 启动逻辑卷管理器(LVM)或软件RAID来组装根文件系统所在的逻辑卷。
- 在嵌入式系统中,可能需要在挂载主存储之前,先加载一些特定的硬件驱动。
initramfs
通过提供一个临时的、完全在内存中运行的、初始的根文件系统来解决这个问题。这个临时的文件系统包含了所有必需的驱动模块、工具(如insmod
, lvm
, cryptsetup
)和脚本,使得内核可以在一个早期的用户空间环境中完成所有准备工作,然后再切换到真正的根文件系统。
它的发展经历了哪些重要的里程碑或版本迭代?
initramfs
是其前身initrd
(Initial RAM Disk)的现代化演进。
initrd
时代:initrd
是一个被内核加载到RAM中的块设备镜像。内核会像挂载一个微型磁盘一样挂载它(通常格式化为ext2等)。这种方式存在一些问题:- 固定大小:
initrd
镜像大小固定,不易扩展。 - 双重缓存:
initrd
本身作为一个块设备,其内容会被内核的页面缓存所缓存,造成内存的浪费。
- 固定大小:
initramfs
的引入:initramfs
代表了根本性的改变。它不再是一个块设备镜像,而是一个cpio归档文件(通常是.cpio.gz
)。- 集成到内核:在构建内核时,这个cpio归档可以被直接链接到内核镜像中。内核启动时,会直接在内存中解压这个归档,并将其内容填充到一个特殊的、基于RAM的文件系统实例——**
rootfs
**中。 tmpfs
/ramfs
后端:initramfs
本质上是一个tmpfs
或ramfs
实例。这意味着它的大小是动态的,并且它直接使用内存,避免了initrd
的双重缓存问题。
- 集成到内核:在构建内核时,这个cpio归档可以被直接链接到内核镜像中。内核启动时,会直接在内存中解压这个归档,并将其内容填充到一个特殊的、基于RAM的文件系统实例——**
init/initramfs.c
的角色:这个文件实现了内核解压和填充initramfs
的核心逻辑。函数populate_rootfs()
负责解析cpio归档,并在rootfs
中创建对应的文件和目录。- 从外部加载
initramfs
:除了内嵌到内核,initramfs
也可以作为一个独立的文件(如initramfs-linux.img
)由引导加载程序(如GRUB, systemd-boot)加载到内存中,并将其地址告知内核。
目前该技术的社区活跃度和主流应用情况如何?
initramfs
是所有现代通用Linux发行版(如Ubuntu, Fedora, Arch Linux, Debian)标准启动流程中不可或缺的一部分。
- 绝对核心:几乎所有的桌面、服务器和许多嵌入式Linux系统都使用
initramfs
。 - 自动化工具:用户通常不直接创建
initramfs
。像dracut
和mkinitcpio
这样的工具会自动扫描当前系统所需的驱动,并生成一个定制化的initramfs
镜像。 - 无盘系统:对于无盘工作站或网络启动(PXE)的服务器,
initramfs
是实现其启动的关键。 - 社区状态:
init/initramfs.c
中的内核代码非常稳定和成熟。社区的活跃度更多地体现在用户空间的生成工具(dracut
等)和引导加载程序的持续发展上。
核心原理与设计
它的核心工作原理是什么?
initramfs.c
的核心工作流程是在内核启动的极早期阶段,构建起第一个可用的根文件系统。
rootfs
的创建:内核在启动时会创建一个特殊的、基于内存的文件系统,名为rootfs
。这是所有挂载点的“祖先”。initramfs
源的定位:内核会查找initramfs
的来源。它会检查一个特殊的ELF段,看是否有cpio归档被链接进了内核镜像。如果没有,它会检查引导加载程序是否在启动参数中提供了一个外部initramfs
的内存地址。- 解压和填充 (
populate_rootfs
):这是init/initramfs.c
的主函数。它会:- 将定位到的cpio归档(可能是压缩的)作为一个数据流进行读取。
- 逐个解析cpio归档中的条目(header + data)。每个条目都描述了一个文件、目录或符号链接。
- 根据条目信息,在
rootfs
中执行相应的VFS操作:创建目录(vfs_mkdir
)、创建文件(vfs_create
)、写入文件内容(vfs_write
)、创建符号链接(vfs_symlink
)等。
- 切换到早期用户空间:当
populate_rootfs
完成后,rootfs
中就包含了initramfs
的所有内容。内核的启动流程继续,最终会执行rootfs
中的/init
程序。 init
脚本执行:这个/init
程序(通常是一个shell脚本)现在作为PID 1运行在一个临时的内存文件系统中。它负责执行所有必要的准备工作:- 挂载
/sys
,/proc
等伪文件系统。 - 使用
modprobe
或insmod
加载必需的内核模块。 - 扫描设备,启动LVM/RAID。
- 向用户索要加密密码。
- 挂载
- 切换真正的根 (
switch_root
):当所有准备工作完成,并且真正的根设备可用时,/init
脚本会挂载真正的根文件系统到一个临时目录(如/new_root
),然后执行switch_root
命令。switch_root
是一个特殊工具,它会删除initramfs
的所有内容,并将挂载点从/new_root
移动到/
,最后在新的根文件系统上执行真正的init
程序(如/sbin/init
或systemd
)。至此,启动的接力棒就从initramfs
交给了真正的系统。
它的主要优势体現在哪些方面?
- 灵活性和强大功能:通过提供一个完整的早期用户空间环境,
initramfs
可以执行任意复杂的初始化逻辑,这是initrd
无法比拟的。 - 高效:基于
tmpfs
/ramfs
,避免了块设备层和双重缓存的开销,内存使用更高效。 - 简化内核:使得内核主干可以保持小巧,将大量的硬件探测和初始化逻辑推迟到
initramfs
的用户空间阶段。
它存在哪些已知的劣势、局限性或在特定场景下的不适用性?
- 增加了启动的复杂性:引入了额外的启动阶段和组件,增加了排查启动问题的难度。
- 镜像大小:
initramfs
需要包含内核模块和用户空间工具,这会增加其镜像大小,从而可能略微延长启动的初始阶段(加载内核和initramfs
到内存)。 - 非必需场景:对于内核被编译为单体(monolithic)、所有必需驱动都内建(built-in)的系统(常见于一些定制的嵌入式设备),
initramfs
是不必要的,内核可以直接挂载根文件系统。
使用场景
在哪些具体的业务或技术场景下,它是首选解决方案?
它是现代通用Linux系统启动的唯一且标准的解决方案。
- 所有使用模块化驱动的桌面/服务器系统。
- 基于LVM或软件RAID的根分区。
- 全盘加密(FDE)系统。
- 通过网络(NFS, iSCSI)启动的系统。
- “Live CD”/“Live USB”系统:它们通常将整个操作系统放在一个压缩的文件系统镜像中(如SquashFS),
initramfs
负责找到并挂载这个镜像。
是否有不推荐使用该技术的场景?为什么?
- 资源极其受限的嵌入式系统:如果Flash空间和RAM都非常宝贵,且硬件配置固定,那么将所有必需驱动都编译进内核,并完全禁用
initramfs
,是一种常见的优化策略,可以减小存储占用并可能加快启动速度。 - 追求极简启动的特定场景:在一些特定的虚拟化或容器场景中,如果根文件系统非常简单(例如,一个
virtio-blk
设备,其驱动可以内建),为了最快的启动速度,也可能会选择不使用initramfs
。
对比分析
请将其 与 其他相似技术 进行详细对比。
initramfs
vs. initrd
(传统)
特性 | initramfs (现代) |
initrd (传统) |
---|---|---|
格式 | cpio归档文件。 | 块设备镜像 (通常是ext2格式)。 |
内核处理 | 内核直接在内存中解压并填充到rootfs (tmpfs )。 |
内核将其作为一个RAM盘块设备来处理,需要文件系统驱动来挂载它。 |
缓存 | 无双重缓存。直接使用内存。 | 有双重缓存。RAM盘本身被页面缓存所缓存。 |
内存使用 | 高效。使用后,内存可以被完全释放和回收。 | 低效。RAM盘占用的内存直到被显式释放前都无法被用作他途。 |
大小 | 动态。tmpfs 的大小可根据需要增长。 |
固定。镜像创建时大小就已确定。 |
当前状态 | 现代标准 | 已被取代的遗留技术 |
CPIO解析器辅助函数:写入、链接、错误和时间戳管理
本代码片段提供了CPIO解析状态机所需的一系列核心辅助函数。这些函数是状态机中do_*
动作函数的底层实现,负责处理具体的任务,包括:将文件数据写入rootfs
并计算校验和(xwrite
),管理硬链接的创建(find_link
),处理错误(error
),以及一个精巧的机制用于在文件提取完成后正确地设置目录的修改时间(dir_utime
)。它们是确保initramfs
被正确、完整地提取到rootfs
中的基础工具集。
实现原理分析
1. 文件写入与校验和 (xwrite
)
xwrite
是一个对底层kernel_write
的封装,旨在提供更强健的大文件写入能力和集成的校验和计算。
- 分块写入: 内核的
write
系统调用有最大单次写入长度的限制(通常是MAX_RW_COUNT
)。xwrite
通过一个while
循环,将可能超过此限制的大块数据分解成多个小的kernel_write
调用,确保所有数据都能被写入。 - 错误重试: 它能正确处理
-EINTR
(被信号中断)和-EAGAIN
(临时不可用)等可恢复的错误,通过continue
语句实现重试,增强了写入的健壮性。 - 校验和计算: 如果CPIO归档格式是带校验和的(
csum_present
为真),xwrite
会在数据成功写入的同时,逐字节地累加出一个运行校验和io_csum
。这个和将在文件写完后与头部中记录的hdr_csum
进行比较,以验证数据完整性。
2. 硬链接管理 (Link Hash)
为了正确处理硬链接(多个文件名指向同一个inode),代码实现了一个小型的、专用的哈希表。
- 数据结构:
struct hash
存储了唯一标识一个inode所需的信息(ino
,major
,minor
,mode
)以及第一次遇到该inode时的文件名。head
数组是一个包含32个链表头的哈希桶。 find_link
函数: 当CPIO解析器遇到一个nlink
大于1的文件时,会调用此函数。- 它使用inode号、设备号等信息通过
hash()
函数计算出一个哈希值,定位到对应的哈希桶。 - 它遍历桶中的链表,查找是否有已记录的、匹配当前inode信息的条目。
- 如果找到匹配项,意味着这个inode之前已经出现过。函数返回之前记录的文件名。调用者(
do_name
)会使用这个返回的路径创建一个硬链接,而不是创建新文件和写入数据。 - 如果未找到,意味着这是此inode第一次出现。函数会分配一个新的
hash
节点,记录下当前inode的信息和文件名,将其添加到哈希表中,并返回NULL
。调用者会继续按正常流程创建文件。
- 它使用inode号、设备号等信息通过
free_hash
函数: 在整个initramfs
解析完毕后,此函数负责遍历整个哈希表,释放所有为硬链接跟踪而分配的内存。
3. 时间戳管理 (Timestamp Management)
在CPIO流中,目录的条目出现在其包含的文件条目之前。如果在一创建目录时就设置其修改时间(mtime),那么后续在其中创建文件时,VFS会自动将目录的mtime更新为当前时间,从而丢失了归档中原始的mtime。
- 延迟处理: 为了解决这个问题,代码实现了一个延迟处理机制。当
do_name
创建一个目录时,它不直接设置mtime,而是调用dir_add
。 dir_add
: 这个函数将目录的名称和原始mtime存储在一个dir_entry
结构体中,并将其添加到一个全局的链表dir_list
里。dir_utime
: 在整个unpack_to_rootfs
的末尾,dir_utime
函数被调用。它遍历dir_list
链表,此时所有的文件和子目录都已创建完毕。它为链表中的每个目录调用do_utime
,将归档中记录的原始mtime设置给它,从而确保了时间戳的正确性。
代码分析
1 | static __initdata bool csum_present; // 标志,指示当前CPIO条目是否带校验和。 |
CPIO解析器状态机实现:从头部解析到文件创建
本代码片段是CPIO解析状态机的具体实现,它包含了一系列do_*
函数,每个函数对应状态机的一个状态,以及核心的parse_header
函数。代码的核心功能是消费来自victim
缓冲区的字节流,解析CPIO头部信息,并根据解析出的元数据(文件名、类型、权限、大小等)在rootfs
中执行相应的文件系统操作,如创建文件、目录、符号链接,处理硬链接,以及写入文件内容。这是将initramfs
归档数据真正转化为一个可用文件系统的执行层。
实现原理分析
该状态机通过一系列短小、专注的函数协同工作,每个函数负责处理解析过程中的一个特定阶段。其核心原理是分段收集和处理,以及在数据不足时暂停并等待更多数据。
- 头部解析 (
parse_header
): 这是理解整个流程的关键。- CPIO “newc” (
070701
) 和 “crc” (070702
) 格式的头部是一个110字节的结构,其中所有元数据都以8个字符的ASCII十六进制字符串表示。 parse_header
函数接收到完整的头部数据后,它的for
循环会精确地遍历这13个8字节的字段。- 在循环中,
simple_strntoul
函数被调用,它将每个8字符的十六进制字符串(如"0000A1ED"
)转换为一个无符号长整型数值。 - 转换后的数值被存入
parsed
数组,然后被赋给一系列全局静态变量(ino
,mode
,uid
,body_len
等)。这一步将ASCII头部“解码”为内核可以直接使用的二进制元数据。
- CPIO “newc” (
- 状态与数据流: 全局变量
state
和next_state
控制着状态机的流程。victim
指向当前数据块,byte_count
记录其大小。eat()
函数是消费数据流的基本操作,它前移victim
指针并减少byte_count
。 - 数据收集 (
read_into
,do_collect
):read_into
是一个关键的辅助函数。它尝试从victim
缓冲区中直接满足一个size
大小的数据读取请求。如果当前缓冲区数据足够,它就直接返回指向victim
中数据的指针,并将状态切换到next
。- 如果数据不足,它会将状态切换到
Collect
,并设置好目标缓冲区collect
、剩余所需字节数remains
和完成后的下一个状态next_state
。 do_collect
函数在这种情况下被激活,它会不断地从victim
拷贝数据到collect
缓冲区,直到remains
减为0,然后它会将状态切换到之前设定的next_state
。
- 状态流转:
- Start -> GotHeader:
do_start
启动整个流程,它调用read_into
请求CPIO_HDRLEN
字节的头部数据。 - GotHeader -> (GotName | GotSymlink | SkipIt):
do_header
是核心的决策函数。它首先检查CPIO魔数,然后调用parse_header
解码头部。根据解码出的mode
(文件类型),它决定下一个状态。 - GotName -> (CopyFile | SkipIt):
do_name
负责创建文件系统对象。它首先处理硬链接(maybe_link
)。对于普通文件,它会通过filp_open
创建并打开文件,然后将状态切换到CopyFile
准备写入数据。对于目录、设备节点等,它直接调用init_mkdir
,init_mknod
等创建,然后进入SkipIt
。 - CopyFile -> SkipIt:
do_copy
负责将文件内容写入do_name
中打开的文件。它使用body_len
作为写入长度的依据,分块写入直到完成,然后关闭文件并将状态切换到SkipIt
。 - GotSymlink -> SkipIt:
do_symlink
在收集完文件名和链接目标后,调用init_symlink
创建符号链接,然后进入SkipIt
。 - SkipIt -> Reset:
do_skip
是一个通用状态,用于跳过当前CPIO条目中剩余的所有数据,然后将状态切换到Reset
。 - Reset -> Start:
do_reset
负责清理填充的空字节,为解析下一个归档做准备。
- Start -> GotHeader:
代码分析
CPIO头部解析 (CPIO Header Parsing)
1 | static __initdata time64_t mtime; // 文件修改时间 |
状态机实现 (State Machine Implementation)
1 | // 定义状态机的各种状态。 |
CPIO解析器状态机:将字节流转换为文件系统
本代码片段是unpack_to_rootfs
函数的“心脏”,它实现了一个经典的状态机,用于逐字节地解析CPIO归档流。flush_buffer
作为从解压器接收明文数据的入口,write_buffer
作为驱动状态机运转的引擎,而actions
数组则是状态机的大脑,将不同的解析状态映射到具体的处理函数。其核心功能是将一个连续的、无结构的CPIO字节流,精确地转换为在rootfs
中创建目录、文件、符号链接等一系列VFS操作。
实现原理分析
该代码的实现是一个精巧的流式处理器,其原理如下:
- 状态机定义 (
actions
数组): 代码的核心是一个名为actions
的函数指针数组。这个数组的索引是一个枚举值(enum state
,如Start
,Collect
,GotHeader
等),代表了解析器当前所处的状态。数组的每个元素都指向一个相应的处理函数(do_start
,do_collect
等)。这种设计将“状态”和“行为”清晰地解耦。 - 状态机引擎 (
write_buffer
): 这个函数是驱动整个状态机运转的引擎。它接收一小块数据,然后进入一个while
循环。在循环中,它根据当前的全局state
变量,从actions
数组中取出对应的函数指针并执行。- 每个
do_*
动作函数被设计为返回0
表示“当前状态的工作还未完成,需要更多数据或更多处理”,返回非0
则表示“当前状态的工作已完成,可以转换到下一个状态了”。 while
循环会一直调用当前状态的动作函数,直到该函数返回非0
,表示一个状态转换点已经达到。- 函数返回消耗的字节数,告知上游调用者(
flush_buffer
)数据处理的进度。
- 每个
- 数据注入与流控制 (
flush_buffer
): 这个函数是解压器和CPIO解析器之间的桥梁。- 它被注册为解压器的回调函数,因此会分批次地接收解压后的明文CPIO数据。
- 它调用
write_buffer
来驱动状态机处理这些数据。 - 它包含一个关键的
while
循环,用于处理write_buffer
未能一次性消耗完所有输入数据的情况,这是流式处理的典型特征。 - 最重要的是,它还负责处理串联的CPIO归档。在一个数据块被
write_buffer
处理完毕后,flush_buffer
会检查下一个字节。如果是CPIO魔数'0'
,它就将状态机重置到Start
状态,准备解析下一个归档。如果是\0
空字节(常用作填充),则进入Reset
状态,为下一个压缩段做准备。
整个流程可以概括为:解压器产生明文数据 -> flush_buffer
接收数据 -> flush_buffer
调用write_buffer
-> write_buffer
根据state
调用actions
数组中的do_*
函数 -> do_*
函数解析数据、执行VFS操作(如创建文件)并更新state
-> 循环往复,直到所有数据被解析完毕。
代码分析
1 | // actions: 状态机动作分派表。 |
Initramfs解包器:解压并提取归档至根文件系统
本代码片段是Linux内核中负责initramfs
解压和提取的核心函数unpack_to_rootfs
。其主要功能是接收一个内存中的initramfs
归档(archive)作为输入,自动检测其压缩格式(如gzip, bzip2, lzma等),对其进行解压,并将解压出的CPIO(Copy In, Copy Out)归档流解析,最终在rootfs
(一个空的ramfs
实例)中创建出对应的目录和文件结构。这是内核从一个二进制镜像文件过渡到拥有一个可用文件系统的关键步骤。
实现原理分析
unpack_to_rootfs
的实现是一个巧妙的流式处理器,它能够处理一个或多个串联在一起的(压缩或未压缩的)数据块。其工作原理如下:
- 缓冲区分配: 函数首先通过
kmalloc
分配一块内存,用于存放CPIO头部、符号链接目标路径和文件名等解析过程中的临时数据。 - 主处理循环: 函数的核心是一个
while
循环,只要还有未处理的数据(len > 0
),循环就会继续。这个循环的设计使得它可以处理由多个数据段拼接而成的initramfs
镜像。 - 压缩格式检测: 在循环的每次迭代中,它首先调用
decompress_method
。这个函数会检查输入缓冲区buf
头部的“魔数”(magic numbers),以识别出数据流的压缩格式(例如,gzip的魔数是0x1f 0x8b
)。如果识别成功,它会返回一个对应的解压缩函数指针(如decompress_gunzip
)。 - 解压与数据流处理:
- 如果
decompress_method
返回了一个有效的解压函数,unpack_to_rootfs
就会调用这个函数。 - 这个解压函数采用回调机制工作。它会从
buf
中读取压缩数据,进行解压,然后将解压出的明文数据块(即CPIO归档流)分批次地传递给一个名为flush_buffer
的回调函数(该函数未在此代码段中显示,但属于同一文件)。 flush_buffer
函数内部实现了一个CPIO解析的状态机。它接收明文数据,解析CPIO头部,根据头部信息在rootfs
中创建文件、目录、符号链接等,并写入文件内容。
- 如果
- 处理未压缩数据: 如果输入数据段不是一个已知的压缩格式,但以CPIO的魔数’0’开头,代码会直接调用
write_buffer
,将这段未压缩的数据直接送入CPIO解析状态机。 - 迭代处理: 当一个压缩流解压完毕或一个未压缩段处理完毕后,主循环会更新
buf
指针和剩余长度len
,使其指向下一段数据,然后开始新一轮的格式检测和处理。这个过程会一直持续,直到所有输入数据都被消耗完毕。 - 清理: 循环结束后,函数会释放之前分配的缓冲区和CPIO解析过程中可能产生的其他资源(如硬链接哈希表)。
代码分析
1 |
|
传统Initrd处理:将内存镜像保存为文件
本代码片段定义了populate_initrd_image
函数,它是内核rootfs
填充过程中的一个回退(fallback)机制。当引导加载程序提供了一个内存镜像,而内核尝试将其作为现代initramfs
(CPIO归档)解压失败时,此函数就会被调用。它的核心功能是假定该镜像是传统的initrd
(一个原始的文件系统镜像,如ext2),并将其完整地、不加修改地保存为rootfs
中的一个名为/initrd.image
的文件。这个过程将后续解析和挂载initrd
的责任委托给了用户空间的/init
脚本。
实现原理分析
此函数的实现逻辑简单而直接,它扮演了一个数据“搬运工”的角色,而不是解析器。
- 条件编译: 整个函数被包裹在
#ifdef CONFIG_BLK_DEV_RAM
中。这是因为传统initrd
的处理流程在用户空间通常需要一个RAM块设备(如/dev/ram0
)。如果内核没有编译RAM块设备的支持,那么保存这个镜像文件就毫无意义,因此整个功能被编译排除。 - 信息通知: 函数首先打印一条内核日志,明确告知用户,传递的镜像不是
initramfs
,现在正被当作initrd
处理。这对于系统启动调试至关重要。 - 文件创建: 它使用VFS函数
filp_open
,在当前的rootfs
(这是一个ramfs
实例)中创建一个新文件/initrd.image
。O_WRONLY|O_CREAT
标志确保了文件被创建并以只写模式打开。权限被设置为0700
(所有者读、写、执行)。 - 数据转储: 函数的核心操作是调用
xwrite
。这个辅助函数负责将引导加载程序放置在内存中(从物理地址initrd_start
到initrd_end
)的整个initrd
镜像的原始二进制数据,完整地写入到刚刚创建的/initrd.image
文件中。 - 资源释放: 写入完成后,调用
fput(file)
来关闭文件描述符并减少其引用计数,释放相关资源。
至此,内核对initrd
的处理就结束了。后续的典型用户空间启动脚本 (/init
) 会执行如下操作:
- 创建RAM块设备节点:
mknod /dev/ram0 b 1 0
- 将镜像内容拷贝到RAM块设备:
dd if=/initrd.image of=/dev/ram0
- 挂载RAM块设备作为新的根文件系统:
mount /dev/ram0 /new_root
- 切换根目录到新的根文件系统:
pivot_root /new_root /new_root/old_root
代码分析
1 |
|
Rootfs填充:从内存镜像到可用的初始文件系统
本代码片段是Linux内核启动过程中一个至关重要的环节。它的核心功能是找到、解压并填充初始根文件系统(rootfs
)。rootfs
最初是一个空的ramfs
实例,而这段代码负责将一个初始的用户空间环境(通常是initramfs
CPIO归档)解压到其中,从而为内核执行第一个用户空间进程(/init
)提供必要的文件和目录。代码还优雅地处理了现代initramfs
和传统initrd
两种不同的初始RAM磁盘机制。
实现原理分析
该功能的实现逻辑清晰地分层,并在一个rootfs_initcall
中被触发,确保它在VFS初始化后、大部分驱动加载前执行。
- 异步调度 (
populate_rootfs
):- 它不直接执行填充操作,而是通过
async_schedule_domain
将核心工作函数do_populate_rootfs
调度为异步执行。这在多核系统上可以与其它初始化任务并行。 - 它提供了一个同步点
wait_for_initramfs
。如果内核的其他部分(在rootfs_initcall
之后)需要访问文件系统,可以调用此函数来确保rootfs
的填充已经完成。
- 它不直接执行填充操作,而是通过
- 核心填充逻辑 (
do_populate_rootfs
):- 第一优先级:内置initramfs: 首先,它会无条件地尝试解压一个内置的
initramfs
。__initramfs_start
和__initramfs_size
是链接器在内核编译时定义的符号,指向一个被链接进内核二进制文件(vmlinux)的CPIO归档。这是现代嵌入式系统中最常见的配置。如果解压失败,系统会panic
,因为这是最基本的根文件系统来源。 - 第二优先级:外部initramfs: 接下来,它检查
initrd_start
。这个变量(如果不为0)指向由引导加载程序(如U-Boot, GRUB)加载到内存中的外部镜像。内核会尝试将这个外部镜像也作为initramfs
(CPIO归档)来解压。如果成功,其内容会覆盖在内置initramfs
之上,允许在不重新编译内核的情况下更新或修改初始用户空间。 - 失败回退:传统initrd (
populate_initrd_image
): 如果将外部镜像作为initramfs
解压失败(例如,因为它不是一个CPIO归档),代码会进入一个回退逻辑。这通常意味着外部镜像是一个传统的initrd
,即一个文件系统的原始镜像(如ext2, romfs等)。- 在这种情况下,内核不会去解析这个文件系统镜像。
- 相反,它会调用
populate_initrd_image
,这个函数会在已经部分填充的rootfs
中创建一个名为/initrd.image
的文件,并将外部镜像的原始二进制数据完整地写入这个文件中。 - 后续的启动过程依赖于
/init
脚本来处理这个文件,典型的步骤是:mount -t ramfs ramfs /dev
,mknod /dev/ram0 b 1 0
,dd if=/initrd.image of=/dev/ram0
,然后挂载/dev/ram0
作为新的根。
- 第一优先级:内置initramfs: 首先,它会无条件地尝试解压一个内置的
- 内存管理: 填充操作完成后,代码会检查是否需要保留(
do_retain_initrd
)外部镜像的内存。通常情况下,内存会被free_initrd_mem
释放。如果需要保留(例如为了kexec
),则会在sysfs中创建一个接口/sys/firmware/initrd
,允许用户空间回读原始镜像。
代码分析
1 |
|