分区存储
- 本页内容
- 应用访问限制
- 将分区存储与 FUSE 搭配使用
- FUSE 和 SDCardFS
- FUSE 性能微调
- 减轻与 FUSE 相关的性能影响
- 隐私优势远超性能劣势
- MediaProvider 和 FUSE 更新
分区存储会限制应用访问外部存储空间。在 Android 11 或更高版本中,以 API 30 或更高版本为目标平台的应用必须使用分区存储。之前,在 Android 10 中,应用可以选择停用分区存储。
应用访问限制
分区存储的目标是保护应用和用户数据的隐私。这包括保护用户信息(例如照片元数据)、防止应用在未经明确许可的情况下修改或删除用户文件,以及保护下载到“下载”或其他文件夹的敏感用户文档。
使用分区存储的应用可具有以下访问权限级别(实际访问权限因实现而异)。
- 对自己的文件拥有读取和写入访问权限(没有权限限制)
- 对其他应用的媒体文件拥有读取访问权限(需要具备
READ_EXTERNAL_STORAGE
权限) - 只有在用户直接同意的情况下,才允许对其他应用的媒体文件拥有写入访问权限(系统图库以及符合“所有文件访问权限”获取条件的应用除外)
- 对其他应用的外部应用数据目录没有读取或写入访问权限
将分区存储与 FUSE 搭配使用
Android 11 或更高版本支持用户空间中的文件系统 (FUSE),这使 MediaProvider 模块可以检查用户空间中的文件操作并根据允许、拒绝或隐去访问权限的政策限制对文件的访问。分区存储中使用 FUSE 的应用可获得分区存储的隐私功能以及通过直接文件路径访问文件的功能(让 File API 继续在应用中运行)。
Android 10 对 MediaProvider 执行的文件访问强制实施了分区存储规则,但对直接文件路径访问(例如,使用 File API 和 NDK API)则不实施此类规则,因为拦截内核调用所需的工作量较大。因此,分区存储中的应用无法使用直接文件路径访问文件。此限制影响了应用开发者的适应能力,因为需要大量的代码更改才能将 File API 访问重写为 MediaProvider API 访问。
FUSE 和 SDCardFS
Android 11 对 FUSE 的支持与 SDCardFS 的弃用无关,但它为以前使用 SDCardFS 的设备提供了媒体库替代项。不同的设备:
- 搭载 Android 11 或更高版本并运行内核版本 5.4 或更高版本的设备无法使用 SDCardFS。
- 升级到 Android 11 或更高版本的设备可以基于 SDCardFS 托管 FUSE,以拦截文件操作并实现隐私目标。
FUSE 性能微调
之前,Android 7 或更低版本中支持 FUSE,其中的外部存储空间作为 FUSE 装载。由于这种 FUSE 实现存在性能和死锁问题,Android 8 引入了 SDCardFS。Android 11 使用经过改进且经过更充分测试的 libfuse
实现重新引入了对 FUSE 的支持,可以通过微调来解决 Android 7 或更低版本中的性能问题。
FUSE 微调包括以下调整项:
- 为
Android/data
和Android/obb
目录绕过 FUSE,以提升依赖这些目录的游戏应用的性能。 - 进行优化(例如微调 FUSE 文件系统的预读率和脏比率),以确保出色的读取性能和流畅的媒体播放。
- 使用 FUSE 回写缓存。
- 通过缓存权限来减少系统服务器的 IPC。
- 对具有“所有文件访问权限”的应用进行优化,以加快批量操作的执行速度。
上述调整项可以在 FUSE 设备和非 FUSE 设备之间实现同等的性能。例如,对使用 FUSE 且微调后的 Pixel 2 以及使用媒体库的 Pixel 2 进行测试之后,我们发现文件路径访问和媒体库之间具有同等的顺序读取性能(例如视频播放)。不过,使用 FUSE 时的顺序写入性能稍差,随机读写可能会慢上一倍。
性能测量结果可能因设备和具体用例而异。由于 MediaProvider API 能提供最为一致的性能,因此关注性能的应用开发者应该为其应用使用 MediaProvider API。
减轻与 FUSE 相关的性能影响
只有大量使用存储在外部共享存储设备上的文件的应用才会受到与 FUSE 相关的性能影响。FUSE 会绕过外部专属存储空间(包括 android/data
和 android/obb
目录),而内部存储空间(如 /data/data
,许多应用在这里存储数据以确保其获得加密和保护)不会装载 FUSE。
-
只需少量使用共享外部存储空间的应用通常会与有限的一些文件(通常少于 100 个文件)交互。这些应用受益于现有的对常用读写操作的优化,在 Android 11 中应该不会受到任何与 FUSE 相关的性能影响。
-
大量使用共享外部存储空间的应用通常会执行批量文件操作,例如列出或移除包含 1000 个文件的目录,或者在文件系统中创建或删除包含 100 万个文件的目录。在 Android 11 中,批量文件操作可能会受到 FUSE 的影响,但如果此类应用符合
MANAGE_EXTERNAL_STORAGE
权限的获取条件,它们将从 2020 年 10 月更新中包含的性能优化项中获益。
注意:如果批量操作涉及图片和视频等大型文件,文件 I/O 对文件操作性能的影响可能比 FUSE 性能开销更大。
为了避免 FUSE 性能开销,应用可以将数据存储在外部专属存储空间中,或使用 ContentProvider
类中的批量 API 来绕过 FUSE 并获取性能经过优化的路径。此外,面向 MediaProvider 系统组件的 2020 年 10 月更新还包括针对拥有 MANAGE_EXTERNAL_STORAGE
权限的文件管理器和类似应用(如备份/恢复软件和杀毒软件)的性能优化项。
隐私优势远超性能劣势
在已针对 FUSE 进行微调的设备上,大多数关键用户历程在 Android 10 和 Android 11 之间的性能同等出色。不过,在针对一组文件操作执行基准测试时,Android 11 的性能可能比 Android 10 要差。对于 Android 11 中性能较差的文件访问模式(例如,随机读取或写入),我们建议使用 MediaProvider API 为应用提供非 FUSE 访问模式,这是确保可获取一致的高性能的最佳方案。
注意:虽然 Android 致力于解决影响用户历程的性能降低问题,但我们坚信,分区存储在确保隐私性方面的优势远超其造成的性能降低问题的影响。
MediaProvider 和 FUSE 更新
MediaProvider 系统组件的行为因 Android 版本而异。
-
在 Android 10 及更低版本中,SDCardFS 是文件系统,而 MediaProvider 为文件(如图片、视频和音乐文件等)集合提供了一个接口。应用使用 File API 创建一个文件时,它可以要求 MediaProvider 扫描该文件并将其记录在数据库中。
-
在 Android 11 或更高版本中,SDCardFS 已废弃,MediaProvider 成为了外部存储空间的文件系统处理程序(适用于 FUSE),可使外部存储空间上的文件系统和 MediaProvider 数据库保持一致。作为 FUSE 文件系统的用户空间处理程序,MediaProvider 可以拦截内核调用并确保文件操作的隐私安全。
在 Android 11 及更高版本中,MediaProvider 也是一个可在 Android 版本之外更新的模块化系统组件(一个 Mainline 模块)。这意味着,MediaProvider 中存在的性能、隐私或安全问题可以通过 Google Play 商店或合作伙伴提供的其他机制以无线下载方式提交和修复。FUSE 处理程序牵涉的各类资源均可更新,这使得更新可以修复 FUSE 性能降低问题和 bug。
FUSE 透传
Android 12 支持 FUSE 透传功能,此功能可以最大限度地降低 FUSE 开销,从而实现可媲美直接访问下层文件系统的性能。android12-5.4
、android12-5.10
和 android-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 → 页面缓存/存储空间。
图 1. SDcardFS 用户空间请求
FUSE 用户空间请求
FUSE 最初用于实现存储空间模拟并让应用能够透明地使用内部存储空间或外部 SD 卡。使用 FUSE 会产生一些开销,因为每个用户空间请求都遵循以下路径:用户空间 → VFS → FUSE 驱动程序 → FUSE 守护程序 → VFS → ext4 → 页面缓存/存储空间。
图 2. FUSE 用户空间请求
FUSE 透传请求 {#fuse-passthrough-requests}
大多数文件访问权限是在文件打开时进行检查,还有一些其他权限是在对文件执行读取和写入操作时进行检查。在某些情况下,可能在文件打开时知道发出请求的应用对请求的文件具有完全访问权限,因此系统无需继续将读取和写入请求从 FUSE 驱动程序转发到 FUSE 守护程序(因为这样做只是将数据从一个位置移到另一个位置)。
借助 FUSE 透传功能,负责处理待处理请求的 FUSE 守护程序可以通知 FUSE 驱动程序:可以执行相关操作,并且可将后续所有读取和写入请求直接转发给下层文件系统。如此一来,就可以避免等待用户空间 FUSE 守护程序回复 FUSE 驱动程序请求所产生的额外开销。
下面对 FUSE 请求与 FUSE 透传请求进行了对比。
图 3. FUSE 请求与 FUSE 透传请求
当应用执行 FUSE 文件系统访问时,会发生以下操作:
-
FUSE 驱动程序处理请求并将其加入队列,然后通过
/dev/fuse
文件(FUSE 守护程序无法读取该文件)中的特定连接实例将请求提交给负责处理该 FUSE 文件系统的 FUSE 守护程序。 -
当 FUSE 守护程序收到打开文件的请求时,它会确定 FUSE 透传是否适用于该特定文件。如果适用,守护程序会执行以下操作:
-
将此请求通知 FUSE 驱动程序。
-
使用
FUSE_DEV_IOC_PASSTHROUGH_OPEN
ioctl(必须对打开的/dev/fuse
的文件描述符执行)针对该文件启用 FUSE 透传功能。
-
-
ioctl 接收包含以下内容的数据结构(作为参数):
-
作为透传功能目标的下层文件系统文件的文件描述符。
-
当前正在处理的 FUSE 请求(必须为待处理状态或创建并待处理状态)的唯一标识符。
-
可以留空并打算供未来实现之用的额外字段。
-
-
如果 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 透传功能所需的全部内核和 Android 框架元素都存在,否则更改 persist.sys.fuse.passthrough.enable
不会有任何效果。
如需在不刷写设备的情况下启用/停用 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 透传功能。
注意:如果 logcat 显示了与 FUSE 透传功能相关的错误消息,可能是有部分组件缺失或过时。
Android 12 CTS 包括 CtsStorageTest
测试,内含触发 FUSE 透传的测试。如需手动运行该测试,请使用 atest,如下所示:
atest CtsStorageTest
更快地获得存储统计信息
在早期版本的 Android 中,系统会遍历特定应用拥有的所有文件以测量磁盘使用情况。此手动测量过程可能需要几分钟的计算时间,然后才能在“设置”中向用户显示结果。
此外,清除缓存数据文件的内部算法仅查看所有应用的修改时间。这使得恶意应用可以通过将修改时间设置在遥远的未来以使其不当地拥有高于其他应用的优先级,从而降低整体用户体验。
为了提升这些体验,Android 8.0 会询问是否利用 ext4 文件系统的“配额”支持来几乎即时地返回磁盘使用情况统计信息。此配额功能还可以防止任何单个应用使用超过 90% 的磁盘空间或 50% 的索引节点,从而提高系统的稳定性。
实现
配额功能是 installd
默认实现的一部分。在特定文件系统上启用配额功能后,installd
会自动使用该功能。如果在所测量的块存储设备上未启用或不支持配额功能,则系统将自动且透明地恢复手动计算方式。
如需在特定块存储设备上启用配额支持,请执行以下操作:
- 启用
CONFIG_QUOTA
、CONFIG_QFMT_V2
和CONFIG_QUOTACTL
内核选项。 - 将
quota
选项添加到 fstab 文件中的 userdata 分区:/dev/block/platform/soc/624000.ufshc/by-name/userdata /data ext4 noatime,nosuid,nodev,barrier=1,noauto_da_alloc latemount,wait,check,formattable,fileencryption=ice,quota
您可以在现有设备上安全地启用或停用 fstab
选项。在更改 fstab
选项后的第一次启动过程中,fsmgr
会强制执行 fsck
传递以更新所有配额数据结构,这可能会导致首次启动时间稍长。后续启动不会受到影响。
配额支持仅在 ext4 和 Linux 3.18 或更高版本上进行了测试。如果在其他文件系统或者较旧的内核版本上启用,设备制造商将负责测试和检查统计信息的正确性。
不需要特殊硬件支持。
验证
StorageHostTest
下包含 CTS 测试,它们可使用用于测量磁盘使用情况的公共 API。无论是否启用了配额支持,这些 API 都应返回正确的值。
调试
测试应用通过为空间大小使用唯一的质数来仔细分配磁盘空间区域。调试这些测试时,请使用此质数来确定导致任何差异的原因。例如,如果增量为 11MB 的测试失败,请检查 Utils.useSpace()
方法以查看 11MB blob 是否存储在 getExternalCacheDir()
中。
还有一些可能对调试有用的内部测试,但它们可能需要停用安全检查才能通过:
runtest -x frameworks/base/services/tests/servicestests/ \ src/com/android/server/pm/InstallerTest.java
adb shell /data/nativetest64/installd_utils_test/installd_utils_test
adb shell /data/nativetest64/installd_cache_test/installd_cache_test
adb shell /data/nativetest64/installd_service_test/installd_service_test
为什么要废弃 SDCardFS?
弃用 SDCardFS 有多方面的原因。
稳定性
SDCardFS 存在与大小写区分有关的几个竞态条件,以及一些关于内存不足情况的问题。在大型目录中,不区分大小写的查询可能会相当慢,因为查询必须遍历下层目录来寻找替代大小写。同时访问上层和下层文件系统也会导致问题。
上游对等性
SDCardFS 需要对 VFS 进行额外的修补,才能支持更改绑定装载选项。这类修补导致需要执行额外的操作才能接受上游对这些区域所做的更改。SDCardFS 的功能可由上游组件进行复制,从而解决这一难点。
与 API 的功能对等性
在之前的 Android 版本中,分区存储限制了对特定类型元数据的访问。通过 SDCardFS 直接访问存储空间时,不支持这些分区存储功能。