在内核级别优化 SquashFS(一直到 Android 9)

SquashFS 是 Linux 的只读压缩文件系统。该文件系统设计为只读属性,因此适合在系统分区上使用。很多 Android 设备都可以通过对其系统分区使用此文件系统来获益,例如以下设备:

  • 存储容量小的设备,例如 Android Watch。
  • 闪存速度缓慢的设备(压缩可减少块 I/O 的数量)。

遗憾的是,SquashFS 的性能落后于 ext4。

优化

为提高 SquashFS 的性能,已经实现下列优化。

减少内存使用量和 memcpy 调用次数

读取块(默认为 128K)时,SquashFS 会尝试抓取包含此块的所有页面。

如果某个页面是最新页面或已被锁定,SquashFS 会转而分配一个完整块,提交读取请求,然后将其内容复制到这些页面。

这种方法极其低效;经过一段时间后,页面缓存可能包含最新页面,即使相邻页面并非最新页面也是如此。

代码现在能够处理有孔(即缺少页面)的块,因而可以通过以下方式来提高性能:

  • 减少 memcpy 调用次数
  • 减少内存分配

异步读取

SquashFS 仍使用已弃用的 ll_rw_block() 函数。使用这种方法存在两个问题:

  • 顾名思义,该函数会等待读取完成之后再返回结果。这是多余的,因为 .readpage() 已在页面锁定上等待。此外,我们需要一个异步机制来高效实现 .readpages()
  • 合并读取请求完全取决于 I/O 调度程序。 ll_rw_block() 只会为每个缓冲区创建一个请求。在应该合并的内容方面,SquashFS 包含的信息比 I/O 调度程序多。此外,合并请求意味着我们对 I/O 调度程序的依赖会有所减少。

因此,ll_rw_block() 函数已被 submit_bio() 替换。

Readpages(预提取)

SquashFS 不会实现 .readpages(),因此内核会反复调用 .readpage()

由于我们的读取请求是异步请求,因此内核可以使用其异步预读机制真正地预提取页面。

优化未压缩块的读取操作

诸如 Android 之类的现代系统包含大量经过压缩的文件。因此,映像包含大量无法压缩的块。

SquashFS 使用相同的逻辑处理经过压缩的块和未压缩的块:当 SquashFS 需要读取一个页面时,它实际上读取的是一个完整块(默认为 128k)。虽然对于经过压缩的块,这是必需的;但对于未压缩的块,这只是在浪费资源。

现在,SquashFS 只读取预读算法建议的内容,而不会读取一个完整块。

这极大地提高了随机读取的性能。

代码

AOSP 中提供 SquashFS 代码优化: