实现动态分区

动态分区是使用 Linux 内核中的 dm-linear device-mapper 模块实现的。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 显示了转换为动态分区前后的分区表示例。

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

支持的动态分区包括:

  • 系统
  • 供应商
  • 产品
  • System Ext
  • ODM

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

分区对齐

如果 super 分区未正确对齐,device-mapper 模块的运行效率可能会降低。super 分区必须与最小 I/O 请求大小保持一致,该大小由块层决定。 默认情况下,构建系统(通过生成 super 分区映像的 lpmake)假设每个动态分区有 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

下面的设备示例将系统和产品服务放入 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
  • 对于虚拟 A/B 启动设备,所有组的大小上限总和不得超过:
    BOARD_SUPER_PARTITION_SIZE - 开销
    请参阅实现虚拟 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 分区,因此无法自行装载 system 本身。system 现在由位于 ramdisk 中的第一阶段 init 装载。

不要设置 BOARD_BUILD_SYSTEM_ROOT_IMAGE。在 Android 10 中,BOARD_BUILD_SYSTEM_ROOT_IMAGE标记仅用于区分系统是由内核装载还是在第一阶段装载init(在 ramdisk 中)。

如果将 BOARD_BUILD_SYSTEM_ROOT_IMAGE 设置为 true,则在 PRODUCT_USE_DYNAMIC_PARTITIONS 也为 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

使用该配置,引导加载程序可以在 system 分区和 vendor 分区的末尾找到 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_devicesinit 使用它来启用 /dev/block/by-name 符号链接。该参数应该是由 ueventd 创建的底层 by-name 符号链接(即 /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 build 的开发者,adb remount 对快速迭代非常有用。动态分区给 adb remount 造成了问题,因为每个文件系统中都不再有空闲空间。为解决此问题,设备可以启用 overlayfs。只要超级分区中有空闲空间,adb remount 就会自动创建临时的动态分区,并使用 overlayfs 进行写入。该临时分区的名称为 scratch,因此请勿将该名称用于其他分区。

要详细了解如何启用 overlayfs,请参阅 AOSP 中的 overlayfs 自述文件

升级 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 分区,并且您希望将它们用作块存储设备来存储动态分区的 extent,并且其按名称符号链接标记为 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 映像,该映像可以直接刷写到 super 分区。除了 super 分区元数据之外,它还会自动捆绑逻辑分区的内容,这意味着它包含 system.imgvendor.img 等。此映像可以直接刷写到 super 分区,而无需任何其他工具或使用 fastbootd。构建之后,super.img 会放置在 ${ANDROID_PRODUCT_OUT} 中。

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

对于改造设备,make dist 会构建一组可以直接刷写到相应物理分区的 super_*.img 映像。例如,当 BOARD_SUPER_PARTITION_BLOCK_DEVICES 是系统供应商时,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 会变为活跃状态,值会开始更新。但是,由于属性触发器在 init 后才会进入活动状态,不能在初始启动阶段使用它们来处理 rootsystemvendor。您可能希望内核默认 read_ahead_kb 足够大,直到 init.rc 脚本可以替换 early-fs(当多个守护进程和设施启动时)。因此,Google 建议您将 on property 功能与 sys.read_ahead_kbinit.rc 控制的属性搭配使用,以处理操作并防止出现竞态条件,如以下示例所示:

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}