FUSE 透传

Android 12 支持 FUSE 透传功能,此功能可以最大限度地降低 FUSE 开销,从而实现可媲美直接访问下层文件系统的性能。android12-5.4android12-5.10android-mainline(仅限测试)内核支持 FUSE 透传功能,这意味着是否支持此功能取决于设备使用的内核和设备搭载的 Android 版本:

  • 从 Android 11 升级到 Android 12 的设备无法支持 FUSE 透传功能,因为这些设备的内核已冻结,并且无法迁移至已正式升级为包含 FUSE 透传变更的内核。

  • 发布时搭载 Android 12 的设备在使用官方内核时可以支持 FUSE 透传功能。在此类设备上,用于实现 FUSE 透传功能的 Android 框架代码已嵌入 MediaProvider Mainline 模块中,该模块会自动升级。未将 MediaProvider 实现为 Mainline 模块的设备(例如 Android Go 设备)也可以获取 MediaProvider 变更,因为这些变更已公开共享。

FUSE 与 SDCardFS

用户空间中的文件系统 (FUSE) 是一种机制,可让内核(FUSE 驱动程序)将 FUSE 文件系统中执行的操作外包给用户空间程序(FUSE 守护程序),由其来实现操作。Android 11 废弃了 SDCardFS,并将 FUSE 作为存储空间模拟的默认解决方案。作为此变更的一部分,Android 实现了自己的 FUSE 守护程序,用于拦截文件访问,强制执行额外的安全和隐私功能,并在运行时操作文件。

虽然 FUSE 在处理页面或属性等可缓存的信息时效果非常理想,但在访问外部存储设备时却会导致性能下降,这一点在中低端设备上尤为明显。导致这些性能下降问题的原因在于,实现 FUSE 文件系统需要一系列组件协同配合,在 FUSE 驱动程序与 FUSE 守护程序之间的通信过程中也需要从内核空间多次切换到用户空间(与此相比,直接访问下层文件系统则更加精简且完全在内核中实现)。

若要减轻这些性能下降问题,应用可以使用拼接来减少数据复制,并使用 ContentProvider API 直接访问下层文件系统文件。即使采取这些措施并进行其他优化之后,与直接访问下层文件系统相比,使用 FUSE 时的读取和写入操作仍会遇到带宽降低的问题,特别是在随机读取操作中更为突出,因为在这种情况下没有缓存或预读可助一臂之力。通过旧路径 /sdcard/ 直接访问存储空间的应用会继续出现显著的性能下降,尤其是在执行 IO 密集型操作时。

SDcardFS 用户空间请求

使用 SDcardFS 可从内核中移除用户空间调用,从而加快存储空间模拟和 FUSE 权限检查。用户空间请求遵循以下路径:用户空间 → VFS → sdcardfs → VFS → ext4 → 页面缓存/存储空间。

FUSE 透传 SDcardFS

图 1. SDcardFS 用户空间请求

FUSE 用户空间请求

FUSE 最初用于实现存储空间模拟并让应用能够透明地使用内部存储空间或外部 SD 卡。使用 FUSE 会产生一些开销,因为每个用户空间请求都遵循以下路径:用户空间 → VFS → FUSE 驱动程序 → FUSE 守护程序 → VFS → ext4 → 页面缓存/存储空间。

FUSE 透传 FUSE

图 2. FUSE 用户空间请求

FUSE 透传请求

大多数文件访问权限是在文件打开时进行检查,还有一些其他权限是在对文件执行读取和写入操作时进行检查。在某些情况下,系统可以在文件打开时确定发出请求的应用对所请求的文件具有完全访问权限,这样就不需要继续将读取和写入请求从 FUSE 驱动程序转发到 FUSE 守护程序(因为这样做只是将数据从一个位置移到另一个位置)。

借助 FUSE 透传功能,负责处理待处理请求的 FUSE 守护程序可以通知 FUSE 驱动程序:可以执行相关操作,并且可将后续所有读取和写入请求直接转发给下层文件系统。如此一来,就可以避免等待用户空间 FUSE 守护程序回复 FUSE 驱动程序请求所产生的额外开销。

下面对 FUSE 请求与 FUSE 透传请求进行了对比。

FUSE 透传对比

图 3. FUSE 请求与 FUSE 透传请求

当应用执行 FUSE 文件系统访问时,会发生以下操作:

  1. FUSE 驱动程序处理请求并将其加入队列,然后通过 /dev/fuse 文件(FUSE 守护程序无法读取该文件)中的特定连接实例将请求提交给负责处理该 FUSE 文件系统的 FUSE 守护程序。

  2. 当 FUSE 守护程序收到打开文件的请求时,它会确定 FUSE 透传是否适用于该特定文件。如果适用,守护程序会执行以下操作:

    1. 将此请求通知 FUSE 驱动程序。

    2. 使用 FUSE_DEV_IOC_PASSTHROUGH_OPEN ioctl(必须对打开的 /dev/fuse 的文件描述符执行)针对该文件启用 FUSE 透传功能。

  3. ioctl 接收包含以下内容的数据结构(作为参数):

    • 作为透传功能目标的下层文件系统文件的文件描述符。

    • 当前正在处理的 FUSE 请求(必须为待处理状态或创建并待处理状态)的唯一标识符。

    • 可以留空并打算供未来实现之用的额外字段。

  4. 如果 ioctl 成功,FUSE 守护程序会完成待处理的请求,FUSE 驱动程序会处理 FUSE 守护程序回复,并且系统会将对下层文件系统文件的引用添加到内核中的 FUSE 文件中。当应用请求对 FUSE 文件执行读写操作时,FUSE 驱动程序会检查是否有可用的对下层文件系统文件的引用。

    • 如果有可用的引用,驱动程序会以下层文件系统文件为目标,使用相同的参数创建一个新的虚拟文件系统 (VFS) 请求。

    • 如果没有可用的引用,驱动程序会将请求转发给 FUSE 守护程序。

对常规文件执行读写操作和读取迭代器/写入迭代器操作以及对内存映射文件执行读写操作时,都会发生上述操作。针对特定文件的 FUSE 透传会一直存在,直到相应文件关闭为止。

实现 FUSE 透传功能

如需在搭载 Android 12 的设备上启用 FUSE 透传功能,请将以下代码行添加到目标设备的 $ANDROID_BUILD_TOP/device/…/device.mk 文件中。

# Use FUSE passthrough
PRODUCT_PRODUCT_PROPERTIES += \
    persist.sys.fuse.passthrough.enable=true

如需停用 FUSE 透传功能,请省略上述配置更改或将 persist.sys.fuse.passthrough.enable 设为 false。如果您之前已启用 FUSE 透传功能,停用该功能会使设备无法使用 FUSE 透传功能,但设备仍可正常运行。

如需在不刷写设备的情况下启用/停用 FUSE 透传功能,请使用 ADB 命令更改系统属性。相关示例如下所示。

adb root
adb shell setprop persist.sys.fuse.passthrough.enable {true,false}
adb reboot

如需获得其他帮助,请参阅参考实现

验证 FUSE 透传功能

如需验证 MediaProvider 是否在使用 FUSE 透传功能,请查看 logcat 中的调试消息。例如:

adb logcat FuseDaemon:V \*:S
--------- beginning of main
03-02 12:09:57.833  3499  3773 I FuseDaemon: Using FUSE passthrough
03-02 12:09:57.833  3499  3773 I FuseDaemon: Starting fuse...

日志中存在 FuseDaemon: Using FUSE passthrough 条目可确保正在使用 FUSE 透传功能。

Android 12 CTS 包括 CtsStorageTest 测试,内含触发 FUSE 透传的测试。如需手动运行该测试,请使用 atest,如下所示:

atest CtsStorageTest