實作動態分區

動態分割功能是使用 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. 轉換為動態分區時的新實體分區表格版面配置

支援的動態分區如下:

  • 系統
  • 供應商
  • 產品
  • 系統 Ext
  • ODM

如果是搭載 Android 10 的裝置,核心指令列選項 androidboot.super_partition 必須留空,讓 sysprop 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>

如果動態分區映像檔的總大小超過 super 分區大小的一半,在 A/B 裝置上,建構系統會擲回錯誤。

您可以按照下列方式設定動態分區清單。如果是使用更新群組的裝置,請在 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 的未使用空間。

系統以 root 權限運作

搭載 Android 10 的裝置不得使用系統式根層級。

使用動態分區的裝置 (無論是啟動動態分區還是進行動態分區改造) 不得使用系統做為根目錄。Linux 核心無法解讀 super 分區,因此無法掛接 system 本身。system 現在已由位於 RAM 磁碟區的第一階段 init 掛載。

請勿設定 BOARD_BUILD_SYSTEM_ROOT_IMAGE。在 Android 10 中,BOARD_BUILD_SYSTEM_ROOT_IMAGE 標記只用於區分系統是透過核心還是 RAM 磁碟中的第一階段 init 掛載。

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 Verified Boot 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

在這種設定下,Bootloader 會預期在 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 系統啟動載入程式變更

如果 Bootloader 已內嵌 libavb,請加入下列修補程式:

如果使用鏈結分割區,請加入額外修補程式:

  • 49936b4c0109411fdd38bd4ba3a32a01c40439a9 — 「libavb:支援分割區開頭的 vbmeta 元件。」

核心指令列變更

必須在核心指令列中新增 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 項目。使用將成為 RAM 磁碟的 fstab 檔案。

您必須變更邏輯磁碟分割區的 fstab 檔案:

  • fs_mgr 標記欄位必須包含 logical 標記和 first_stage_mount 標記,這兩個標記是在 Android 10 中引進,用來表示要將分區掛載至第一個階段。
  • 分區可能會將 avb=vbmeta partition name 指定為 fs_mgr 標記,然後在嘗試掛載任何裝置之前,由第一階段 init 初始化指定的 vbmeta 分區。
  • dev 欄位必須是分割區名稱。

下列 fstab 項目會按照上述規則,將系統、廠商和產品設為邏輯分區。

#<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 檔案複製到第一階段的 RAM 磁碟。

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 儲值

對於使用 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。詳情請參閱「新裝置的板卡設定變更」。

舉例來說,如果裝置已具有系統和供應商分區,而您想要將這些分區轉換為動態分區,並在更新期間新增產品分區,請設定以下板卡設定:

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 映像檔,可直接閃記至超級分區。它會自動將邏輯區隔的內容組合起來,也就是說,除了 super 區隔中繼資料之外,它還包含 system.imgvendor.img 等。這個映像檔可以直接刷入 super 分割區,無須使用任何額外工具或 fastbootd。建構完成後,super.img 會放置在 ${ANDROID_PRODUCT_OUT} 中。

針對啟動內含動態分區的 A/B 裝置,super.img 會包含 A 插槽中的圖片。直接閃過超級映像檔後,請將插槽 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 功能,並搭配 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}