SquashFS(SQUASH File System) 是一种只读压缩文件系统,旨在最小化存储空间并提供高性能访问。它通常用于 Linux 发行版中的 Live CD/DVD 映像和嵌入式系统等场景,以减小文件系统的大小并提高性能, 以下是 SquashFS 文件系统的一些主要特点和概述:
- 只读文件系统: SquashFS 是一种只读文件系统,这意味着在挂载后不能对其进行写入操作。它主要用于存储静态文件和只读数据,例如操作系统文件、应用程序和配置文件
- 高压缩比: SquashFS 通过使用压缩算法来减小文件系统的大小。它可以实现高压缩比,从而节省存储空间。这对于嵌入式系统和 Live CD/DVD 映像非常有用,因为它们需要占用尽可能少的磁盘空间
- 高性能: 尽管是只读文件系统,但 SquashFS 提供了快速的访问速度。它通过将文件系统的内容预先解压缩并缓存在内存中来实现高性能。这意味着在挂载后,文件系统中的文件可以迅速访问,而无需实时解压缩
- 适用于嵌入式系统: SquashFS 广泛用于嵌入式系统,如路由器、智能电视和嵌入式控制器。它可以帮助减小设备的固件大小,同时提供快速的启动和运行性能
- 可堆叠使用: SquashFS 文件系统可以与其他文件系统堆叠使用,以实现特定的用例。例如,它可以与 tmpfs(临时文件系统) 一起使用,以实现只读根文件系统和可写临时文件系统的组合
- 不支持写入操作: 由于 SquashFS 是只读文件系统,它不支持写入操作。如果需要对文件系统进行写入操作,通常需要与可写文件系统(如 EXT4 或 FAT)一起使用
SquashFS 文件系统在减小文件系统大小、提高性能和存储静态数据方面具有很大优势,因此在嵌入式系统和 Live CD/DVD 制作中非常常见。它提供了一种有效的方式来管理只读数据,并在资源有限的环境中提供高性能, 在 Linux 中使用 SQUASHFS 需要打开内核宏 CONFIG_SQUASHFS.
SQUASHFS 文件系统是一个只读文件系统,可以看到映射文件的 mmap 接口使用了 generic_file_readonly_mmap 函数,generic_file_readonly_mmap 函数为文件映射的 VMA 提供的 vm_ops 接口为 generic_file_vm_ops,该数据接口实现了 fault 接口 filemap_fault,那么文件映射 VMA 发生缺页时 filemap_fault 函数会被调用.
在 SQUASHFS 文件系统里,文件映射(File-Mapped) 的数据架构如上图,用户进程使用文件映射将文件映射到进程地址空间之后,进程使用 VMA 描述映射之后的虚拟内存区域,SQUASHFS 文件系统会为 VMA 提供相应 generic_file_vm_ops,另外 vm_file 指向映射的文件(struct file), 其又指向唯一的 STRUCT inode, 其 mapping 成员用于指向 STRUCT address_space, 该数据结构用于维护文件与 PAGE CACHE 和 VMA 的映射关系,其中 i_mmap 成员指向一颗区间树(RB-TREE), 该区间树维护了映射到该文件的 VMA. 另外 i_pages 指向 XARRAY 数组,该数组维护了文件映射的 PAGE CACHE,每个 PAGE CACHE 对应一个 STRUCT page 数据结构,STRUCT page 的 mapping 成员反过来指向 STRUCT address_space, 那么可以知道 PAGE CACHE 被哪些 VMA 映射,因此形成了一个闭环. 当进程首次访问 VMA 虚拟内存区域时,会触发缺页异常构造这些逻辑。那么接下来通过一个实践案例了解这种异常场景,实践案例在 BiscuitOS 上的部署逻辑如下:
cd BiscuitOS
make menuconfig
[*] DIY BiscuitOS/Broiler Hardware --->
[*] Virtio-BLK: SQUASHFS Filesystem Disk --->
[*] Package --->
[*] Paging Mechanism --->
[*] Page Fault with File-Mapped SQUASHFS --->
# 部署实践案例
make
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-FILE-SQUASHFS-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build
BiscuitOS-PAGING-PF-FILE-SQUASHFS-default Source Code on Gitee
实践案例由一个应用程序构成,进程在 23 行在 “/mnt/squashfs/” 目录下打开文件 BiscuitOS.txt 文件,该目录已经挂载为 SQUASHFS 文件系统,进程接着在 29 行调用 mmap 函数将文件映射到进程的地址空间,并在 40 行对文件对应的虚拟内存进行读操作,操作完毕之后就是释放虚拟内存和关闭文件. 以上便是一个最基础的实践案例,可以知道 40 行读操作就会触发缺页,为了可以看到内存在缺页异常里的流动,在 40 行前后加上 BS_DEBUG 开关:
接着在 SQUASHFS 文件映射内存缺页流程必经之路上任意位置加上 BS_DEBUG 函数,以此观察内存在某个函数里的流动,例如上图在 do_read_fault 函数的 4518 行加上 bs_debug 打印,以此确认内存流动到这里,接下来执行如下命令进行实践(需要提前打开内核宏: CONFIG_SQUASHFS_FS):
# 编译应用程序
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-FILE-SQUASHFS-default/
# 编译内核
make kernel
# 编译实践案例
make build
当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本,脚本里包括实践所需的命令,可以看到进程执行之后对虚拟内存的访问引起了缺页异常,并且该案例的缺页异常处理流程打印了字符串 “SQUASHFS PF on filemap_fault 0x6000000000” 两次, 那么说明实践案例分配了 PAGECACHE,同时也可以看到 PAGECACHE 按着之前分析的代码路径流动。最后开发者可以在该路径上的任何地方使用 bs_debug 查看 PAGECACHE 在缺页异常处理流程里的流动.
对于 SQUASHFS 文件系统映射文件到地址空间之后,进程访问该虚拟内存时,由于 MMU 发现对于的物理内存不存在,那么触发缺页异常。在缺页异常处理函数里,其主要做三个事情,首先是分配 PAGECACHE,如上图调用 filemap_alloc_folio 函数进行分配; 当分配 PAGECACHE 之后 SQUASHFS 文件系统向 BIO 层发送请求从磁盘里读取读取多个页表内容到 PAGECACHE, 由于磁盘 I/O 延迟无法立即获得文件内存,因此缺页异常会通过 VM_FAULT_RETRY 再次进行缺页处理,以确保文件内容已经更新到 PAGECACHE 里; 最后一个任务就是更新页表指向新的 PAGECACHE,以及更新页表标记为脏页等. 完成三个任务之后缺页异常处理函数就返回,那么进程可以正常访问虚拟内存. 因为 VM_FAULT_RETRY 的缘故,handle_mm_fault 会被执行两次,这么做的原因有如下:
- 磁盘 I/O 延迟: 当页面不在物理内存中,需要从磁盘加载时,可能由于磁盘 I/O 操作的延迟,内核无法立即获取所需页面。此时,内核可能会将页面标记为需要重新尝试,并等待磁盘 I/O 完成
- 页面锁定: 在某些情况下,内核可能会锁定某些页面,以确保它们在内存中不会被交换出去或释放。如果页面被锁定,内核可能会等待解锁页面后才能重新加载
- 其他临时情况: 还可能存在其他临时情况,导致页面无法立即加载到内存中。在这种情况下,内核可能会标记页面并尝试重新加载
do_read_fault 函数作为 SQUASHFS 文件系统映射内存读操作导致缺页的核心处理函数,与 do_shared_fault 写缺页不同的是,其在 4522-4526 行提供了 do_fault_around 函数,该函数在文件映射的内存发生缺页时用于一种优化策略,其作用是预取(prefetch)附近的页面,以减少未来可能的缺页。这个优化策略基于以下假设:
- 文件访问通常具有局部性(locality): 即如果一个页面被访问,那么附近的页面可能在不久的将来也会被访问。这是因为文件通常以较小的块进行读取,而不是逐个字节或逐个页面
- 文件映射的内存通常是按顺序或近似顺序访问的
SQUASHFS 文件系统对于 FAULT-AROUND 机制提供了 filemap_map_pages(vmf->vma->v-m_ops->map_pages) 函数来预读文件, 该函数是通用的文件预读操作,其主要任务就是未相邻的虚拟内存提前建立映射到相应 PAGECACHE 的页表,注意这里不是从磁盘读取内容,而是建立页表. FAULT AROUND 直接返回 VM_FAULT_NOPAGE,那么缺页异常函数可以直接完成. 接下来进程访问相邻的虚拟内存不会发生缺页.