實作動態區隔

動態分割是透過 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 顯示轉換為動態分區前後的範例分區表。

分區資料表配置
圖 1. 轉換為動態分區時的新實體分區表配置

支援的動態分區如下:

  • 系統
  • 供應商
  • 產品
  • 系統分機號碼
  • 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

Jamboard 設定變更

您必須設定 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_foovendorproductodm,並放入 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 - overhead
  • 對於非 A/B 裝置和改裝 A/B 裝置,所有群組的最大大小總和必須為:
    BOARD_SUPER_PARTITION_SIZE - overhead
  • 在建構期間,更新群組中每個分割區的圖片大小總和,不得超過群組大小上限。
  • 計算時必須納入經常用量,才能將中繼資料、對齊等因素納入考量。合理的額外負荷為 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 的未使用空間。

系統即為根目錄的變更

搭載 Android 10 的裝置不得使用系統即為根目錄。

搭載動態分區的裝置 (無論是推出時就搭載,還是改裝成搭載動態分區),都不得使用以系統為根目錄的設定。Linux 核心無法解讀 super 分割區,因此無法掛接 system 本身。system 現在由第一階段 init 裝載,後者位於 ramdisk 中。

請勿設定 BOARD_BUILD_SYSTEM_ROOT_IMAGE。在 Android 10 中,BOARD_BUILD_SYSTEM_ROOT_IMAGE 標記只用於區分系統是由核心掛接,還是由 ramdisk 中的第一階段 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 驗證啟動 2.0 時,如果裝置未使用鏈結分割區描述元,則不必進行任何變更。不過,如果使用鏈結式分割區,且其中一個已驗證的分割區是動態分割區,則必須進行變更。

以下是裝置的範例設定,該裝置會將 vbmeta 鏈結至 systemvendor 分割區。

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: Support vbmeta blobs in beginning of partition."

核心指令列變更

您必須在核心指令列中新增 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 旗標,然後指定的 vbmeta 分割區會由第一階段 init 初始化,再嘗試掛接任何裝置。
  • 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 檔案複製到第一階段的 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

開機載入程式 (或任何非使用者空間的刷機工具) 無法辨識動態分割區,因此無法刷機。為解決這個問題,裝置必須使用快速啟動通訊協定的使用者空間實作,也就是 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

Jamboard 設定變更

您必須設定下列開發板變數:

  • 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;

如需其他設定,請參閱「在新裝置上實作動態分割區」。

如要進一步瞭解改造更新,請參閱「OTA for A/B Devices without Dynamic Partitions」。

原廠映像檔

如果裝置支援動態分區,請避免使用使用者空間 Fastboot 刷入原廠映像檔,因為啟動至使用者空間的速度比其他刷入方法慢。

為解決這個問題,make dist 現在會建構額外的 super.img 圖片,可直接刷入超級分割區。它會自動將邏輯分割區的內容組合在一起,也就是說,除了 super 分割區中繼資料之外,還包含 system.imgvendor.img 等。這個映像檔可以直接刷入 super 分割區,不需要任何額外工具或使用 fastbootd。建構完成後,super.img 會放置在 ${ANDROID_PRODUCT_OUT} 中。

對於搭載動態分區的 A/B 裝置,A 插槽中包含映像檔。super.img直接刷入超級映像檔後,請將 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。您可能會預期在 early-fs (各種精靈和設施啟動時) init.rc 指令碼可以覆寫之前,核心預設 read_ahead_kb 就已足夠。init.rc因此,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}