本文转自android官网,原文地址:https://source.android.google.cn/devices/storage
1、概览
1.1、存储
Android 一直在不断发展,可支持各种存储设备类型和功能。所有 Android 版本均支持配有传统存储(包括便携式存储和内置存储)的设备。便携式存储是指物理介质(如 SD 卡或 USB 设备),用于进行临时数据传输/文件存储。物理介质可以随设备一起保留更长时间,但并非固定在设备上,可以移除。自 Android 1.0 开始,SD 卡已可用作便携式存储;Android 6.0 增加对 USB 的支持。“内置”存储可通过将部分内部存储暴露于模拟层来实现存储,并且从 Android 3.0 开始便已支持此功能。
从 Android 6.0 开始,Android 支持可合并的存储设备,这种存储设备是指可以像内部存储设备那样进行加密和格式化的物理介质(例如 SD 卡或 USB 设备)。移动存储设备可存储各类应用数据。
1.2、权限
采用各种 Android 权限保护对外部存储设备的访问。从 Android 1.0 开始,采用 WRITE_EXTERNAL_STORAGE
权限保护写入访问。从 Android 4.1 开始,采用 READ_EXTERNAL_STORAGE
权限保护读取访问。
从 Android 4.4 开始,外部存储设备上的文件所有者、组和模式根据目录结构合成。这样,应用可在外部存储设备上管理其特定文件包的目录,而无需获得广泛的 WRITE_EXTERNAL_STORAGE
权限。例如,文件包名称为 com.example.foo
的应用现在可以自由访问外部存储设备上的 Android/data/com.example.foo/
,没有权限限制。通过将原始存储设备封装在 FUSE 守护进程中,可实现此类合成权限。
1.3、运行时权限
Android 6.0 引入了一种新的运行时权限模式,在该模式中,应用可在运行时根据需要请求功能。由于新模式包含 READ/WRITE_EXTERNAL_STORAGE
权限,因此平台需要动态授予存储访问权限,而不会终止或重新启动已运行的应用。通过维护所有安装存储设备的三个不同视图可实现该模式:
/mnt/runtime/default
是向无特殊存储权限的应用以及adbd
和其他系统组件所在的根命名空间显示。/mnt/runtime/read
是向具有READ_EXTERNAL_STORAGE
的应用显示/mnt/runtime/write
是向具有WRITE_EXTERNAL_STORAGE
的应用显示
在 Zygote 进行 fork 操作时,我们会为各运行应用创建装载命名空间,并将相应的初始视图挂载到位。稍后,当授予运行时权限时,vold
将跳转到已运行应用的装载命名空间,并将升级后的视图挂载到位。请注意,权限降级定会导致应用被终止。
用于实现此特性的 setns()
功能至少需要运行 Linux 3.8,但补丁程序已反向移植至 Linux 3.4。PermissionsHostTest
CTS 测试可用于验证内核行为是否正确。
在 Android 6.0 中,第三方应用无权访问 sdcard_r
和 sdcard_rw
GID。相反,访问通过仅为该应用装载适当的运行时视图来控制。系统会使用 everybody
GID 来阻止用户间交互。
2、传统存储设备
Android 支持采用传统存储的设备,它被定义为具有不可变 POSIX 权限类和模式且不区分大小写的文件系统。传统存储设备的概念包括模拟和便携式存储设备。便携式存储设备指的是系统未合并的任何外部存储设备,因此,未经格式化、加密或绑定到特定设备。由于传统的外部存储设备对存储的数据提供最低限度的保护,因此系统代码不应将敏感数据存储到外部存储设备中。具体来说,只能将配置和日志文件存储到可为其提供妥善保护的内部存储设备中。
2.1、多用户外部存储设备
从 Android 4.2 开始,设备可以支持多用户,且外部存储设备必须满足以下限制条件:
- 每个用户都必须有各自的独立主要外部存储设备,且不得访问其他用户的主要外部存储设备。
/sdcard
路径必须根据运行进程的用户身份解析到特定于该用户的正确主要外部存储设备。Android/obb
目录中较大的 OBB 文件的存储可作为优化在多个用户之间共享。- 次要外部存储设备不得让应用写入内容,除非在特定于软件包的目录中获得合成的权限。
此功能的默认平台实现利用 Linux 内核命名空间,为每个 Zygote 所派生的进程创建独立的装载表,然后使用绑定装载向私有命名空间提供特定于用户的正确主要外部存储设备。
启动时,系统会在 EMULATED_STORAGE_SOURCE
(隐藏于应用中)装载一个模拟的外部存储设备 FUSE 守护进程。在 Zygote 派生之后,它会将特定于用户的相应子目录从 FUSE 守护进程下绑定装载到 EMULATED_STORAGE_TARGET
,以便外部存储设备路径正确解析应用。由于应用缺少其他用户存储的可访问装载点,它们只能供启动的用户访问存储设备。
该实现还使用共享的子树内核功能将装载事件从默认的根命名空间传播到应用命名空间中,从而确保 ASEC 容器和 OBB 装载等功能继续正常运行。它通过将 rootfs 装载为共享模式,然后在每个 Zygote 命名空间都创建好后重新将其装载为从属模式来实现。
2.2、多个外部存储设备
从 Android 4.4 开始,多个外部存储设备通过 Context.getExternalFilesDirs()
、Context.getExternalCacheDirs()
和 Context.getObbDirs()
提供给开发者。
通过这些 API 提供的外部存储设备必须是设备的半永久部件(如电池盒中的 SD 卡插槽)。开发者希望存储在这些位置的数据可供长期使用。因此,瞬态存储设备(如 USB 大容量存储驱动器)不应通过这些 API 提供。
WRITE_EXTERNAL_STORAGE
权限必须仅向在设备上的主要外部存储设备授予写入权限。不允许应用写入次要外部存储设备,除非在特定于软件包的目录中获得合成的权限。以这种方式限制写入可确保系统在应用被卸载时将文件清理干净。
2.3、USB 媒体支持
Android 6.0 支持只需短时间内连接到设备的便携式存储设备,如 U 盘。当用户插入新的便携式设备时,该平台会显示一条通知,以让用户复制或管理相应设备上的内容。
在 Android 6.0 中,任何未合并的设备均被视为便携式设备。由于便携式存储设备只能短时间连接到设备,因此 Android 平台会避免执行媒体扫描之类的繁重操作。第三方应用必须通过存储访问框架与便携式存储设备中的文件进行交互;出于隐私和安全考虑,明确禁止直接访问。
3、可合并的存储设备
Android 一直都支持外部存储配件(如 SD 卡),但由于这些配件存在预期的无常性,以及传统外部存储设备只受最低限度的数据保护,因此这些配件一直以来仅限于进行简单的文件存储。Android 6.0 推出了合并外部存储媒介(使其可以像内部存储设备一样使用)的功能。
注意:在运行 Android 7.0-8.1 的设备上,文件级加密 (FBE) 无法用于可合并的存储设备。在使用 FBE 的设备上,必须将新添加的存储媒介(例如 SD 卡)用作传统存储设备。
运行 Android 9 及更高版本的设备可以使用可合并的存储设备和 FBE。
当合并外部存储媒介时,系统将对其进行格式化和加密处理,以便一次只在一台 Android 设备上使用。由于该媒介与合并它的 Android 设备紧密关联,因此可以安全地为所有用户存储应用和私密数据。
当用户将新的存储媒介(如 SD卡)插入到可合并的位置时,Android 会询问他们想要如何使用该媒介。他们可以选择合并该媒介,这样的话,系统会对该媒介进行格式化和加密处理,或者也可以继续按原样将其用于简单的文件存储。如果他们选择合并媒介,平台会询问是否将主要共享存储内容(通常装载在 /sdcard
上)迁移到新合并的媒介上,从而腾出宝贵的内部存储空间。不同于因使用 MBR 而限制为 2TB 的传统存储设备,可合并的存储设备使用 GPT,因而文件存储空间上限约为 9ZB。
只有当开发者通过 android:installLocation
属性指示提供支持时,才能将应用放置在合并的存储媒介上。新安装的受支持的应用将自动放置在具有最多可用空间的存储设备上,用户可以在“设置”应用中在存储设备之间移动支持的应用。移动到已合并媒介的应用在媒介弹出时被记住,并在重新插入媒介时返回弹出前的状态。
3.2、安全性
平台为每个合并的设备随机生成加密密钥,该密钥存储在 Android 设备的内部存储设备上。这样可以有效地使得合并的媒介与内部存储设备一样安全。密钥与合并的设备(基于合并的分区 GUID)相关联。合并的设备使用通过 aes-cbc-essiv:sha256
算法和 128 位密钥大小配置的 dm-crypt
进行加密。
合并设备的磁盘布局紧密对应内部数据分区,包括 SELinux 标签等。当在 Android 设备上支持多用户时,合并的存储设备也通过与内部存储设备相同的隔离级别支持多用户。
由于合并的存储设备的内容与合并该设备的 Android 设备密切相关,加密密钥不应可以从父设备中进行提取,因此该存储设备无法装载到其他位置。
内容模式的默认加密算法是 aes-256-xts
,而文件名的默认加密算法是 aes-256-heh
。您可以通过分别更改属性 ro.crypto.volume.contents_mode
和 ro.crypto.volume.filenames_mode
的值(更改方式为在 device.mk
中设置 PRODUCT_PROPERTY_OVERRIDES
)来更改这些设置。
如果您的内核不支持 HEH 文件名加密,您可以通过将以下内容添加到 device.mk 来改用 CTS 模式:
PRODUCT_PROPERTY_OVERRIDES += \
ro.crypto.volume.filenames_mode=aes-256-cts
3.4、性能和稳定性
应该只考虑合并位于稳定位置(如电池盒内或防护盖后面的插槽)的外部存储媒介,以避免意外的数据丢失或损坏。尤其是,绝不应该考虑合并连接到手机或平板电脑的 USB 设备。一种常见的例外情况是连接到电视类设备的外部 U 盘,因为整个电视机通常安装在一个稳定的位置。
当用户合并新的存储设备时,平台将运行基准测试,并将其性能与内部存储设备进行比较。如果所合并设备的速度明显慢于内部存储设备,则平台将警告用户体验可能会受到影响。此基准根据常用 Android 应用的实际 I/O 行为得出。目前,AOSP 实现只会在超出单个阈值时警告用户,但是设备制造商可以进一步做出调整,例如如果存储卡运行非常缓慢,则完全拒绝合并。
合并的设备必须使用支持 POSIX 权限和扩展属性(如 ext4
或 f2fs
)的文件系统进行格式化。为了获得最佳性能,建议基于闪存的存储设备使用 f2fs
文件系统。
在执行周期性空闲维护时,平台将向合并的媒介发出 FI_TRIM
(就像对待内部存储设备那样)。目前的 SD 卡规范不支持 DISCARD
命令;不过,内核会回退到使用 ERASE
命令,SD 卡固件可以选择使用该命令来实现优化目的。
3.5、修正双重加密
在 Android 8.x 及更低版本中,可合并的存储设备不支持 FBE。带有可合并的存储设备的所有现有设备都使用全盘加密(FDE)。在 Android 9 中,可合并的存储设备支持 FBE。但在默认情况下,文件内容已进行双重加密,因为可合并的存储设备具有 FDE 和 FBE 层。默认情况下,这两个层都会加密文件内容,这会降低设备性能。要解决双重加密问题并提高设备性能,请执行以下操作:
- 将这些补丁程序添加到内核中。
- 要使用
vold
传达此项更改,请将以下内容添加到device.mk
中:
PRODUCT_PROPERTY_OVERRIDES += ro.crypto.allow_encrypt_override=true
如果您设置了此项内容,但内核修补程序不存在,则可合并的存储设备将无法工作,并且 vold
日志将包含一条错误消息(提示它无法创建 dm 设备)。
注意:请勿使用 OTA 更新更改此标记,因为这会更改可合并的存储设备的磁盘格式。
3.6、测试
要测试可合并的存储设备是否正常工作,请运行此 CTS 测试:
cts-tradefed run commandAndExit cts-dev \-m CtsAppSecurityHostTestCases \-t android.appsecurity.cts.AdoptableHostTest
要在设备没有内置插槽或正使用 USB 连接器实现有效的 adb 连接时验证 U 盘和 SD 卡的行为,请使用:
adb shell sm set-virtual-disk true
4、设备配置
外部存储空间由 vold
init 服务和 StorageManagerService
系统服务共同管理。外部实体存储卷的装载由 vold
处理:通过执行分阶段操作准备好媒体,然后再将其提供给应用。
注意:在 Android 8.0 中,MountService
类已更名为 StorageManagerService
。
4.1、文件映射
对于 Android 4.2.2 及更早版本,特定于设备的 vold.fstab
配置文件定义从 sysfs 设备到文件系统装载点的映射,每行都遵循以下格式:
dev_mount <label> <mount_point> <partition> <sysfs_path> [flags]
label
:卷的标签。mount_point
:要装载卷的文件系统路径。partition
:分区编号(从 1 开始);如果是第一个可用分区,则为“auto”。sysfs_path
:可以提供此装载点的设备的一个或多个 sysfs 路径。这些路径用空格分开,且必须都以/
开头。flags
:可选的逗号分隔标记列表,不能包含/
。可能的值包括nonremovable
和encryptable
。
对于 Android 4.3 及更高版本,init、vold 和 recovery 所使用的各种 fstab 文件在 /fstab.<device>
文件中进行统一。对于由 vold
管理的外部存储卷,条目应采用以下格式:
<src> <mnt_point> <type> <mnt_flags> <fs_mgr_flags>
src
:sysfs(通常在 /sys 下装载)下可以提供装载点的设备的路径。路径必须以/
开头。mount_point
:要装载卷的文件系统路径。type
:卷上的文件系统类型。如果是外部卡,则通常为vfat
。mnt_flags
:Vold
会忽略此字段,应将其设置为defaults
fs_mgr_flags
:Vold
会忽略此字段中不包含voldmanaged=
标记的统一的 fstab 中的任何行。该标记必须后跟描述卡的标签,以及分区号或字词auto
。例如:voldmanaged=sdcard:auto
。其他可能的标记有nonremovable
、encryptable=sdcard
、noemulatedsd
和encryptable=userdata
。
4.2、配置详情
框架层级以及更高层级的外部存储交互通过 StorageManagerService
来处理。由于 Android 6.0 中进行了配置更改(例如移除了 storage_list.xml 资源叠加层),因此配置详情分成了两类。
4.2.1、Android 5.x 及更低版本
设备专属的 storage_list.xml
配置文件(通常通过 frameworks/base
叠加层提供)定义存储设备的属性和限制。<StorageList>
元素包含一个或多个 <storage>
元素,其中一个元素应被标记为主元素。<storage>
属性包括:
mountPoint
:此装载的文件系统路径。storageDescription
:描述此装载的字符串资源。primary
:如果此装载是主要外部存储,则为 true。removable
:如果此装载包含可移动媒体(如物理 SD 卡),则为 true。emulated
:如果此装载由可能使用 FUSE 守护进程的内部存储模拟和支持,则为 true。mtp-reserve
:MTP 应为免费存储预留的存储 MB 数。仅在装载被标记为模拟时使用。allowMassStorage
:如果此装载可通过 USB 大容量存储设备共享,则为 true。maxFileSize
:最大文件大小(以 MB 为单位)。
设备可以通过模拟由内部存储支持的文件系统(不区分大小写,无需权限)来提供外部存储。system/core/sdcard
中的 FUSE 守护进程提供一个可能的实现,可添加为特定于设备的 init.rc
服务:
# virtual sdcard daemon running as media_rw (1023)
service sdcard /system/bin/sdcard <source_path> <dest_path> 1023 1023class late_start
其中,source_path
为提供支持的内部存储,dest_path
为目标装载点。
配置特定于设备的 init.rc
脚本时,必须将 EXTERNAL_STORAGE
环境变量定义为主要外部存储的路径。/sdcard
路径也必须通过符号链接解析到同一位置。如果设备在平台更新之间调整外部存储的位置,则应创建符号链接,以便旧的路径继续发挥作用。
4.2.2、Android 6.0及以上
目前,存储子系统的配置集中在特定于设备的 fstab
文件中,并且移除了一些历史静态配置文件/变量,以支持更多动态行为:
storage_list.xml
资源叠加层已被移除,框架已不再使用该叠加层。现在,存储设备在被vold
检测到时动态配置。EMULATED_STORAGE_SOURCE/TARGET
环境变量已被移除,Zygote 已不再使用这些变量来配置特定于用户的装载点。相反,用户分离现在由特定于用户的 GID 强制执行,主要共享存储由vold
在运行时装载到位。- 开发者可以根据其使用情形继续动态或静态构建路径。在路径中包含 UUID 可识别每个卡,以便为开发者提供更清晰的位置。(例如,
/storage/ABCD-1234/report.txt
明显是与/storage/DCBA-4321/report.txt
不同的文件。)
- 开发者可以根据其使用情形继续动态或静态构建路径。在路径中包含 UUID 可识别每个卡,以便为开发者提供更清晰的位置。(例如,
- 硬编码的 FUSE 服务已从特定于设备的
init.rc
文件中移除,在需要时将从vold
动态派生。
除了这些配置更改之外,Android 6.0 还包含可合并的存储设备的概念。对于 Android 6.0 设备,任何未被合并的物理媒体都被视为便携式设备。
可合并的存储设备
要在 fstab
中表示可合并的存储设备,请在 fs_mgr_flags
字段中使用 encryptable=userdata
属性。典型定义如下:
/devices/platform/mtk-msdc.1/mmc_host* auto auto defaults
voldmanaged=sdcard1:auto,encryptable=userdata
合并存储设备时,该平台会擦除内容并写入定义两个分区的 GUID 分区表:
- 一个较小的空
android_meta
分区,预留以备将来使用的。分区类型 GUID 为 19A710A2-B3CA-11E4-B026-10604B889DCF。 - 一个较大的
android_ext
分区,使用 dm-crypt 加密并使用ext4
或f2fs
(取决于内核功能)格式化。分区类型 GUID 为 193D1EA4-B3CA-11E4-B075-10604B889DCF。
便携式存储设备
在 fstab
中,具有 voldmanaged
属性的存储设备默认被视为便携式设备,除非定义了其他属性(如 encryptable=userdata
)。例如,典型的 USB OTG 设备的定义如下:
/devices/*/xhci-hcd.0.auto/usb* auto auto defaultsvoldmanaged=usb:auto
该平台在装载之前使用 blkid
检测文件系统类型,用户可以选择在文件系统不受支持时将媒体格式化。
5、更快地获得存储统计信息
在早期版本的 Android 中,系统会遍历特定应用拥有的所有文件以测量磁盘使用情况。此手动测量过程可能需要几分钟的计算时间,然后才能在“设置”中向用户显示结果。
此外,清除缓存数据文件的内部算法仅查看所有应用的修改时间。这使得恶意应用可以通过将修改时间设置在遥远的未来以使其不当地拥有高于其他应用的优先级,从而降低整体用户体验。
为了提升这些体验,Android 8.0 会询问是否利用 ext4 文件系统的“配额”支持来几乎即时地返回磁盘使用情况统计信息。此配额功能还可以防止任何单个应用使用超过 90% 的磁盘空间或 50% 的索引节点,从而提高系统的稳定性。
5.1、实现
配额功能是 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 或更高版本上进行了测试。如果在其他文件系统或者较旧的内核版本上启用,设备制造商将负责测试和检查统计信息的正确性。
不需要特殊硬件支持。
5.2、验证
StorageHostTest
下包含 CTS 测试,它们可使用用于测量磁盘使用情况的公共 API。无论是否启用了配额支持,这些 API 都应返回正确的值。
5.3、调试
测试应用通过为空间大小使用唯一的质数来仔细分配磁盘空间区域。调试这些测试时,请使用此质数来确定任何差异的原因。例如,如果增量为 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