Erofs-utils 中制作 EROFS 压缩镜像的代码逻辑
1. erofs-utils
是什么 §
erofs-utils
是一组工具,用于处理 EROFS
(Enhanced Read-Only File System,增强型只读文件系统)的文件系统镜像。这包括创建、检查和解包 EROFS 镜像。EROFS 是由华为开发,主要用于 Android 和其他嵌入式系统中,特别强调高效的读取性能和对压缩数据的支持。
构建 erofs-utils
的基本过程如下:
- 最后能够把对应的二进制程序都放入到
$(pwd)/build
目录下
2. 制作镜像 §
制作镜像使用的是 erofs-utils
工具组中的 mkfs.erofs
,这个工具会将一个目录树转换成一个 EROFS 镜像,该镜像可以挂载为只读文件系统。
假设我们想将 /path/to/source/dir
目录下的内容制作成 EROFS 镜像文件 /path/to/erofs.img
,可以使用以下指令:
这样,就能把我们在目录下的数据都压缩进 erofs 的镜像了:
3. 挂载镜像 §
挂载镜像要求 kernel 是支持 erofs
的,可以通过 lsmod | grep erofs
检查。
假设我们想将 /path/to/erofs.img
EROFS 镜像文件挂载到 /path/to/mount
,可以使用以下指令:
4. 制作镜像的时候都发生了什么? §
制作镜像的操作都由 mkfs.erofs
来实现,我们可以将其划分为几个不同的阶段:
- 解析命令行参数
- 扫描输入目录
- 文件压缩
- 构建文件系统元数据
- 生成镜像文件
4.1 解析命令行参数 §
Erofs 专门使用了一个数据结构来保存所有涉及的配置:
在程序中将其定义为一个全局变量,在开始时,首先需要对其进行初始化:
然后把可选的配置先初始化到默认配置:
值得注意的是,这里出现了一个 sbi
的变量,他代表着 erofs
的 superblock,也是定义为一个全局变量:
接下来就是解析命令行参数了,这里把所有的逻辑都写到 mkfs_parse_options_cfg
里面了,有种力大飞转的美感 : (
为了更好地理解这一坨代码在解析什么,我们可以先看一下 mkfs.erofs
的 help 文档:
可以看到,mkfs.erofs
的使用形式是:
mkfs.erofs [OPTS] FILE SOURCE(S)
基本上,这里的每一个可配置的参数会被映射到程序的数据结构中,解析这一系列的参数使用的是 getopt_long
,这是一个 GNU getopt
函数的扩展,getopt
是 POSIX 标准的一部分。使用时,getopt_long
会逐步解析每一个参数,然后将其转化为对应的返回值。getopt_long
会接受一个短参数(字符串形式)和长参数(结构体形式),并返回匹配到的参数的短参数值(匹配到对应的短参数或长参数指定的短参数形式)。短参数的每个字符代表一个短选项,如果选项需要参数,则在该字符后面加上冒号。
mkfs.erofs
的长参数定义如下:
根据这样的定义进行解析:
这里的长参数和短参数是完全分开的,也就是说各自表达不同的含义,具体可以结合上述的手册比对。
在解析完一系列的参数之后,读取传入的 FILE
和 SOURCE(S)
:
除了解析一般的命令行参数之外,erofs-utils
还会检查 SOURCE_DATE_EPOCH
,以确保构建产物的时间戳是一致的。在构建产物中嵌入相同的时间戳是为了实现可重复构建(reproducible builds),这是一种确保软件构建过程可靠性和安全性的重要实践。
4.2 扫描输入目录 §
接下来,mkfs.erofs
会扫描传入的 FILE 文件路径,这在函数 dev_open
中实现:
4.3 压缩文件 §
首先解释一下 erofs
的布局。erofs
采用的是 fixed-sized output compression,简而言之就是压缩的时候,原数据从头开始压缩,直到压缩得到的数据填满 4 K(即压缩时的单位大小,可配置),填满后再从新的原数据头开始压缩,周而复始得到一系列的 4 K 压缩数据和末尾的数据。
与之相对应的是 fixed-sized output compression,即每次取固定大小的数据做压缩,压缩出的数据大小不固定。如下图所示:
在 mkfs.erofs
中,需要先初始化出一系列的 bucket 来放置这些数据:
接着,先写入 superblock:
完成 superblock 的写入之后,开始提取压缩的一些配置:
erofs_load_compress_hints
函数用于从配置文件中加载压缩提示(compress hints)信息,这些信息用于指导 EROFS(Enhanced Read-Only File System)文件系统如何处理特定文件或文件模式的压缩。函数通过解析一个给定的文件来设置压缩配置,这些配置可以指定哪些文件应该被压缩以及使用什么算法进行压缩。
下一步就是初始化 compressor 了,后续压缩都是调用 compressor 的 compress_destsize
方法实现的。
最后,如果有去重的需求,会调用 z_erofs_dedupe_init
做初始化:
4.4 构建文件系统 §
构建文件系统分为两步:
- 构建 xattrs:这是一些扩展属性,在这里就不过多介绍
- 构建文件系统:主要是构建以镜像目录为根的 inode 树
这里的 erofs_mkfs_build_tree_from_path
是最核心的部分,他将 source 文件夹下的文件构造成一颗树,并在后续进行压缩,最终写入镜像。
- 通过
erofs_iget_from_path
为传入的目录文件创建目录文件 inode。该目录文件对应的是 erofs 文件系统的根目录 /
- 将该 inode 的 parent 指向自己,说明自己是根目录
- 调用
erofs_mkfs_build_tree
递归地为根目录创建子目录及文件,并一一对应当前目录下的子目录和文件
具体来说,在执行 erofs_iget_from_path
的过程中,有如下流程:
- 通过
lstat
解析 path,可以快速获知当前 path 是目录还是文件
- 传入的是目录,因此不会执行
erofs_iget
而直接调用 erofs_new_inode
创建一个新的 inode
- 通过
erofs_fill_inode
对新 inode 进行初始化
在 erofs_fill_inode
中,主要就是装填 inode 的属性。此时,也将 path 设入 inode 的 srcpath 中,建立了源文件系统与目标文件系统的映射关系。
最后,由于是新的 inode 。需要将其插入 inode_hashtable
中,用来加速查询。
完成之后,进入到 erofs_mkfs_build_tree
函数,这个函数负责初始化 root 目录下的目录项,然后递归地向下进行构建。遍历时有两种情况:
- 如果遍历到文件,就调用
erofs_write_file
写入
- 如果遍历到路径,递归地创建下面目录的 inode 树,也是调用
erofs_mkfs_build_tree_from_path
来实现
4.5 生成镜像文件 §
在 erofs_mkfs_build_tree
中,遍历到文件就会写入镜像。写入时调用函数 erofs_write_file
,这个函数也会判断两种情况:
- 需要压缩,调用压缩函数
erofs_write_compressed_file
写入
- 不需要压缩,直接写入
不压缩写入并不重要,我们主要看压缩的逻辑:
完整的流程大致如下:
最后,镜像的布局如下图: