实施动态分区

动态分区是使用 Linux 内核中的 dm-linear 设备映射器模块实现的。super 分区包含元数据,其中列出了 super 中每个动态分区的名称和块范围。在第一阶段 init 期间,系统会对该元数据进行解析和验证,并创建虚拟块设备来表示每个动态分区。

应用 OTA 时,系统会自动根据需要创建、调整大小或删除动态分区。对于 A/B 设备,元数据有两份副本,更改仅应用于代表目标槽位的副本。

由于动态分区是在用户空间中实现的,因此引导加载程序所需的分区无法设为动态分区。例如,引导加载程序会读取 bootdtbovbmeta,因此这些分区必须保留为物理分区。

每个动态分区都可以属于一个更新组。这些组会限制该组中的分区可以占用的最大空间。例如,systemvendor 可以属于一个组,该组会限制 systemvendor 的总大小。

在新设备上实现动态分区

本部分详细介绍了如何在搭载 Android 10 及更高版本的新设备上实现动态分区。要更新现有设备,请参阅升级 Android 设备

分区变更

对于搭载 Android 10 的设备,请创建一个名为 super 的分区。super 分区在内部处理 A/B 槽位,因此 A/B 设备不需要单独的 super_asuper_b 分区。所有未被引导加载程序使用的只读 AOSP 分区都必须是动态分区,并且必须从 GUID 分区表 (GPT) 中移除。供应商专用分区不必是动态分区,可以放置在 GPT 中。

要估算 super 的大小,请将要从 GPT 中删除的分区的大小相加。对于 A/B 设备,这应包括两个槽位的大小。图 1 显示了转换为动态分区之前和之后的示例分区表。

Partition table layout
图 1. 转换为动态分区时的新物理分区表布局

受支持的动态分区包括:

  • System
  • Vendor
  • Product
  • System Ext
  • ODM

对于搭载 Android 10 的设备,内核命令行选项 androidboot.super_partition 必须为空,以便命令系统属性 ro.boot.super_partition 为空。

分区对齐

如果 super 分区未正确对齐,则设备映射器模块的运行效率可能会降低。super 分区必须与块层确定的最小 I/O 请求大小对齐。默认情况下,构建系统(通过 lpmake 生成 super 分区映像)假定 1 MiB 对齐足以满足每个动态分区的需求。但是,供应商应确保 super 分区已正确对齐。

您可以通过检查 sysfs 来确定块设备的最小请求大小。例如:

# ls -l /dev/block/by-name/super
lrwxrwxrwx 1 root root 16 1970-04-05 01:41 /dev/block/by-name/super -> /dev/block/sda17
# cat /sys/block/sda/queue/minimum_io_size
786432

您可以采用类似的方式验证 super 分区的对齐方式:

# cat /sys/block/sda/sda17/alignment_offset

对齐偏移量必须为 0。

设备配置变更

要启用动态分区,请在 device.mk 中添加以下标志:

PRODUCT_USE_DYNAMIC_PARTITIONS := true

主板配置变更

您需要设置 super 分区的大小:

BOARD_SUPER_PARTITION_SIZE := <size-in-bytes>

在 A/B 设备上,如果动态分区映像的总大小超过 super 分区大小的一半,构建系统会抛出错误。

您可以按如下方式配置动态分区列表。对于使用更新组的设备,请在 BOARD_SUPER_PARTITION_GROUPS 变量中列出这些组。然后,每个组名称都具有 BOARD_group_SIZEBOARD_group_PARTITION_LIST 变量。对于 A/B 设备,组的最大大小应仅涵盖一个槽位,因为组名称在内部带有槽位后缀。

以下示例设备将所有分区放入名为 example_dynamic_partitions 的组中:

BOARD_SUPER_PARTITION_GROUPS := example_dynamic_partitions
BOARD_EXAMPLE_DYNAMIC_PARTITIONS_SIZE := 6442450944
BOARD_EXAMPLE_DYNAMIC_PARTITIONS_PARTITION_LIST := system vendor product

以下示例设备将 system 和 product 服务放入 group_foo,并将 vendorproductodm 放入 group_bar

BOARD_SUPER_PARTITION_GROUPS := group_foo group_bar
BOARD_GROUP_FOO_SIZE := 4831838208
BOARD_GROUP_FOO_PARTITION_LIST := system product_services
BOARD_GROUP_BAR_SIZE := 1610612736
BOARD_GROUP_BAR_PARTITION_LIST := vendor product odm
  • 对于 Virtual A/B 启动设备,所有组的最大大小之和必须至多为:
    BOARD_SUPER_PARTITION_SIZE - 开销
    请参阅实现 Virtual A/B
  • 对于 A/B 启动设备,所有组的最大大小之和必须为:
    BOARD_SUPER_PARTITION_SIZE / 2 - 开销
  • 对于非 A/B 设备和改造 A/B 设备,所有组的最大大小之和必须为:
    BOARD_SUPER_PARTITION_SIZE - 开销
  • 在构建时,更新组中每个分区的映像大小之和不得超过该组的最大大小。
  • 开销是计算中所需的,用于考虑元数据、对齐方式等。合理的开销为 4 MiB,但您可以根据设备需要选择更大的开销。

调整动态分区大小

在动态分区之前,分区大小被过度分配,以确保它们有足够的空间用于未来的更新。实际大小按原样采用,大多数只读分区的文件系统中都有一些可用空间。在动态分区中,该可用空间不可用,可用于在 OTA 期间增大分区。务必确保分区未浪费空间,并且分配的大小尽可能小。

对于只读 ext4 映像,如果未指定硬编码分区大小,则构建系统会自动分配最小大小。构建系统会调整映像的大小,使文件系统的未使用空间尽可能少。这可确保设备不会浪费可用于 OTA 的空间。

此外,可以通过启用块级重复数据删除来进一步压缩 ext4 映像。要启用此功能,请使用以下配置:

BOARD_EXT4_SHARE_DUP_BLOCKS := true

如果不需要自动分配分区最小大小,则有两种方法可以控制分区大小。您可以指定最小可用空间,使用 BOARD_partitionIMAGE_PARTITION_RESERVED_SIZE;或者您可以指定 BOARD_partitionIMAGE_PARTITION_SIZE 以强制动态分区达到特定大小。除非必要,否则不建议使用这两种方法。

例如:

BOARD_PRODUCTIMAGE_PARTITION_RESERVED_SIZE := 52428800

这会强制 product.img 中的文件系统具有 50 MiB 的未使用空间。

System-as-root 变更

搭载 Android 10 的设备不得使用 system-as-root。

具有动态分区(无论是启动时还是改造动态分区)的设备不得使用 system-as-root。Linux 内核无法解释 super 分区,因此无法自行挂载 systemsystem 现在由位于 ramdisk 中的第一阶段 init 挂载。

请勿设置 BOARD_BUILD_SYSTEM_ROOT_IMAGE。在 Android 10 中,BOARD_BUILD_SYSTEM_ROOT_IMAGE 标志仅用于区分系统是由内核挂载还是由 ramdisk 中的第一阶段 init 挂载。

PRODUCT_USE_DYNAMIC_PARTITIONS 也为 true 时,将 BOARD_BUILD_SYSTEM_ROOT_IMAGE 设置为 true 会导致构建错误。

BOARD_USES_RECOVERY_AS_BOOT 设置为 true 时,恢复映像构建为 boot.img,其中包含恢复 ramdisk。以前,引导加载程序使用 skip_initramfs 内核命令行参数来决定要启动进入哪种模式。对于 Android 10 设备,引导加载程序不得将 skip_initramfs 传递到内核命令行。相反,引导加载程序应传递 androidboot.force_normal_boot=1 以跳过恢复并启动正常的 Android。搭载 Android 12 或更高版本的设备必须使用 bootconfig 来传递 androidboot.force_normal_boot=1

AVB 配置变更

使用 Android 验证启动 2.0 时,如果设备未使用链接分区描述符,则无需进行任何更改。但是,如果使用链接分区,并且其中一个经过验证的分区是动态分区,则需要进行更改。

以下是设备链接 systemvendor 分区的 vbmeta 的示例配置。

BOARD_AVB_SYSTEM_KEY_PATH := external/avb/test/data/testkey_rsa2048.pem
BOARD_AVB_SYSTEM_ALGORITHM := SHA256_RSA2048
BOARD_AVB_SYSTEM_ROLLBACK_INDEX := $(PLATFORM_SECURITY_PATCH_TIMESTAMP)
BOARD_AVB_SYSTEM_ROLLBACK_INDEX_LOCATION := 1

BOARD_AVB_VENDOR_KEY_PATH := external/avb/test/data/testkey_rsa2048.pem
BOARD_AVB_VENDOR_ALGORITHM := SHA256_RSA2048
BOARD_AVB_VENDOR_ROLLBACK_INDEX := $(PLATFORM_SECURITY_PATCH_TIMESTAMP)
BOARD_AVB_VENDOR_ROLLBACK_INDEX_LOCATION := 1

使用此配置,引导加载程序希望在 systemvendor 分区的末尾找到 vbmeta 页脚。由于这些分区对引导加载程序不再可见(它们位于 super 中),因此需要进行两项更改。

  • vbmeta_systemvbmeta_vendor 分区添加到设备的分区表。对于 A/B 设备,添加 vbmeta_system_avbmeta_system_bvbmeta_vendor_avbmeta_vendor_b。如果添加这些分区中的一个或多个,它们的大小应与 vbmeta 分区的大小相同。
  • 通过添加 VBMETA_ 重命名配置标志,并指定链接扩展到哪些分区:
    BOARD_AVB_VBMETA_SYSTEM := system
    BOARD_AVB_VBMETA_SYSTEM_KEY_PATH := external/avb/test/data/testkey_rsa2048.pem
    BOARD_AVB_VBMETA_SYSTEM_ALGORITHM := SHA256_RSA2048
    BOARD_AVB_VBMETA_SYSTEM_ROLLBACK_INDEX := $(PLATFORM_SECURITY_PATCH_TIMESTAMP)
    BOARD_AVB_VBMETA_SYSTEM_ROLLBACK_INDEX_LOCATION := 1
    
    BOARD_AVB_VBMETA_VENDOR := vendor
    BOARD_AVB_VBMETA_VENDOR_KEY_PATH := external/avb/test/data/testkey_rsa2048.pem
    BOARD_AVB_VBMETA_VENDOR_ALGORITHM := SHA256_RSA2048
    BOARD_AVB_VBMETA_VENDOR_ROLLBACK_INDEX := $(PLATFORM_SECURITY_PATCH_TIMESTAMP)
    BOARD_AVB_VBMETA_VENDOR_ROLLBACK_INDEX_LOCATION := 1

设备可以使用这些分区中的一个、两个或全部都不使用。仅当链接到逻辑分区时才需要进行更改。

AVB 引导加载程序变更

如果引导加载程序嵌入了 libavb,请包含以下补丁:

如果使用链接分区,请包含其他补丁:

  • 49936b4c0109411fdd38bd4ba3a32a01c40439a9 — “libavb:支持分区开头的 vbmeta Blob。”

内核命令行变更

必须将新参数 androidboot.boot_devices 添加到内核命令行。init 使用此参数来启用 /dev/block/by-name 符号链接。它应该是 ueventd 创建的底层按名称符号链接的设备路径组件,即 /dev/block/platform/device-path/by-name/partition-name。搭载 Android 12 或更高版本的设备必须使用 bootconfig 将 androidboot.boot_devices 传递给 init

例如,如果按名称的超级分区符号链接是 /dev/block/platform/soc/100000.ufshc/by-name/super,您可以在 BoardConfig.mk 文件中添加命令行参数,如下所示:

BOARD_KERNEL_CMDLINE += androidboot.boot_devices=soc/100000.ufshc
您可以在 BoardConfig.mk 文件中添加 bootconfig 参数,如下所示:
BOARD_BOOTCONFIG += androidboot.boot_devices=soc/100000.ufshc

fstab 变更

设备树和设备树叠加层不得包含 fstab 条目。请使用将成为 ramdisk 一部分的 fstab 文件。

必须对逻辑分区的 fstab 文件进行更改:

  • fs_mgr 标志字段必须包含 logical 标志和 Android 10 中引入的 first_stage_mount 标志,后者表示要在第一阶段挂载分区。
  • 分区可以将 avb=vbmeta partition name 指定为 fs_mgr 标志,然后指定的 vbmeta 分区由第一阶段 init 初始化,然后再尝试挂载任何设备。
  • dev 字段必须是分区名称。

以下 fstab 条目根据上述规则将 system、vendor 和 product 设置为逻辑分区。

#<dev>  <mnt_point> <type>  <mnt_flags options> <fs_mgr_flags>
system   /system     ext4    ro,barrier=1        wait,slotselect,avb=vbmeta,logical,first_stage_mount
vendor   /vendor     ext4    ro,barrier=1        wait,slotselect,avb,logical,first_stage_mount
product  /product    ext4    ro,barrier=1        wait,slotselect,avb,logical,first_stage_mount

将 fstab 文件复制到第一阶段 ramdisk 中。

SELinux 变更

超级分区块设备必须标记为 super_block_device 标签。例如,如果按名称的超级分区符号链接是 /dev/block/platform/soc/100000.ufshc/by-name/super,请将以下行添加到 file_contexts

/dev/block/platform/soc/10000\.ufshc/by-name/super   u:object_r:super_block_device:s0

fastbootd

引导加载程序(或任何非用户空间刷写工具)不理解动态分区,因此无法刷写它们。为了解决这个问题,设备必须使用 fastboot 协议的用户空间实现,称为 fastbootd。

如需详细了解如何实现 fastbootd,请参阅将 Fastboot 移至用户空间

adb remount

对于使用 eng 或 userdebug 构建版本的开发者,adb remount 对于快速迭代非常有用。动态分区给 adb remount 带来了一个问题,因为每个文件系统内不再有可用空间。为了解决这个问题,设备可以启用 overlayfs。只要超级分区内有可用空间,adb remount 就会自动创建一个临时动态分区,并使用 overlayfs 进行写入。临时分区名为 scratch,因此请勿将此名称用于其他分区。

如需详细了解如何启用 overlayfs,请参阅 AOSP 中的 overlayfs README

升级 Android 设备

如果您将设备升级到 Android 10,并且想要在 OTA 中包含动态分区支持,则无需更改内置分区表。需要进行一些额外的配置。

设备配置变更

要改造动态分区,请在 device.mk 中添加以下标志:

PRODUCT_USE_DYNAMIC_PARTITIONS := true
PRODUCT_RETROFIT_DYNAMIC_PARTITIONS := true

主板配置变更

您需要设置以下主板变量:

  • BOARD_SUPER_PARTITION_BLOCK_DEVICES 设置为用于存储动态分区区段的块设备列表。这是设备上现有物理分区的名称列表。
  • BOARD_SUPER_PARTITION_partition_DEVICE_SIZE 分别设置为 BOARD_SUPER_PARTITION_BLOCK_DEVICES 中每个块设备的大小。这是设备上现有物理分区的大小列表。这通常是现有主板配置中的 BOARD_partitionIMAGE_PARTITION_SIZE
  • 取消设置 BOARD_SUPER_PARTITION_BLOCK_DEVICES 中所有分区的现有 BOARD_partitionIMAGE_PARTITION_SIZE
  • BOARD_SUPER_PARTITION_SIZE 设置为 BOARD_SUPER_PARTITION_partition_DEVICE_SIZE 的总和。
  • BOARD_SUPER_PARTITION_METADATA_DEVICE 设置为存储动态分区元数据的块设备。它必须是 BOARD_SUPER_PARTITION_BLOCK_DEVICES 之一。通常,这设置为 system
  • 分别设置 BOARD_SUPER_PARTITION_GROUPSBOARD_group_SIZEBOARD_group_PARTITION_LIST。有关详细信息,请参阅新设备上的主板配置变更

例如,如果设备已经具有 system 和 vendor 分区,并且您想要将它们转换为动态分区并在更新期间添加新的 product 分区,请设置此主板配置:

BOARD_SUPER_PARTITION_BLOCK_DEVICES := system vendor
BOARD_SUPER_PARTITION_METADATA_DEVICE := system

# Rename BOARD_SYSTEMIMAGE_PARTITION_SIZE to BOARD_SUPER_PARTITION_SYSTEM_DEVICE_SIZE.
BOARD_SUPER_PARTITION_SYSTEM_DEVICE_SIZE := <size-in-bytes>

# Rename BOARD_VENDORIMAGE_PARTITION_SIZE to BOARD_SUPER_PARTITION_VENDOR_DEVICE_SIZE
BOARD_SUPER_PARTITION_VENDOR_DEVICE_SIZE := <size-in-bytes>

# This is BOARD_SUPER_PARTITION_SYSTEM_DEVICE_SIZE + BOARD_SUPER_PARTITION_VENDOR_DEVICE_SIZE
BOARD_SUPER_PARTITION_SIZE := <size-in-bytes>

# Configuration for dynamic partitions. For example:
BOARD_SUPER_PARTITION_GROUPS := group_foo
BOARD_GROUP_FOO_SIZE := <size-in-bytes>
BOARD_GROUP_FOO_PARTITION_LIST := system vendor product

SELinux 变更

超级分区块设备必须标记有属性 super_block_device_type。例如,如果设备已经具有 systemvendor 分区,您想要将它们用作块设备来存储动态分区的区段,并且它们的按名称符号链接标记为 system_block_device

/dev/block/platform/soc/10000\.ufshc/by-name/system   u:object_r:system_block_device:s0
/dev/block/platform/soc/10000\.ufshc/by-name/vendor   u:object_r:system_block_device:s0

然后,将以下行添加到 device.te

typeattribute system_block_device super_block_device_type;

对于其他配置,请参阅在新设备上实现动态分区

有关改造更新的更多信息,请参阅没有动态分区的 A/B 设备的 OTA

工厂映像

对于启动时支持动态分区的设备,请避免使用用户空间 fastboot 来刷写工厂映像,因为启动到用户空间比其他刷写方法慢。

为了解决这个问题,make dist 现在构建了一个额外的 super.img 映像,可以直接刷写到超级分区。它会自动捆绑逻辑分区的内容,这意味着它包含 system.imgvendor.img 等,以及 super 分区元数据。此映像可以直接刷写到 super 分区,而无需任何其他工具或使用 fastbootd。构建完成后,super.img 将放置在 ${ANDROID_PRODUCT_OUT} 中。

对于启动时具有动态分区的 A/B 设备,super.img 包含 A 槽位中的映像。直接刷写超级映像后,请在重启设备之前将槽位 A 标记为可启动。

对于改造设备,当 BOARD_SUPER_PARTITION_BLOCK_DEVICES 是 system vendor 时,make dist 构建一组 super_*.img 映像,可以直接刷写到相应的物理分区。例如,make dist 构建 super_system.imgsuper_vendor.img。这些映像放置在 target_files.zip 的 OTA 文件夹中。

设备映射器存储设备调整

动态分区容纳许多不确定的设备映射器对象。这些对象可能并非都按预期实例化,因此您必须跟踪所有挂载,并使用其底层存储设备更新所有关联分区的 Android 属性。

init 内的机制跟踪挂载并异步更新 Android 属性。完成此操作所需的时间无法保证在特定时间段内,因此您必须为所有 on property 触发器提供足够的反应时间。属性为 dev.mnt.blk.<partition>,其中 <partition>rootsystemdatavendor 等。每个属性都与基本存储设备名称相关联,如以下示例所示:

taimen:/ % getprop | grep dev.mnt.blk
[dev.mnt.blk.data]: [sda]
[dev.mnt.blk.firmware]: [sde]
[dev.mnt.blk.metadata]: [sde]
[dev.mnt.blk.persist]: [sda]
[dev.mnt.blk.root]: [dm-0]
[dev.mnt.blk.vendor]: [dm-1]

blueline:/ $ getprop | grep dev.mnt.blk
[dev.mnt.blk.data]: [dm-4]
[dev.mnt.blk.metadata]: [sda]
[dev.mnt.blk.mnt.scratch]: [sda]
[dev.mnt.blk.mnt.vendor.persist]: [sdf]
[dev.mnt.blk.product]: [dm-2]
[dev.mnt.blk.root]: [dm-0]
[dev.mnt.blk.system_ext]: [dm-3]
[dev.mnt.blk.vendor]: [dm-1]
[dev.mnt.blk.vendor.firmware_mnt]: [sda]

init.rc 语言允许将 Android 属性扩展为规则的一部分,并且平台可以根据需要使用如下命令调整存储设备:

write /sys/block/${dev.mnt.blk.root}/queue/read_ahead_kb 128
write /sys/block/${dev.mnt.blk.data}/queue/read_ahead_kb 128

一旦命令处理在第二阶段 init 中开始,epoll loop 就会变为活动状态,并且值开始更新。但是,由于属性触发器在 late-init 之前处于非活动状态,因此它们无法在初始启动阶段用于处理 rootsystemvendor。您可能希望内核默认 read_ahead_kb 足以满足需求,直到 init.rc 脚本可以在 early-fs 中覆盖(当各种守护程序和工具开始运行时)。因此,Google 建议您使用 on property 功能,并结合 init.rc 控制的属性(如 sys.read_ahead_kb)来处理操作的计时并防止竞争情况,如以下示例所示:

on property:dev.mnt.blk.root=* && property:sys.read_ahead_kb=*
    write /sys/block/${dev.mnt.blk.root}/queue/read_ahead_kb ${sys.read_ahead_kb:-2048}

on property:dev.mnt.blk.system=* && property:sys.read_ahead_kb=*
    write /sys/block/${dev.mnt.blk.system}/queue/read_ahead_kb ${sys.read_ahead_kb:-2048}

on property:dev.mnt.blk.vendor=* && property:sys.read_ahead_kb=*
    write /sys/block/${dev.mnt.blk.vendor}/queue/read_ahead_kb ${sys.read_ahead_kb:-2048}

on property:dev.mnt.blk.product=* && property:sys.read_ahead_kb=*
    write /sys/block/${dev.mnt.blk.system_ext}/queue/read_ahead_kb ${sys.read_ahead_kb:-2048}

on property:dev.mnt.blk.oem=* && property:sys.read_ahead_kb=*
    write /sys/block/${dev.mnt.blk.oem}/queue/read_ahead_kb ${sys.read_ahead_kb:-2048}

on property:dev.mnt.blk.data=* && property:sys.read_ahead_kb=*
    write /sys/block/${dev.mnt.blk.data}/queue/read_ahead_kb ${sys.read_ahead_kb:-2048}

on early-fs:
    setprop sys.read_ahead_kb ${ro.read_ahead_kb.boot:-2048}

on property:sys.boot_completed=1
   setprop sys.read_ahead_kb ${ro.read_ahead_kb.bootcomplete:-128}