Android 动态分区 (super) 深度解析:原理、结构与实战
一、引言:从传统分区到动态分区
1.1 传统分区方案的困境
在 Android 早期版本中,系统固件采用静态分区布局。以 Android 8/9 为例,system、vendor、product、odm 等分区在编译时就被固定了大小:
bash
# 典型的 Android 9 分区表
Number Start End Size Name
1 2048 6143 4096 boot_a
2 6144 10239 4096 dtbo_a
3 10240 ... ... system_a
4 ... ... ... vendor_a
5 ... ... ... product_a
这种方案的痛点非常明显:
- 空间浪费:每个分区必须预留足够的空间以应对未来的 OTA 升级,但实际使用率可能只有 60%-70%
- 扩展困难:如果需要增大某个分区,必须重新调整整个 GPT 分区表,这对已出货的设备几乎不可能
- 灵活性差:不同厂商、不同机型的空间分配需求千差万别,静态方案无法适配
比如,某款设备
product分区利用率仅 40%,而vendor分区却因为合入了大量驱动而空间不足------静态分区对此无能为力。
2.2 动态分区方案的诞生
Android 10(API 29)引入了动态分区(Dynamic Partitions)机制,核心思路是在一个物理 super 分区内部管理多个逻辑分区。这套方案带来了以下革命性变化:
| 特性 | 传统分区 | 动态分区 |
|---|---|---|
| 分区大小调整 | 需修改 GPT | 运行时调整 |
| OTA 升级 | 需预留大量空间 | 按需分配 |
| 分区数量 | 受 GPT 限制 | 灵活创建 |
| 空间利用率 | 低 | 高 |
动态分区的核心理念是:将物理存储空间池化,逻辑分区按需从中分配 block。这类似于文件系统的 extent 机制,或者虚拟内存的分页机制。
二、Super 分区整体架构
2.1 物理视角 vs 逻辑视角
在动态分区方案中,我们需要区分两个层级:
#mermaid-svg-iDb0WRIW09TTuj98{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-iDb0WRIW09TTuj98 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-iDb0WRIW09TTuj98 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-iDb0WRIW09TTuj98 .error-icon{fill:#552222;}#mermaid-svg-iDb0WRIW09TTuj98 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-iDb0WRIW09TTuj98 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-iDb0WRIW09TTuj98 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-iDb0WRIW09TTuj98 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-iDb0WRIW09TTuj98 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-iDb0WRIW09TTuj98 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-iDb0WRIW09TTuj98 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-iDb0WRIW09TTuj98 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-iDb0WRIW09TTuj98 .marker.cross{stroke:#333333;}#mermaid-svg-iDb0WRIW09TTuj98 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-iDb0WRIW09TTuj98 p{margin:0;}#mermaid-svg-iDb0WRIW09TTuj98 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-iDb0WRIW09TTuj98 .cluster-label text{fill:#333;}#mermaid-svg-iDb0WRIW09TTuj98 .cluster-label span{color:#333;}#mermaid-svg-iDb0WRIW09TTuj98 .cluster-label span p{background-color:transparent;}#mermaid-svg-iDb0WRIW09TTuj98 .label text,#mermaid-svg-iDb0WRIW09TTuj98 span{fill:#333;color:#333;}#mermaid-svg-iDb0WRIW09TTuj98 .node rect,#mermaid-svg-iDb0WRIW09TTuj98 .node circle,#mermaid-svg-iDb0WRIW09TTuj98 .node ellipse,#mermaid-svg-iDb0WRIW09TTuj98 .node polygon,#mermaid-svg-iDb0WRIW09TTuj98 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-iDb0WRIW09TTuj98 .rough-node .label text,#mermaid-svg-iDb0WRIW09TTuj98 .node .label text,#mermaid-svg-iDb0WRIW09TTuj98 .image-shape .label,#mermaid-svg-iDb0WRIW09TTuj98 .icon-shape .label{text-anchor:middle;}#mermaid-svg-iDb0WRIW09TTuj98 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-iDb0WRIW09TTuj98 .rough-node .label,#mermaid-svg-iDb0WRIW09TTuj98 .node .label,#mermaid-svg-iDb0WRIW09TTuj98 .image-shape .label,#mermaid-svg-iDb0WRIW09TTuj98 .icon-shape .label{text-align:center;}#mermaid-svg-iDb0WRIW09TTuj98 .node.clickable{cursor:pointer;}#mermaid-svg-iDb0WRIW09TTuj98 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-iDb0WRIW09TTuj98 .arrowheadPath{fill:#333333;}#mermaid-svg-iDb0WRIW09TTuj98 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-iDb0WRIW09TTuj98 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-iDb0WRIW09TTuj98 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-iDb0WRIW09TTuj98 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-iDb0WRIW09TTuj98 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-iDb0WRIW09TTuj98 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-iDb0WRIW09TTuj98 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-iDb0WRIW09TTuj98 .cluster text{fill:#333;}#mermaid-svg-iDb0WRIW09TTuj98 .cluster span{color:#333;}#mermaid-svg-iDb0WRIW09TTuj98 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-iDb0WRIW09TTuj98 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-iDb0WRIW09TTuj98 rect.text{fill:none;stroke-width:0;}#mermaid-svg-iDb0WRIW09TTuj98 .icon-shape,#mermaid-svg-iDb0WRIW09TTuj98 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-iDb0WRIW09TTuj98 .icon-shape p,#mermaid-svg-iDb0WRIW09TTuj98 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-iDb0WRIW09TTuj98 .icon-shape .label rect,#mermaid-svg-iDb0WRIW09TTuj98 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-iDb0WRIW09TTuj98 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-iDb0WRIW09TTuj98 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-iDb0WRIW09TTuj98 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} GPT Partition Table
super 物理分区
逻辑分区层
system
vendor
product
引导分区
boot_a
boot_b
关键点 :从 Linux 内核视角,super 是一个物理分区(通过 GPT 描述);而 system、vendor 等逻辑分区通过 device-mapper 映射为虚拟块设备。用户空间的代码几乎无感知。
2.2 整体数据布局
super 分区的二进制布局如下:
Offset Contents
───────────────────────────────────────────────────────
0x0000 GPT Header + GPT Partition Table
0x1000 super partition header (LP_META_HEADER)
0x1000 + 4096 LP Table #1 (主分区表)
├── LP Table Header
├── Partition Entries (N个)
├── Group Descriptors (M个)
└── Block Mapping Table (K个extent)
0x1000 + 4096 * 2 LP Table #2 (备用分区表,结构同上)
0x1000 + 4096 * N LP Table #3 (仅 OTA 使用)
...
[逻辑分区数据区域]
├── system 分区数据
├── vendor 分区数据
├── product 分区数据
└── ...
说明:LP Table 默认每 4096 字节(一个 sector 对齐)放置一份,最多支持 4 份拷贝(包括主/备和 OTA 镜像),通过 metadata_slot 来管理多副本。
三、GPT 与 Super 分区的底层关系
3.1 GPT 分区表中的 super 条目
在理解动态分区之前,有必要先回顾一下 GPT(GUID Partition Table)的结构,因为 super 分区本身就是一个 GPT 分区。
GPT 的布局如下:
LBA 0: Protective MBR (防止旧系统误识别)
LBA 1: GPT Header
- Signature: "EFI PART"
- MyLBA: 1
- Partition Entry Start: LBA 2
- Number of Partition Entries: 128
- Size of Partition Entry: 128 bytes
LBA 2~33: GPT Partition Entries (128个, 每个128字节)
Entry 0: boot_a
Entry 1: boot_b
Entry 2: super ← 我们的主角
Entry 3: cache
...
LBA 34~: 数据区域
#mermaid-svg-cu1lxqFCx09IEQi8{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-cu1lxqFCx09IEQi8 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-cu1lxqFCx09IEQi8 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-cu1lxqFCx09IEQi8 .error-icon{fill:#552222;}#mermaid-svg-cu1lxqFCx09IEQi8 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-cu1lxqFCx09IEQi8 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-cu1lxqFCx09IEQi8 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-cu1lxqFCx09IEQi8 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-cu1lxqFCx09IEQi8 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-cu1lxqFCx09IEQi8 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-cu1lxqFCx09IEQi8 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-cu1lxqFCx09IEQi8 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-cu1lxqFCx09IEQi8 .marker.cross{stroke:#333333;}#mermaid-svg-cu1lxqFCx09IEQi8 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-cu1lxqFCx09IEQi8 p{margin:0;}#mermaid-svg-cu1lxqFCx09IEQi8 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-cu1lxqFCx09IEQi8 .cluster-label text{fill:#333;}#mermaid-svg-cu1lxqFCx09IEQi8 .cluster-label span{color:#333;}#mermaid-svg-cu1lxqFCx09IEQi8 .cluster-label span p{background-color:transparent;}#mermaid-svg-cu1lxqFCx09IEQi8 .label text,#mermaid-svg-cu1lxqFCx09IEQi8 span{fill:#333;color:#333;}#mermaid-svg-cu1lxqFCx09IEQi8 .node rect,#mermaid-svg-cu1lxqFCx09IEQi8 .node circle,#mermaid-svg-cu1lxqFCx09IEQi8 .node ellipse,#mermaid-svg-cu1lxqFCx09IEQi8 .node polygon,#mermaid-svg-cu1lxqFCx09IEQi8 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-cu1lxqFCx09IEQi8 .rough-node .label text,#mermaid-svg-cu1lxqFCx09IEQi8 .node .label text,#mermaid-svg-cu1lxqFCx09IEQi8 .image-shape .label,#mermaid-svg-cu1lxqFCx09IEQi8 .icon-shape .label{text-anchor:middle;}#mermaid-svg-cu1lxqFCx09IEQi8 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-cu1lxqFCx09IEQi8 .rough-node .label,#mermaid-svg-cu1lxqFCx09IEQi8 .node .label,#mermaid-svg-cu1lxqFCx09IEQi8 .image-shape .label,#mermaid-svg-cu1lxqFCx09IEQi8 .icon-shape .label{text-align:center;}#mermaid-svg-cu1lxqFCx09IEQi8 .node.clickable{cursor:pointer;}#mermaid-svg-cu1lxqFCx09IEQi8 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-cu1lxqFCx09IEQi8 .arrowheadPath{fill:#333333;}#mermaid-svg-cu1lxqFCx09IEQi8 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-cu1lxqFCx09IEQi8 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-cu1lxqFCx09IEQi8 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-cu1lxqFCx09IEQi8 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-cu1lxqFCx09IEQi8 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-cu1lxqFCx09IEQi8 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-cu1lxqFCx09IEQi8 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-cu1lxqFCx09IEQi8 .cluster text{fill:#333;}#mermaid-svg-cu1lxqFCx09IEQi8 .cluster span{color:#333;}#mermaid-svg-cu1lxqFCx09IEQi8 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-cu1lxqFCx09IEQi8 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-cu1lxqFCx09IEQi8 rect.text{fill:none;stroke-width:0;}#mermaid-svg-cu1lxqFCx09IEQi8 .icon-shape,#mermaid-svg-cu1lxqFCx09IEQi8 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-cu1lxqFCx09IEQi8 .icon-shape p,#mermaid-svg-cu1lxqFCx09IEQi8 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-cu1lxqFCx09IEQi8 .icon-shape .label rect,#mermaid-svg-cu1lxqFCx09IEQi8 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-cu1lxqFCx09IEQi8 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-cu1lxqFCx09IEQi8 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-cu1lxqFCx09IEQi8 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} LBA 0
Protective MBR
LBA 1
GPT Header
LBA 2~33
GPT Partition Entries
boot_a
boot_b
super ← 动态分区
cache
LBA 34~ 数据区域
super 分区数据
super 分区的 GPT 分区表项中,其分区类型 GUID 为:
GUID: A19B880C-3C6D-48BC-8B1B-E9EBB02D8A51
这个 GUID 告诉引导加载程序(bootloader)和内核:这是一个动态 super 分区,内部包含了需要进一步解析的逻辑分区元数据。
GPT 条目中 super 分区的起始 LBA 和大小决定了整个动态分区体系的物理边界:
c
// GPT Partition Entry for super (伪代码)
struct GptEntry {
uint8_t type_guid[16]; // = A19B880C-... (动态分区专用 GUID)
uint8_t unique_guid[16]; // 设备唯一标识
uint64_t starting_lba; // super 分区的起始位置
uint64_t ending_lba; // super 分区的结束位置
uint64_t attributes; // 属性位
uint16_t name[36]; // "super"
};
值得注意的是,GPT 中的 super 分区大小通常不等于所有逻辑分区大小之和。这是因为:
- 元数据区域:LP table 本身占用几 KB 到几十 KB
- 预留空间:为了支持 OTA 升级时的空间调整,通常会预留 10%-20% 的空白空间
- 对齐要求:逻辑分区以 1MB 对齐,导致不可避免的碎片
3.2 Bootloader 如何找到 super 分区
Bootloader(如 U-Boot、Little Kernel、ABL)启动时需要加载 system 分区来启动 Android。在动态分区方案中,bootloader 必须通过以下步骤才能访问逻辑分区:
Step 1: 扫描 GPT 找到 type_guid 匹配 super 的分区
Step 2: 从 super 分区的起始位置 + 4KB 偏移读取 LP 元数据
Step 3: 解析 LP 元数据,得到 system_a/vendor_a 等逻辑分区的 extent
Step 4: 配置 device-mapper 创建 dm-linear 设备
Step 5: 通过 dm 设备读取 system 分区的文件系统数据
这个流程相比传统方案(直接读取 GPT 分区)增加了复杂度,但换来了极大的灵活性。
四、数据结构深度剖析
动态分区的所有元数据定义在 AOSP 源码中的 system/liblp/ 目录下。核心头文件是 include/liblp/partition_opts.h 和 include/liblp/metadata_format.h。以下逐一分析。
4.1 LP_META_HEADER (元数据头部)
每份 LP Table 以 LP_META_HEADER 开头:
c
// system/liblp/include/liblp/metadata_format.h
#define LP_META_HEADER_MAGIC 0x414C5030 // "LP0" in ASCII
#define LP_META_HEADER_MAJOR_VER 10
#define LP_META_HEADER_MINOR_VER 0
struct LpMetadataHeaderV1_0 {
uint32_t magic; // 魔数: 0x414C5030 ("LP0")
uint32_t major_version; // 主版本号: 10
uint32_t minor_version; // 次版本号: 0
uint32_t linear_extent_count; // 线性 extent 总数
uint32_t extent_count; // extent 项数(含非线性的预留)
uint32_t logical_block_size; // 逻辑块大小(通常 4096)
uint32_t partition_count; // 逻辑分区数量
uint32_t reserved[7];
};
// 对齐到 128 字节
字段解读:
| 字段 | 说明 | 典型值 |
|---|---|---|
magic |
魔数,用于标识有效头部 | 0x414C5030 |
major_version |
主版本号 | 10+ |
linear_extent_count |
所有分区的线性 extent 总数 | N |
logical_block_size |
逻辑块大小,与存储块对齐 | 4096 |
partition_count |
该 super 中管理的逻辑分区数量 | 3~8 |
版本演进:V10 是 Android 10 引入的第一个版本。后续 Android 11+ 扩展了 header 结构(增加了快照、COW 等信息),但核心字段保持不变。
3.3 分区表项 (LpMetadataPartition)
紧接着 header 的是 partition_count 个分区表项:
c
struct LpMetadataPartition {
uint32_t name_size; // 分区名称长度(含 '\0' 终止符)
uint32_t attributes; // 属性标志位
uint32_t first_extent_index; // 第一个 extent 在 extent 表中的索引
uint32_t num_extents; // 该分区拥有的 extent 数量
uint32_t group_index; // 所属 group 索引
uint32_t reserved[2];
char name[]; // 变长字段:分区名称
};
// 每项最小 24 字节 + 名称长度
attributes 标志位:
c
// 属性常量定义
enum {
LP_PARTITION_ATTR_NONE = 0,
LP_PARTITION_ATTR_READONLY = (1 << 0), // 只读分区
LP_PARTITION_ATTR_SLOT_SUFFIXED = (1 << 1), // 需要 A/B slot 后缀
LP_PARTITION_ATTR_UPDATED = (1 << 2), // OTA 更新标记
LP_PARTITION_ATTR_DISABLED = (1 << 3), // 禁用标记
};
典型的分区表示例:
c
// 假设 device 分区配置
Partition Entry #1:
name_size = 7 // "system" + '\0'
attributes = 0x03 // READONLY | SLOT_SUFFIXED
first_extent_index = 0
num_extents = 3
group_index = 0 // "default" group
name = "system_a"
Partition Entry #2:
name_size = 7
attributes = 0x03
first_extent_index = 3
num_extents = 2
group_index = 0
name = "vendor_a"
3.4 线性 Extent (LpMetadataExtent)
Extent 是动态分区最核心的设计------它描述了一段连续的物理 block 范围:
c
struct LpMetadataExtent {
uint64_t num_sectors; // 该 extent 包含的扇区数(1 sector = 512B)
uint64_t sector_offset; // 在 super 分区内的起始扇区偏移
uint64_t padding[0];
};
Extent 与分区的映射关系:
Partition "system_a" (3 extents):
Extent #0: sector_offset=10000, num_sectors=20000
Extent #1: sector_offset=50000, num_sectors=15000
Extent #2: sector_offset=80000, num_sectors=5000
内存中的虚拟块设备:
[0 ~ 19999] → super 分区 [sector 10000 ~ 29999]
[20000 ~ 34999] → super 分区 [sector 50000 ~ 64999]
[35000 ~ 39999] → super 分区 [sector 80000 ~ 84999]
这种设计的关键优势:
- 碎片整理:物理上分散的空间可以被拼接成连续的虚拟空间
- 空间复用:被删除分区的 block 可通过修改 extent 重新分配给其他分区
- 极小元数据开销:每个 extent 仅 16 字节
4.5 Group Descriptor (组描述符)
分区可以分组管理,组描述符定义了组的属性:
c
struct LpMetadataPartitionGroup {
uint32_t name_size; // 组名称长度
uint32_t flags; // 标志位
uint64_t maximum_size; // 该组在 super 中可用的最大字节数
char name[]; // 变长字段:组名称
};
// 每项最小 16 字节 + 名称长度
常见的组:
| 组名 | 用途 | 典型最大大小 |
|---|---|---|
default |
A/B 分区组 | super 分区大小 |
google_dynamic_updatable |
Google 服务可更新分区 | 2GB |
vendor_group |
厂商自定义组 | 1GB |
组的实际作用 :在 OTA 升级时,组内的空间可以作为整体进行管理。例如,A/B 切换时,default 组内的所有分区一起切换到另一个 slot。
4.6 元数据的多重备份与 CRC 保护
动态分区的元数据采用了多重备份 + CRC 校验的容错设计。这是理解 super 分区可靠性的关键。
整个 super 分区的起始区域 layout 如下:
#mermaid-svg-DRdJcCDBYTLYPGIV{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-DRdJcCDBYTLYPGIV .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-DRdJcCDBYTLYPGIV .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-DRdJcCDBYTLYPGIV .error-icon{fill:#552222;}#mermaid-svg-DRdJcCDBYTLYPGIV .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-DRdJcCDBYTLYPGIV .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-DRdJcCDBYTLYPGIV .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-DRdJcCDBYTLYPGIV .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-DRdJcCDBYTLYPGIV .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-DRdJcCDBYTLYPGIV .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-DRdJcCDBYTLYPGIV .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-DRdJcCDBYTLYPGIV .marker{fill:#333333;stroke:#333333;}#mermaid-svg-DRdJcCDBYTLYPGIV .marker.cross{stroke:#333333;}#mermaid-svg-DRdJcCDBYTLYPGIV svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-DRdJcCDBYTLYPGIV p{margin:0;}#mermaid-svg-DRdJcCDBYTLYPGIV .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-DRdJcCDBYTLYPGIV .cluster-label text{fill:#333;}#mermaid-svg-DRdJcCDBYTLYPGIV .cluster-label span{color:#333;}#mermaid-svg-DRdJcCDBYTLYPGIV .cluster-label span p{background-color:transparent;}#mermaid-svg-DRdJcCDBYTLYPGIV .label text,#mermaid-svg-DRdJcCDBYTLYPGIV span{fill:#333;color:#333;}#mermaid-svg-DRdJcCDBYTLYPGIV .node rect,#mermaid-svg-DRdJcCDBYTLYPGIV .node circle,#mermaid-svg-DRdJcCDBYTLYPGIV .node ellipse,#mermaid-svg-DRdJcCDBYTLYPGIV .node polygon,#mermaid-svg-DRdJcCDBYTLYPGIV .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-DRdJcCDBYTLYPGIV .rough-node .label text,#mermaid-svg-DRdJcCDBYTLYPGIV .node .label text,#mermaid-svg-DRdJcCDBYTLYPGIV .image-shape .label,#mermaid-svg-DRdJcCDBYTLYPGIV .icon-shape .label{text-anchor:middle;}#mermaid-svg-DRdJcCDBYTLYPGIV .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-DRdJcCDBYTLYPGIV .rough-node .label,#mermaid-svg-DRdJcCDBYTLYPGIV .node .label,#mermaid-svg-DRdJcCDBYTLYPGIV .image-shape .label,#mermaid-svg-DRdJcCDBYTLYPGIV .icon-shape .label{text-align:center;}#mermaid-svg-DRdJcCDBYTLYPGIV .node.clickable{cursor:pointer;}#mermaid-svg-DRdJcCDBYTLYPGIV .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-DRdJcCDBYTLYPGIV .arrowheadPath{fill:#333333;}#mermaid-svg-DRdJcCDBYTLYPGIV .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-DRdJcCDBYTLYPGIV .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-DRdJcCDBYTLYPGIV .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-DRdJcCDBYTLYPGIV .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-DRdJcCDBYTLYPGIV .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-DRdJcCDBYTLYPGIV .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-DRdJcCDBYTLYPGIV .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-DRdJcCDBYTLYPGIV .cluster text{fill:#333;}#mermaid-svg-DRdJcCDBYTLYPGIV .cluster span{color:#333;}#mermaid-svg-DRdJcCDBYTLYPGIV div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-DRdJcCDBYTLYPGIV .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-DRdJcCDBYTLYPGIV rect.text{fill:none;stroke-width:0;}#mermaid-svg-DRdJcCDBYTLYPGIV .icon-shape,#mermaid-svg-DRdJcCDBYTLYPGIV .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-DRdJcCDBYTLYPGIV .icon-shape p,#mermaid-svg-DRdJcCDBYTLYPGIV .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-DRdJcCDBYTLYPGIV .icon-shape .label rect,#mermaid-svg-DRdJcCDBYTLYPGIV .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-DRdJcCDBYTLYPGIV .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-DRdJcCDBYTLYPGIV .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-DRdJcCDBYTLYPGIV :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} super 分区
元数据区域
逻辑分区数据区域
Slot 0
主副本
Slot 1
备用副本
Slot 2
OTA 专用
Slot 3
OTA 备用
system
vendor
product
多重备份的作用:
| Slot | 用途 | 说明 |
|---|---|---|
| Slot 0 | 主副本 | 当前系统使用的元数据 |
| Slot 1 | 备用副本 | 主副本损坏时自动切换 |
| Slot 2~3 | OTA 专用 | 升级过程中暂存新元数据 |
CRC 校验机制:
c
// 校验过程 (伪代码)
bool VerifyMetadata(LpMetadataHeader* header, uint8_t* data, size_t size) {
// 1. 验证魔数
if (header->magic != LP_META_HEADER_MAGIC) return false;
// 2. 计算 CRC32
uint32_t expected_crc = header->metadata_crc32;
uint32_t actual_crc = crc32(data + sizeof(LpMetadataHeader),
size - sizeof(LpMetadataHeader));
// 3. 比较
return expected_crc == actual_crc;
}
当 liblp 读取元数据时,它会先尝试 Slot 0,如果 CRC 校验失败则自动回退到 Slot 1:
cpp
std::unique_ptr<LpMetadata> ReadMetadataWithRetry(int fd) {
// 先尝试 slot 0
for (int slot = 0; slot < kMaxMetadataSlots; slot++) {
auto metadata = ReadMetadata(fd, slot);
if (metadata) {
return metadata; // 校验通过,直接返回
}
// 校验失败,记录日志并尝试下一个 slot
LWARN("Metadata slot %d is corrupt, trying next", slot);
}
// 所有 slot 都损坏
LERROR("All metadata slots are corrupt!");
return nullptr;
}
这种设计确保了在意外断电或刷写中断的情况下,设备仍然可以恢复。
五、Device Mapper 机制深度分析
5.1 dm-linear 映射原理
动态分区的核心机制依赖于 Linux 内核的 Device Mapper 框架。每个逻辑分区对应一个 dm-linear 设备,它本质上是一个"虚拟块设备",将 I/O 请求重定向到 super 分区的特定区域。
渲染错误: Mermaid 渲染失败: Lexical error on line 2. Unrecognized text. ...0
dm-linear 虚拟设备] DM --> Remap[I -----------------------^
使用 dmsetup table 可以看到实际的映射关系:
bash
# 一个典型的 dm-linear 映射表
adb shell dmsetup table
# 输出格式: <设备名> <起始扇区> <扇区数> linear <源设备> <源偏移>
system_a: 0 2048000 linear 259:3 2048000
vendor_a: 0 1024000 linear 259:3 4096000
product_a: 0 1024000 linear 259:3 5120000
解释:
0 2048000:虚拟设备的起始扇区为 0,长度 2048000 扇区(约 1GB)linear:使用线性映射目标类型259:3:源设备(super 分区)的设备号2048000:在源设备中的偏移量
5.2 Device Mapper 在内核中的实现
Device Mapper 是 Linux 内核中的一个框架,通过 dm 模块实现。dm-linear 目标的代码在 drivers/md/dm-linear.c 中:
c
// drivers/md/dm-linear.c (内核源码)
static int linear_map(struct dm_target *ti, struct bio *bio) {
struct linear_c *lc = ti->private;
// 关键操作:将 bio 的目标扇区重新映射
// 从虚拟设备的扇区 → 物理设备的扇区
bio->bi_iter.bi_sector += lc->start;
// 将 bio 提交到真实设备
return DM_MAPIO_REMAPPED;
}
当内核收到对 /dev/dm-0 的读写请求时:
- VFS 层:文件系统层将 I/O 发送到块设备
- dm 核心:根据映射表找到对应的 dm-linear 目标
- linear_map:将虚拟扇区号加上偏移量,转换为物理扇区号
- 提交 bio:将重新映射后的 bio 提交到 super 分区
这个过程完全在内核中完成,零拷贝、零上下文切换,性能开销极低(仅增加一次加法运算)。
5.3 内核中的元数据解析
在高通平台中,ABL(Apps Bootloader)或 UFS 驱动会在内核启动前完成逻辑分区的创建。但在某些架构中,内核需要自身解析 LP 元数据:
c
// 内核中的 LP 元数据解析 (伪代码)
int dm_init_dynamic_partitions(struct block_device *super_bdev) {
// 1. 读取 LP 元数据
struct lp_metadata *meta = read_lp_metadata(super_bdev, 0);
if (!meta) return -EINVAL;
// 2. 遍历所有分区
for (int i = 0; i < meta->header.partition_count; i++) {
struct lp_partition *part = &meta->partitions[i];
// 3. 为每个分区创建 dm-linear 设备
struct dm_ioctl *dm = create_dm_device(part->name);
for (int j = 0; j < part->num_extents; j++) {
struct lp_extent *ext = &meta->extents[part->first_extent_index + j];
add_dm_table(dm, ext->num_sectors * 512,
super_bdev->bd_dev, ext->sector_offset * 512);
}
load_dm_device(dm);
}
return 0;
}
六、lpmake:从零构建 Super 镜像
6.1 lpmake 命令详解
lpmake 是构建 super 镜像的核心工具,源码在 system/liblp/lpmake.cpp。编译系统和 OTA 工具链都依赖它来生成最终的 super.img。
bash
# lpmake 基本用法
lpmake \
--device-size 4294967296 \ # super 分区总大小 (4GB)
--metadata-size 65536 \ # 元数据区域大小 (64KB)
--metadata-slots 2 \ # 元数据 slot 数量
--super-name super \ # super 分区名称
--partition system_a:readonly:1G:default \
--partition vendor_a:readonly:512M:default \
--partition product_a:readonly:512M:default \
--image system_a=out/system.img \
--image vendor_a=out/vendor.img \
--image product_a=out/product.img \
--sparse \ # 输出稀疏格式
--output super.img
参数详解:
| 参数 | 说明 | 示例值 |
|---|---|---|
--device-size |
super 物理分区的大小,必须与 GPT 一致 | 4294967296 |
--metadata-size |
为元数据分配的字节数 | 65536 |
--metadata-slots |
元数据副本数 | 2 或 3 |
--partition |
定义逻辑分区 | system_a:readonly:1G:default |
--image |
指定分区镜像文件路径 | system_a=out/system.img |
--sparse |
输出 Android Sparse 格式 | 默认开启 |
--alignment |
分区对齐大小 | 4096 (4KB) |
6.2 lpmake 内部工作流
lpmake 执行流程:
1. 解析命令行参数
├── 收集所有分区定义
├── 收集所有镜像文件
└── 验证参数合法性
2. 分配空间 (Space Allocator)
├── 按传入顺序排列分区
├── 按 alignment 对齐
├── 将 extent 分配到 super 的空闲区域
└── 确保所有分区在 size 约束内
3. 生成 LP 元数据
├── 填充 LP_META_HEADER
├── 写入所有 Partition Entries
├── 写入所有 Extent Entries
├── 写入 Group Descriptors
├── 计算并写入 CRC32
└── 将元数据复制到多个 slot
4. 写入镜像数据
├── 创建输出文件
├── 写入 GPT 头部 (可选)
├── 写入 LP 元数据
└── 按 extent 位置写入各分区镜像数据
6.3 空间分配算法
lpmake 的空间分配是该工具最值得关注的技术细节。它的核心算法如下:
cpp
// 简化的空间分配器
class SpaceAllocator {
public:
// 初始空闲区间为整个 super 分区(除去元数据头部)
SpaceAllocator(uint64_t super_size, uint64_t metadata_size) {
free_regions_.push_back({
.start = metadata_size, // 元数据之后
.end = super_size - 1 // 到 super 末尾
});
}
// 分配一块连续空间
bool Allocate(uint64_t size, uint32_t alignment,
uint64_t *out_start, uint64_t *out_end) {
for (auto it = free_regions_.begin(); it != free_regions_.end(); ++it) {
uint64_t aligned_start = AlignUp(it->start, alignment);
uint64_t end = aligned_start + size - 1;
if (end <= it->end) {
// 找到合适的空闲区间
*out_start = aligned_start;
*out_end = end;
// 更新空闲区间
it->start = AlignUp(end + 1, alignment);
if (it->start >= it->end) {
free_regions_.erase(it);
}
return true;
}
}
return false; // 空间不足
}
private:
struct Region { uint64_t start; uint64_t end; };
std::list<Region> free_regions_;
};
这个算法的特点:
- 首次适应(First-Fit):按顺序查找第一个能容纳分区的空闲区域
- 对齐约束:每个 extent 都按 logical_block_size 对齐
- 无碎片整理:当前实现不会对已分配空间进行碎片整理
6.4 稀疏镜像格式 (Android Sparse)
lpmake 默认输出 Android Sparse 格式。这种格式可以显著减少 super 镜像的大小:
#mermaid-svg-j04ZuvGEAz6voErL{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-j04ZuvGEAz6voErL .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-j04ZuvGEAz6voErL .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-j04ZuvGEAz6voErL .error-icon{fill:#552222;}#mermaid-svg-j04ZuvGEAz6voErL .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-j04ZuvGEAz6voErL .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-j04ZuvGEAz6voErL .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-j04ZuvGEAz6voErL .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-j04ZuvGEAz6voErL .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-j04ZuvGEAz6voErL .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-j04ZuvGEAz6voErL .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-j04ZuvGEAz6voErL .marker{fill:#333333;stroke:#333333;}#mermaid-svg-j04ZuvGEAz6voErL .marker.cross{stroke:#333333;}#mermaid-svg-j04ZuvGEAz6voErL svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-j04ZuvGEAz6voErL p{margin:0;}#mermaid-svg-j04ZuvGEAz6voErL .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-j04ZuvGEAz6voErL .cluster-label text{fill:#333;}#mermaid-svg-j04ZuvGEAz6voErL .cluster-label span{color:#333;}#mermaid-svg-j04ZuvGEAz6voErL .cluster-label span p{background-color:transparent;}#mermaid-svg-j04ZuvGEAz6voErL .label text,#mermaid-svg-j04ZuvGEAz6voErL span{fill:#333;color:#333;}#mermaid-svg-j04ZuvGEAz6voErL .node rect,#mermaid-svg-j04ZuvGEAz6voErL .node circle,#mermaid-svg-j04ZuvGEAz6voErL .node ellipse,#mermaid-svg-j04ZuvGEAz6voErL .node polygon,#mermaid-svg-j04ZuvGEAz6voErL .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-j04ZuvGEAz6voErL .rough-node .label text,#mermaid-svg-j04ZuvGEAz6voErL .node .label text,#mermaid-svg-j04ZuvGEAz6voErL .image-shape .label,#mermaid-svg-j04ZuvGEAz6voErL .icon-shape .label{text-anchor:middle;}#mermaid-svg-j04ZuvGEAz6voErL .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-j04ZuvGEAz6voErL .rough-node .label,#mermaid-svg-j04ZuvGEAz6voErL .node .label,#mermaid-svg-j04ZuvGEAz6voErL .image-shape .label,#mermaid-svg-j04ZuvGEAz6voErL .icon-shape .label{text-align:center;}#mermaid-svg-j04ZuvGEAz6voErL .node.clickable{cursor:pointer;}#mermaid-svg-j04ZuvGEAz6voErL .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-j04ZuvGEAz6voErL .arrowheadPath{fill:#333333;}#mermaid-svg-j04ZuvGEAz6voErL .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-j04ZuvGEAz6voErL .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-j04ZuvGEAz6voErL .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-j04ZuvGEAz6voErL .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-j04ZuvGEAz6voErL .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-j04ZuvGEAz6voErL .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-j04ZuvGEAz6voErL .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-j04ZuvGEAz6voErL .cluster text{fill:#333;}#mermaid-svg-j04ZuvGEAz6voErL .cluster span{color:#333;}#mermaid-svg-j04ZuvGEAz6voErL div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-j04ZuvGEAz6voErL .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-j04ZuvGEAz6voErL rect.text{fill:none;stroke-width:0;}#mermaid-svg-j04ZuvGEAz6voErL .icon-shape,#mermaid-svg-j04ZuvGEAz6voErL .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-j04ZuvGEAz6voErL .icon-shape p,#mermaid-svg-j04ZuvGEAz6voErL .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-j04ZuvGEAz6voErL .icon-shape .label rect,#mermaid-svg-j04ZuvGEAz6voErL .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-j04ZuvGEAz6voErL .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-j04ZuvGEAz6voErL .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-j04ZuvGEAz6voErL :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Sparse Header
魔数 + 块大小 + 总块数
Raw Chunk CAC1
原始数据
Fill Chunk CAC2
填充数据
Skip Chunk CAC4
空洞跳过
... 更多 Chunk
Chunk 类型:
| 类型 | 标识 | 说明 |
|---|---|---|
CAC1 |
Raw | 原始数据块 |
CAC2 |
Fill | 用固定值填充(如全零) |
CAC3 |
Don't Care | 保留但无数据 |
CAC4 |
Skip | 跳过(空洞,不占用空间) |
稀疏格式的实际效果:一个 4GB 的 super 分区,如果实际数据只有 2GB,稀疏镜像可能只有 2.1GB(考虑元数据),节省近 50% 的传输和存储空间。
七、lpdump 工具实战
4.1 使用 lpdump 解析 super 镜像
lpdump 是 AOSP 自带的元数据解析工具,源码在 system/liblp/lpdump.cpp。
bash
# 从设备读取 super 元数据
adb shell lpdump
# 或者直接解析 super 镜像文件
lpdump /path/to/super.img
输出示例:
Device: super
Slot 0:
Metadata version: 10.0
Block device size: 4,294,967,296 bytes (4096 MB)
Partition layout:
Name: system_a
Group: default
Attributes: readonly+slot-suffixed
Size: 1,048,576,000 bytes (1000 MB)
Extents:
[0: 0, 2048000 sectors]
Name: vendor_a
Group: default
Attributes: readonly+slot-suffixed
Size: 524,288,000 bytes (500 MB)
Extents:
[0: 2048000, 1024000 sectors]
Name: product_a
Group: default
Attributes: readonly+slot-suffixed
Size: 524,288,000 bytes (500 MB)
Extents:
[0: 3072000, 1024000 sectors]
Groups:
Name: default
Maximum size: 4,294,967,296 bytes
Name: google_dynamic_updatable
Maximum size: 1,073,741,824 bytes
4.2 解析过程详解
lpdump 内部的工作流程:
1. 打开设备/文件
2. 跳过 GPT(前 512 字节的 MBR + GPT header)
3. 在 sector 8 (4096 字节偏移) 开始搜索 LP 元数据
4. 读取 LP_META_HEADER,验证 magic
5. 根据 header 中的 partition_count 读取分区表项
6. 读取 extent 表构建完整映射
7. 读取 group 描述符
8. 格式化输出
关键代码片段:
cpp
// system/liblp/lpdump.cpp (简化)
int DumpMetadata(int fd) {
// 1. 读取 header
LpMetadataHeader header;
if (!ReadHeader(fd, &header)) return -1;
// 2. 验证魔数
if (header.magic != LP_META_HEADER_MAGIC) {
fprintf(stderr, "Invalid magic: 0x%08x\n", header.magic);
return -1;
}
// 3. 读取分区表
std::vector<LpMetadataPartition> partitions;
partitions.resize(header.partition_count);
read(fd, partitions.data(), sizeof(LpMetadataPartition) * header.partition_count);
// 4. 输出每个分区的详细信息
for (auto& part : partitions) {
printf("Name: %s\n", part.name);
printf(" Size: %llu bytes\n", CalculateSize(part));
printf(" Extents:\n");
for (int i = 0; i < part.num_extents; i++) {
auto& ext = extents[part.first_extent_index + i];
printf(" [%d: %llu, %llu sectors]\n",
i, ext.sector_offset, ext.num_sectors);
}
}
return 0;
}
八、lpunpack:从 super 拆解逻辑分区
5.1 基本用法
lpunpack 用于从 super 镜像中提取出各个逻辑分区的原始镜像:
bash
# 解包整个 super
lpunpack super.img output_dir/
# 指定 metadata slot
lpunpack --slot=0 super.img output_dir/
输出:
output_dir/
├── system_a.img
├── vendor_a.img
├── product_a.img
└── system_b.img (如果可用)
5.2 实现原理
lpunpack 的核心是读取 extent 映射,然后从 super 中按照 extent 描述的物理扇区位置读取数据,拼合为原始分区镜像:
cpp
// system/liblp/lpunpack.cpp (简化)
int UnpackImage(int fd, const LpMetadata& metadata,
const LpMetadataPartition& partition,
const std::string& output_dir) {
// 创建输出文件
std::string path = output_dir + "/" + partition.name + ".img";
int out_fd = open(path.c_str(), O_CREAT | O_WRONLY, 0644);
// 遍历该分区的所有 extent
for (int i = 0; i < partition.num_extents; i++) {
auto& extent = metadata.extents[partition.first_extent_index + i];
// 从 super 分区的对应扇区偏移读取数据
lseek(fd, extent.sector_offset * 512, SEEK_SET);
std::vector<char> buffer(extent.num_sectors * 512);
read(fd, buffer.data(), buffer.size());
// 写入输出文件
write(out_fd, buffer.data(), buffer.size());
}
close(out_fd);
return 0;
}
注意 :如果分区包含多个不连续的 extent,
lpunpack会按照分区表项中的顺序依次读取并拼接,最终在用户空间呈现为一个连续的文件。
九、liblp 核心源码分析
9.1 liblp 的整体架构
liblp 是 AOSP 中管理动态分区元数据的基础库,位于 system/liblp/。其核心组件:
system/liblp/
├── include/liblp/
│ ├── metadata_format.h ← 数据结构定义
│ ├── partition_opts.h ← 分区选项
│ ├── builder.h ← 元数据构建器
│ ├── reader.h ← 元数据读取器
│ └── writer.h ← 元数据写入器
├── builder.cpp ← 构建/修改元数据
├── reader.cpp ← 从设备/镜像读取元数据
├── writer.cpp ← 写入元数据到设备/镜像
├── utility.cpp ← 工具函数
└── lpdump.cpp ← lpdump 工具
6.2 读取流程 (reader.cpp)
cpp
// 核心读取函数
std::unique_ptr<LpMetadata> ReadMetadata(int fd, uint32_t slot) {
auto metadata = std::make_unique<LpMetadata>();
// Step 1: 定位 metadata 位置
// metadata 从 super 分区的 LPMETA_OFFSET 开始
// 每份 metadata 间隔 LP_SECTOR_SIZE (4096 字节)
off64_t offset = GetMetadataOffset(slot);
lseek64(fd, offset, SEEK_SET);
// Step 2: 读取 header
LpMetadataHeader header;
ReadFully(fd, &header, sizeof(header));
// Step 3: 验证完整性
if (header.magic != LP_META_HEADER_MAGIC) {
return nullptr; // 无效的 metadata
}
// Step 4: 读取所有分区表项
metadata->partitions.resize(header.partition_count);
ReadFully(fd, metadata->partitions.data(),
sizeof(LpMetadataPartition) * header.partition_count);
// Step 5: 读取所有 extent
metadata->extents.resize(header.extent_count);
ReadFully(fd, metadata->extents.data(),
sizeof(LpMetadataExtent) * header.extent_count);
// Step 6: 读取 group 表
metadata->groups.resize(header.group_count);
ReadFully(fd, metadata->groups.data(),
sizeof(LpMetadataPartitionGroup) * header.group_count);
return metadata;
}
6.3 写入流程 (writer.cpp)
写入流程比读取复杂,需要处理 CRC 校验和对齐:
cpp
bool WriteMetadata(int fd, const LpMetadata& metadata) {
// Step 1: 序列化所有元数据到缓冲区
std::vector<uint8_t> buffer;
SerializeMetadata(metadata, &buffer);
// Step 2: 计算 CRC32 校验和
uint32_t crc32 = ComputeCrc32(buffer);
// Step 3: 写入到所有 metadata slot
for (int slot = 0; slot < kMaxMetadataSlots; slot++) {
off64_t offset = slot * 4096;
WriteFully(fd, GetMetadataMagic(), 4);
WriteFully(fd, buffer.data(), buffer.size());
// 写入 CRC 到 slot 末尾
WriteFully(fd, &crc32, sizeof(crc32));
}
// Step 4: 同步 (fsync)
fsync(fd);
return true;
}
6.4 构建器 (builder.cpp)
builder 用于从零创建或修改元数据------这在编译时生成 super 镜像时非常关键:
cpp
class PartitionOpener {
public:
// 添加一个逻辑分区
bool AddPartition(const std::string& name, uint64_t size,
uint32_t attributes) {
LpMetadataPartition part = {};
part.name = name;
part.attributes = attributes;
// 自动分配 extent
uint64_t remaining = size;
uint64_t sector = FindFreeSectors();
while (remaining > 0) {
LpMetadataExtent extent;
uint64_t alloc_size = std::min(remaining, kMaxExtentSize);
extent.num_sectors = alloc_size / 512;
extent.sector_offset = sector;
part.extents.push_back(extent);
sector += extent.num_sectors;
remaining -= alloc_size;
}
partitions_.push_back(part);
return true;
}
// 最终生成完整的 LpMetadata
LpMetadata Build() {
LpMetadata metadata;
// 合并所有分区和 extent 到最终的线性表中
LinearizeExtents(&metadata);
// 检查空间约束
ValidateGroupSizes(metadata);
return metadata;
}
};
十、Super 分区生命周期管理
7.1 编译时:生成 super.img
在 AOSP 编译过程中,build/make/core/config.mk 和 BoardConfig.mk 共同决定了 super 分区的大小和内容:
makefile
# BoardConfig.mk 典型配置
BOARD_SUPER_PARTITION_SIZE := 4294967296 # 4GB
BOARD_SUPER_PARTITION_GROUPS := default
BOARD_SUPER_PARTITION_ERROR_LIMIT := 4194304000 # 约 4GB
# 各逻辑分区大小
BOARD_SYSTEMIMAGE_PARTITION_SIZE := 1073741824 # 1GB
BOARD_VENDORIMAGE_PARTITION_SIZE := 536870912 # 512MB
BOARD_PRODUCTIMAGE_PARTITION_SIZE := 536870912 # 512MB
# A/B 切片
TARGET_NO_RECOVERY := true
BOARD_USES_RECOVERY_AS_BOOT := true
AB_OTA_UPDATER := true
构建超级镜像的核心脚本在 build/make/tools/releasetools/:
python
# add_img_to_target_files.py (简化)
def BuildSuperImage(input_dir, output_file):
# 1. 收集所有逻辑分区的镜像文件
images = {
'system_a': input_dir + '/SYSTEM',
'vendor_a': input_dir + '/VENDOR',
'product_a': input_dir + '/PRODUCT',
}
# 2. 计算空间分配
total_size = sum(os.path.getsize(f) for f in images.values())
# 3. 调用 lpmake 生成 super 镜像
args = [
'lpmake',
'--device-size', str(BOARD_SUPER_PARTITION_SIZE),
'--metadata-size', '65536',
'--super-name', 'super',
]
for name, path in images.items():
args += ['--partition', f'{name}:readonly:{os.path.getsize(path)}']
args += ['--image', f'{name}={path}']
subprocess.run(args, check=True)
# 输出 super.img
7.2 刷写时:fastboot 的处理
fastboot flash super 是动态分区刷写的入口:
bash
# 刷写整个 super
fastboot flash super super.img
# 仅刷写特定逻辑分区(稀疏模式)
fastboot flash system_a system_a.img
fastboot 内部的处理逻辑:
1. 解析 super.img 的 LP 元数据
2. 对比设备当前的元数据
3. 按 extent 顺序逐块写入
4. 更新元数据(新 slot)
5. 验证写入完整性
关键策略:fastboot 支持稀疏写入,即只写入 super 中实际包含数据的区域,跳过空洞。这大幅减少了刷写时间。
7.3 OTA 升级时:动态调整
动态分区最大的优势体现在 OTA 升级中:
#mermaid-svg-ezqt53hzsrzF9STG{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-ezqt53hzsrzF9STG .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ezqt53hzsrzF9STG .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ezqt53hzsrzF9STG .error-icon{fill:#552222;}#mermaid-svg-ezqt53hzsrzF9STG .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ezqt53hzsrzF9STG .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ezqt53hzsrzF9STG .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ezqt53hzsrzF9STG .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ezqt53hzsrzF9STG .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ezqt53hzsrzF9STG .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ezqt53hzsrzF9STG .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ezqt53hzsrzF9STG .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ezqt53hzsrzF9STG .marker.cross{stroke:#333333;}#mermaid-svg-ezqt53hzsrzF9STG svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ezqt53hzsrzF9STG p{margin:0;}#mermaid-svg-ezqt53hzsrzF9STG .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-ezqt53hzsrzF9STG .cluster-label text{fill:#333;}#mermaid-svg-ezqt53hzsrzF9STG .cluster-label span{color:#333;}#mermaid-svg-ezqt53hzsrzF9STG .cluster-label span p{background-color:transparent;}#mermaid-svg-ezqt53hzsrzF9STG .label text,#mermaid-svg-ezqt53hzsrzF9STG span{fill:#333;color:#333;}#mermaid-svg-ezqt53hzsrzF9STG .node rect,#mermaid-svg-ezqt53hzsrzF9STG .node circle,#mermaid-svg-ezqt53hzsrzF9STG .node ellipse,#mermaid-svg-ezqt53hzsrzF9STG .node polygon,#mermaid-svg-ezqt53hzsrzF9STG .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-ezqt53hzsrzF9STG .rough-node .label text,#mermaid-svg-ezqt53hzsrzF9STG .node .label text,#mermaid-svg-ezqt53hzsrzF9STG .image-shape .label,#mermaid-svg-ezqt53hzsrzF9STG .icon-shape .label{text-anchor:middle;}#mermaid-svg-ezqt53hzsrzF9STG .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-ezqt53hzsrzF9STG .rough-node .label,#mermaid-svg-ezqt53hzsrzF9STG .node .label,#mermaid-svg-ezqt53hzsrzF9STG .image-shape .label,#mermaid-svg-ezqt53hzsrzF9STG .icon-shape .label{text-align:center;}#mermaid-svg-ezqt53hzsrzF9STG .node.clickable{cursor:pointer;}#mermaid-svg-ezqt53hzsrzF9STG .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-ezqt53hzsrzF9STG .arrowheadPath{fill:#333333;}#mermaid-svg-ezqt53hzsrzF9STG .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-ezqt53hzsrzF9STG .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-ezqt53hzsrzF9STG .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ezqt53hzsrzF9STG .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-ezqt53hzsrzF9STG .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ezqt53hzsrzF9STG .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-ezqt53hzsrzF9STG .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-ezqt53hzsrzF9STG .cluster text{fill:#333;}#mermaid-svg-ezqt53hzsrzF9STG .cluster span{color:#333;}#mermaid-svg-ezqt53hzsrzF9STG div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-ezqt53hzsrzF9STG .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-ezqt53hzsrzF9STG rect.text{fill:none;stroke-width:0;}#mermaid-svg-ezqt53hzsrzF9STG .icon-shape,#mermaid-svg-ezqt53hzsrzF9STG .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ezqt53hzsrzF9STG .icon-shape p,#mermaid-svg-ezqt53hzsrzF9STG .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-ezqt53hzsrzF9STG .icon-shape .label rect,#mermaid-svg-ezqt53hzsrzF9STG .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ezqt53hzsrzF9STG .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-ezqt53hzsrzF9STG .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-ezqt53hzsrzF9STG :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} OTA 升级后
system: 1.2GB
vendor: 512MB
product: 512MB
空闲: 0MB
OTA 升级前
system: 1GB
vendor: 512MB
product: 256MB
空闲: 256MB
OTA 包中的 update_engine 通过如下步骤调整分区:
- 读取旧元数据:获取当前 slot 的分配情况
- 计算新需求:根据 OTA 包内容计算各分区的新大小
- 重新分配空间:减小某分区 → 增大另一分区
- 写入 COW 分区:对于启用快照的设备,写入改动前的备份
- 更新元数据:将新元数据写入备用 slot
- 切换 slot:重启进入新系统
snapshot(快照)机制(Android 11+):在 OTA 过程中,被覆盖的数据块会先备份到 COW(Copy-On-Write)分区。如果 OTA 失败,可以回滚到升级前的状态。这是 Android 虚拟 A/B (Virtual A/B) 的核心基础。
十一、Virtual A/B 与 Snapshot 机制详解
11.1 为什么需要 Virtual A/B
传统 A/B(也称为"物理 A/B")需要为每个逻辑分区维护 A 和 B 两份副本:
#mermaid-svg-dFFjDqol7lI2GVU7{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-dFFjDqol7lI2GVU7 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-dFFjDqol7lI2GVU7 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-dFFjDqol7lI2GVU7 .error-icon{fill:#552222;}#mermaid-svg-dFFjDqol7lI2GVU7 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-dFFjDqol7lI2GVU7 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-dFFjDqol7lI2GVU7 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-dFFjDqol7lI2GVU7 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-dFFjDqol7lI2GVU7 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-dFFjDqol7lI2GVU7 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-dFFjDqol7lI2GVU7 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-dFFjDqol7lI2GVU7 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-dFFjDqol7lI2GVU7 .marker.cross{stroke:#333333;}#mermaid-svg-dFFjDqol7lI2GVU7 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-dFFjDqol7lI2GVU7 p{margin:0;}#mermaid-svg-dFFjDqol7lI2GVU7 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-dFFjDqol7lI2GVU7 .cluster-label text{fill:#333;}#mermaid-svg-dFFjDqol7lI2GVU7 .cluster-label span{color:#333;}#mermaid-svg-dFFjDqol7lI2GVU7 .cluster-label span p{background-color:transparent;}#mermaid-svg-dFFjDqol7lI2GVU7 .label text,#mermaid-svg-dFFjDqol7lI2GVU7 span{fill:#333;color:#333;}#mermaid-svg-dFFjDqol7lI2GVU7 .node rect,#mermaid-svg-dFFjDqol7lI2GVU7 .node circle,#mermaid-svg-dFFjDqol7lI2GVU7 .node ellipse,#mermaid-svg-dFFjDqol7lI2GVU7 .node polygon,#mermaid-svg-dFFjDqol7lI2GVU7 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-dFFjDqol7lI2GVU7 .rough-node .label text,#mermaid-svg-dFFjDqol7lI2GVU7 .node .label text,#mermaid-svg-dFFjDqol7lI2GVU7 .image-shape .label,#mermaid-svg-dFFjDqol7lI2GVU7 .icon-shape .label{text-anchor:middle;}#mermaid-svg-dFFjDqol7lI2GVU7 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-dFFjDqol7lI2GVU7 .rough-node .label,#mermaid-svg-dFFjDqol7lI2GVU7 .node .label,#mermaid-svg-dFFjDqol7lI2GVU7 .image-shape .label,#mermaid-svg-dFFjDqol7lI2GVU7 .icon-shape .label{text-align:center;}#mermaid-svg-dFFjDqol7lI2GVU7 .node.clickable{cursor:pointer;}#mermaid-svg-dFFjDqol7lI2GVU7 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-dFFjDqol7lI2GVU7 .arrowheadPath{fill:#333333;}#mermaid-svg-dFFjDqol7lI2GVU7 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-dFFjDqol7lI2GVU7 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-dFFjDqol7lI2GVU7 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-dFFjDqol7lI2GVU7 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-dFFjDqol7lI2GVU7 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-dFFjDqol7lI2GVU7 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-dFFjDqol7lI2GVU7 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-dFFjDqol7lI2GVU7 .cluster text{fill:#333;}#mermaid-svg-dFFjDqol7lI2GVU7 .cluster span{color:#333;}#mermaid-svg-dFFjDqol7lI2GVU7 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-dFFjDqol7lI2GVU7 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-dFFjDqol7lI2GVU7 rect.text{fill:none;stroke-width:0;}#mermaid-svg-dFFjDqol7lI2GVU7 .icon-shape,#mermaid-svg-dFFjDqol7lI2GVU7 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-dFFjDqol7lI2GVU7 .icon-shape p,#mermaid-svg-dFFjDqol7lI2GVU7 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-dFFjDqol7lI2GVU7 .icon-shape .label rect,#mermaid-svg-dFFjDqol7lI2GVU7 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-dFFjDqol7lI2GVU7 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-dFFjDqol7lI2GVU7 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-dFFjDqol7lI2GVU7 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} super 分区
A Slot
B Slot
system_a
vendor_a
product_a
system_b
vendor_b
product_b
这意味着 50% 的空间被用于"备份",对于存储空间受限的设备这是巨大的浪费。
Virtual A/B 的核心思想是:不再为 B slot 预分配空间,而是使用 Copy-On-Write (COW) 机制,在 OTA 过程中只备份那些被覆盖的数据块:
#mermaid-svg-94ACHoFaVRSVe2gC{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-94ACHoFaVRSVe2gC .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-94ACHoFaVRSVe2gC .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-94ACHoFaVRSVe2gC .error-icon{fill:#552222;}#mermaid-svg-94ACHoFaVRSVe2gC .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-94ACHoFaVRSVe2gC .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-94ACHoFaVRSVe2gC .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-94ACHoFaVRSVe2gC .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-94ACHoFaVRSVe2gC .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-94ACHoFaVRSVe2gC .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-94ACHoFaVRSVe2gC .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-94ACHoFaVRSVe2gC .marker{fill:#333333;stroke:#333333;}#mermaid-svg-94ACHoFaVRSVe2gC .marker.cross{stroke:#333333;}#mermaid-svg-94ACHoFaVRSVe2gC svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-94ACHoFaVRSVe2gC p{margin:0;}#mermaid-svg-94ACHoFaVRSVe2gC .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-94ACHoFaVRSVe2gC .cluster-label text{fill:#333;}#mermaid-svg-94ACHoFaVRSVe2gC .cluster-label span{color:#333;}#mermaid-svg-94ACHoFaVRSVe2gC .cluster-label span p{background-color:transparent;}#mermaid-svg-94ACHoFaVRSVe2gC .label text,#mermaid-svg-94ACHoFaVRSVe2gC span{fill:#333;color:#333;}#mermaid-svg-94ACHoFaVRSVe2gC .node rect,#mermaid-svg-94ACHoFaVRSVe2gC .node circle,#mermaid-svg-94ACHoFaVRSVe2gC .node ellipse,#mermaid-svg-94ACHoFaVRSVe2gC .node polygon,#mermaid-svg-94ACHoFaVRSVe2gC .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-94ACHoFaVRSVe2gC .rough-node .label text,#mermaid-svg-94ACHoFaVRSVe2gC .node .label text,#mermaid-svg-94ACHoFaVRSVe2gC .image-shape .label,#mermaid-svg-94ACHoFaVRSVe2gC .icon-shape .label{text-anchor:middle;}#mermaid-svg-94ACHoFaVRSVe2gC .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-94ACHoFaVRSVe2gC .rough-node .label,#mermaid-svg-94ACHoFaVRSVe2gC .node .label,#mermaid-svg-94ACHoFaVRSVe2gC .image-shape .label,#mermaid-svg-94ACHoFaVRSVe2gC .icon-shape .label{text-align:center;}#mermaid-svg-94ACHoFaVRSVe2gC .node.clickable{cursor:pointer;}#mermaid-svg-94ACHoFaVRSVe2gC .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-94ACHoFaVRSVe2gC .arrowheadPath{fill:#333333;}#mermaid-svg-94ACHoFaVRSVe2gC .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-94ACHoFaVRSVe2gC .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-94ACHoFaVRSVe2gC .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-94ACHoFaVRSVe2gC .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-94ACHoFaVRSVe2gC .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-94ACHoFaVRSVe2gC .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-94ACHoFaVRSVe2gC .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-94ACHoFaVRSVe2gC .cluster text{fill:#333;}#mermaid-svg-94ACHoFaVRSVe2gC .cluster span{color:#333;}#mermaid-svg-94ACHoFaVRSVe2gC div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-94ACHoFaVRSVe2gC .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-94ACHoFaVRSVe2gC rect.text{fill:none;stroke-width:0;}#mermaid-svg-94ACHoFaVRSVe2gC .icon-shape,#mermaid-svg-94ACHoFaVRSVe2gC .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-94ACHoFaVRSVe2gC .icon-shape p,#mermaid-svg-94ACHoFaVRSVe2gC .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-94ACHoFaVRSVe2gC .icon-shape .label rect,#mermaid-svg-94ACHoFaVRSVe2gC .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-94ACHoFaVRSVe2gC .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-94ACHoFaVRSVe2gC .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-94ACHoFaVRSVe2gC :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} super 分区
A Slot
激活使用
COW 分区
按需分配
system
vendor
product
OTA 备份数据块
11.2 COW 分区的数据结构
COW(Copy-On-Write)分区使用了专门的数据结构来跟踪被修改的 block:
c
// system/update_engine/的分区快照相关结构
struct CowHeader {
uint32_t magic; // "COW\0"
uint32_t major_version;
uint32_t minor_version;
uint64_t block_size; // 通常为 4096
uint64_t num_blocks; // COW 管理的 block 总数
uint64_t num_merges; // 已合并的操作数
uint64_t op_count; // 操作条目数
};
// COW 操作条目:记录哪些 block 被修改了
struct CowOperation {
uint64_t source_block; // 在源分区中的 block 号
uint64_t target_block; // 在目标分区中的 block 号
uint32_t num_blocks; // 连续的 block 数量
uint8_t type; // 操作类型
// 类型: kCowCopyOp (拷贝), kCowReplaceOp (替换),
// kCowZeroOp (清零), kCowXorOp (异或增量)
};
11.3 OTA 升级过程中的 Snapshot 处理
一次完整的 Virtual A/B OTA 流程如下:
Phase 1: 下载 OTA 包 (在 system_a 运行中)
├── update_engine 下载 OTA 包到 /data/ota_package
├── 解析 OTA 包中的 payload
└── 读取当前 system_a 的 LP 元数据
Phase 2: 生成新分区布局
├── 从 OTA payload 中提取新分区的 sizes
├── 计算需要增大的分区和需要减小的分区
├── 重新分配 super 中的空闲空间
└── 创建新的 LP 元数据 (写入 slot 2)
Phase 3: 写时复制 (COW)
├── 对于将要被覆盖的 block:
│ 1. 读取原始 block 数据
│ 2. 写入 COW 分区
│ 3. 记录 CowOperation
├── 将新数据写入目标位置
└── 更新 COW 元数据
Phase 4: 切换与合并
├── 更新 boot_control 的 slot 标记
├── 重启进入新系统 (system_b)
├── 在 init 阶段检查是否有未完成的 COW
├── 如果有,启动 cow_merge 进程
└── 合并完成后释放 COW 空间
11.4 Snapshot 的合并过程 (cow_merge)
合并是 Virtual A/B 中最关键的环节------它将 COW 中的修改"合并"到目标分区,使新系统可以独立运行:
cpp
// 简化的 cow_merge 逻辑
bool MergeCowToTarget(const std::string& cow_device,
const std::string& target_device) {
// 1. 读取 COW 头部
CowHeader header = ReadCowHeader(cow_device);
// 2. 遍历所有操作
for (uint64_t i = 0; i < header.op_count; i++) {
CowOperation op = ReadCowOp(cow_device, i);
switch (op.type) {
case kCowCopyOp:
// 从 COW 设备读取备份数据,写回目标位置
char buffer[op.num_blocks * 4096];
ReadCowData(cow_device, op.source_block, buffer);
WriteDevice(target_device, op.target_block, buffer);
break;
case kCowZeroOp:
// 目标位置清零
ZeroDevice(target_device, op.target_block, op.num_blocks);
break;
case kCowReplaceOp:
// 数据已经在新位置,只需更新映射
UpdateMapping(target_device, op.target_block, op.num_blocks);
break;
}
// 标记操作为"已合并"
MarkCowOpCompleted(cow_device, i);
}
// 3. 合并完成后,释放 COW 空间
ReleaseCowSpace(cow_device);
return true;
}
合并过程可以在系统空闲时后台执行,也可以在 init 阶段同步执行。Google 的设计目标是将合并时间控制在 2 分钟以内。
十二、AVB 与动态分区的集成
12.1 完整性校验链
Android Verified Boot (AVB) 2.0 与动态分区紧密配合:
#mermaid-svg-3qglujpHiQDQJ2KO{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-3qglujpHiQDQJ2KO .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-3qglujpHiQDQJ2KO .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-3qglujpHiQDQJ2KO .error-icon{fill:#552222;}#mermaid-svg-3qglujpHiQDQJ2KO .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-3qglujpHiQDQJ2KO .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-3qglujpHiQDQJ2KO .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-3qglujpHiQDQJ2KO .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-3qglujpHiQDQJ2KO .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-3qglujpHiQDQJ2KO .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-3qglujpHiQDQJ2KO .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-3qglujpHiQDQJ2KO .marker{fill:#333333;stroke:#333333;}#mermaid-svg-3qglujpHiQDQJ2KO .marker.cross{stroke:#333333;}#mermaid-svg-3qglujpHiQDQJ2KO svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-3qglujpHiQDQJ2KO p{margin:0;}#mermaid-svg-3qglujpHiQDQJ2KO .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-3qglujpHiQDQJ2KO .cluster-label text{fill:#333;}#mermaid-svg-3qglujpHiQDQJ2KO .cluster-label span{color:#333;}#mermaid-svg-3qglujpHiQDQJ2KO .cluster-label span p{background-color:transparent;}#mermaid-svg-3qglujpHiQDQJ2KO .label text,#mermaid-svg-3qglujpHiQDQJ2KO span{fill:#333;color:#333;}#mermaid-svg-3qglujpHiQDQJ2KO .node rect,#mermaid-svg-3qglujpHiQDQJ2KO .node circle,#mermaid-svg-3qglujpHiQDQJ2KO .node ellipse,#mermaid-svg-3qglujpHiQDQJ2KO .node polygon,#mermaid-svg-3qglujpHiQDQJ2KO .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-3qglujpHiQDQJ2KO .rough-node .label text,#mermaid-svg-3qglujpHiQDQJ2KO .node .label text,#mermaid-svg-3qglujpHiQDQJ2KO .image-shape .label,#mermaid-svg-3qglujpHiQDQJ2KO .icon-shape .label{text-anchor:middle;}#mermaid-svg-3qglujpHiQDQJ2KO .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-3qglujpHiQDQJ2KO .rough-node .label,#mermaid-svg-3qglujpHiQDQJ2KO .node .label,#mermaid-svg-3qglujpHiQDQJ2KO .image-shape .label,#mermaid-svg-3qglujpHiQDQJ2KO .icon-shape .label{text-align:center;}#mermaid-svg-3qglujpHiQDQJ2KO .node.clickable{cursor:pointer;}#mermaid-svg-3qglujpHiQDQJ2KO .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-3qglujpHiQDQJ2KO .arrowheadPath{fill:#333333;}#mermaid-svg-3qglujpHiQDQJ2KO .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-3qglujpHiQDQJ2KO .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-3qglujpHiQDQJ2KO .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-3qglujpHiQDQJ2KO .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-3qglujpHiQDQJ2KO .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-3qglujpHiQDQJ2KO .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-3qglujpHiQDQJ2KO .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-3qglujpHiQDQJ2KO .cluster text{fill:#333;}#mermaid-svg-3qglujpHiQDQJ2KO .cluster span{color:#333;}#mermaid-svg-3qglujpHiQDQJ2KO div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-3qglujpHiQDQJ2KO .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-3qglujpHiQDQJ2KO rect.text{fill:none;stroke-width:0;}#mermaid-svg-3qglujpHiQDQJ2KO .icon-shape,#mermaid-svg-3qglujpHiQDQJ2KO .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-3qglujpHiQDQJ2KO .icon-shape p,#mermaid-svg-3qglujpHiQDQJ2KO .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-3qglujpHiQDQJ2KO .icon-shape .label rect,#mermaid-svg-3qglujpHiQDQJ2KO .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-3qglujpHiQDQJ2KO .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-3qglujpHiQDQJ2KO .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-3qglujpHiQDQJ2KO :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} avb: 验证签名
avb: 验证签名
avb: 验证 vbmeta
avb: 验证 init
avb: 验证文件系统
avb: 验证文件系统
avb: 验证文件系统
PBL 一级引导
SBL 二级引导
ABL 应用引导
Linux Kernel
init 进程
system
vendor
product
Bootloader (ABL) 通过 GPT 找到 super 分区,读取 LP 元数据,通过 device-mapper 创建逻辑分区设备,然后校验 system 分区的 vbmeta 头部。
12.2 vbmeta 在动态分区中的位置
对于动态分区,AVB 的 vbmeta 信息嵌入在逻辑分区的镜像头部:
#mermaid-svg-HSvfPi4sCJpOjUV6{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-HSvfPi4sCJpOjUV6 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-HSvfPi4sCJpOjUV6 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-HSvfPi4sCJpOjUV6 .error-icon{fill:#552222;}#mermaid-svg-HSvfPi4sCJpOjUV6 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-HSvfPi4sCJpOjUV6 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-HSvfPi4sCJpOjUV6 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-HSvfPi4sCJpOjUV6 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-HSvfPi4sCJpOjUV6 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-HSvfPi4sCJpOjUV6 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-HSvfPi4sCJpOjUV6 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-HSvfPi4sCJpOjUV6 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-HSvfPi4sCJpOjUV6 .marker.cross{stroke:#333333;}#mermaid-svg-HSvfPi4sCJpOjUV6 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-HSvfPi4sCJpOjUV6 p{margin:0;}#mermaid-svg-HSvfPi4sCJpOjUV6 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-HSvfPi4sCJpOjUV6 .cluster-label text{fill:#333;}#mermaid-svg-HSvfPi4sCJpOjUV6 .cluster-label span{color:#333;}#mermaid-svg-HSvfPi4sCJpOjUV6 .cluster-label span p{background-color:transparent;}#mermaid-svg-HSvfPi4sCJpOjUV6 .label text,#mermaid-svg-HSvfPi4sCJpOjUV6 span{fill:#333;color:#333;}#mermaid-svg-HSvfPi4sCJpOjUV6 .node rect,#mermaid-svg-HSvfPi4sCJpOjUV6 .node circle,#mermaid-svg-HSvfPi4sCJpOjUV6 .node ellipse,#mermaid-svg-HSvfPi4sCJpOjUV6 .node polygon,#mermaid-svg-HSvfPi4sCJpOjUV6 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-HSvfPi4sCJpOjUV6 .rough-node .label text,#mermaid-svg-HSvfPi4sCJpOjUV6 .node .label text,#mermaid-svg-HSvfPi4sCJpOjUV6 .image-shape .label,#mermaid-svg-HSvfPi4sCJpOjUV6 .icon-shape .label{text-anchor:middle;}#mermaid-svg-HSvfPi4sCJpOjUV6 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-HSvfPi4sCJpOjUV6 .rough-node .label,#mermaid-svg-HSvfPi4sCJpOjUV6 .node .label,#mermaid-svg-HSvfPi4sCJpOjUV6 .image-shape .label,#mermaid-svg-HSvfPi4sCJpOjUV6 .icon-shape .label{text-align:center;}#mermaid-svg-HSvfPi4sCJpOjUV6 .node.clickable{cursor:pointer;}#mermaid-svg-HSvfPi4sCJpOjUV6 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-HSvfPi4sCJpOjUV6 .arrowheadPath{fill:#333333;}#mermaid-svg-HSvfPi4sCJpOjUV6 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-HSvfPi4sCJpOjUV6 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-HSvfPi4sCJpOjUV6 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-HSvfPi4sCJpOjUV6 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-HSvfPi4sCJpOjUV6 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-HSvfPi4sCJpOjUV6 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-HSvfPi4sCJpOjUV6 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-HSvfPi4sCJpOjUV6 .cluster text{fill:#333;}#mermaid-svg-HSvfPi4sCJpOjUV6 .cluster span{color:#333;}#mermaid-svg-HSvfPi4sCJpOjUV6 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-HSvfPi4sCJpOjUV6 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-HSvfPi4sCJpOjUV6 rect.text{fill:none;stroke-width:0;}#mermaid-svg-HSvfPi4sCJpOjUV6 .icon-shape,#mermaid-svg-HSvfPi4sCJpOjUV6 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-HSvfPi4sCJpOjUV6 .icon-shape p,#mermaid-svg-HSvfPi4sCJpOjUV6 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-HSvfPi4sCJpOjUV6 .icon-shape .label rect,#mermaid-svg-HSvfPi4sCJpOjUV6 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-HSvfPi4sCJpOjUV6 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-HSvfPi4sCJpOjUV6 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-HSvfPi4sCJpOjUV6 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} system_a 逻辑分区
AVB Header
vbmeta 元数据 + 签名
ext4 / erofs
文件系统数据
哈希树 / dm-verity
完整性校验
Bootloader 读取方式:
cpp
// bootloader vbmeta 校验伪代码
void VerifyDynamicPartition(const char* name) {
// 1. 通过 liblp 获取分区的 extent
auto metadata = ReadMetadata(super_fd, current_slot);
// 2. 计算分区设备路径
char devpath[64];
snprintf(devpath, sizeof(devpath),
"/dev/block/dm-%d", GetDmNumber(name));
// 3. 在分区头部查找 AVB 信息
int fd = open(devpath, O_RDONLY);
AvbVBMetaData vbmeta = AvbReadVBMeta(fd);
// 4. 验证签名链
if (!AvbVerifyVBMeta(vbmeta, trusted_keys)) {
EnterRecovery();
}
}
十三、实际案例:AOSP 配置分析
13.1 Pixel 6 (Oriole) 的 super 分区配置
让我们以 Pixel 6 为例,分析实际设备中的动态分区布局:
bash
# 从 Pixel 6 读取 lpdump 输出 (Android 13)
super: 5,345,285,376 bytes (5GB)
Slot 0:
逻辑分区 (共 6 个):
Name Size Group
system_a 1,572,864,000 default
system_b 1,572,864,000 default
vendor_a 536,870,912 default
vendor_b 536,870,912 default
product_a 805,306,368 google_dynamic_updatable
product_b 805,306,368 google_dynamic_updatable
空闲空间: ~523,000,000 bytes (~500MB)
这个配置反映了几个关键设计决策:
- A/B 对称分配:两个 slot 的大小完全一致,这是 A/B 升级的基本要求
- product 单独分组 :
google_dynamic_updatable组允许 Google Play 直接更新 product 分区,而无需完整 OTA - 500MB 预留空间:用于应对未来 OTA 可能增长的分区
13.2 定制 ROM 的 BoardConfig 示例
对于自定义 ROM 开发者,BoardConfig.mk 的配置示例如下:
makefile
# 启用动态分区
BOARD_SUPER_PARTITION_SIZE := 5368709120 # 5GB
BOARD_SUPER_PARTITION_GROUPS := default
# default group 包含所有 A/B 分区
BOARD_SUPER_PARTITION_GROUP_DEFAULT_SIZE := 5368709120
# 分区大小
BOARD_SYSTEMIMAGE_PARTITION_SIZE := 1572864000
BOARD_VENDORIMAGE_PARTITION_SIZE := 536870912
BOARD_PRODUCTIMAGE_PARTITION_SIZE := 805306368
# 启用 AVB
BOARD_AVB_ENABLE := true
BOARD_AVB_MAKE_VBMETA_IMAGE_ARGS += --set_hashtree_disabled_flag
# 文件系统
BOARD_SYSTEMIMAGE_FILE_SYSTEM_TYPE := ext4
BOARD_USERDATAIMAGE_FILE_SYSTEM_TYPE := f2fs
关键提醒 :修改动态分区配置后,必须执行
make clean重新编译,否则旧的分区镜像大小与新的元数据不匹配,会导致lpmake失败。
十四、常见问题与调试技巧
14.1 调试工具汇总
bash
# 1. 查看设备 super 分区元数据
adb shell lpdump
# 2. 查看逻辑分区的 device-mapper 映射
adb shell dmsetup table
# 输出示例:
# system_a: 0 2048000 linear /dev/block/sda16 2048000
# 3. 查看分区设备
ls -l /dev/block/by-name/
# super -> /dev/block/sda16
# system_a -> /dev/block/dm-0
# vendor_a -> /dev/block/dm-1
# 4. 分析 super 镜像
lpdump super.img
lpunpack super.img out/
# 5. 十六进制查看元数据
xxd -l 128 super.img | head -20
14.2 常见错误及解决方案
| 错误 | 原因 | 解决方案 |
|---|---|---|
lpdump: Invalid magic |
元数据损坏或非 super 镜像 | 确认文件是有效的 super 分区镜像 |
lpmake: Partition size exceeds group limit |
分区总大小超过 group 限制 | 调整 BOARD_SUPER_PARTITION_GROUP_*_SIZE |
fastboot: No such partition |
GPT 中没有 super 分区 | 检查分区表或刷写正确的 GPT |
liblp: CRC mismatch |
元数据校验失败 | 尝试另一份 metadata slot(--slot=1) |
update_engine: Not enough space |
OTA 时空间不足 | 清理 cache 或增大 super 分区 |
14.3 手动修复元数据
在极端情况下,如果 super 分区的元数据损坏,可以尝试手动修复:
bash
# 1. 备份当前扇区
dd if=/dev/block/sda16 of=/tmp/super_backup.img bs=4M
# 2. 读取元数据区域
dd if=/dev/block/sda16 of=/tmp/metadata.bin bs=512 count=32
# 3. 使用 lpdump 诊断
lpdump /tmp/super_backup.img
# 4. 如果有备用 slot,尝试切换
lpdump --slot=1 /tmp/super_backup.img
警告:直接操作分区数据风险极高,仅限开发者调试设备使用。生产环境应通过 fastboot 或 OTA 刷写完整镜像。
14.4 实战:从 super.img 中提取并分析某个逻辑分区
以下是完整的"从 super.img 提取并分析 system 分区"的实战流程:
bash
# Step 1: 先查看 super 镜像的元数据
lpdump super.img
# 输出中可以看到 system_a 的 extent 信息:
# system_a: size=1GB, extents=[0: 65536, 2048000]
# Step 2: 使用 lpunpack 提取所有逻辑分区
lpunpack super.img extracted/
# Step 3: 检查提取的分区
ls -lh extracted/
# -rw-r--r-- system_a.img (1GB)
# -rw-r--r-- vendor_a.img (500MB)
# Step 4: 挂载提取的 system 分区
sudo mount -o loop,ro extracted/system_a.img /mnt/android_system/
ls /mnt/android_system/
# bin build.prop etc framework lib media ...
# Step 5: 分析 build.prop 确认版本
cat /mnt/android_system/build.prop | grep "ro.build.version"
# Step 6: 卸载
sudo umount /mnt/android_system/
直接 dd 提取特定分区的方法(不依赖 lpunpack):
bash
# 直接从 super 分区中 dd 出 system_a 的数据
# 假设 lpdump 显示 system_a 的 extent 为:
# [0: 65536 sectors, 2048000 sectors]
# 1 sector = 512 bytes
# 偏移 = 65536 * 512 = 33554432 bytes
# 大小 = 2048000 * 512 = 1048576000 bytes
dd if=super.img of=system_a_direct.img \
bs=512 skip=65536 count=2048000
# 验证提取的镜像
file system_a_direct.img
# system_a_direct.img: Linux rev 1.0 ext4 filesystem data
# 检查文件系统完整性
e2fsck -n system_a_direct.img
十五、总结与展望
15.1 动态分区的核心优势回顾
- 空间池化:将多个逻辑分区合并到一个物理分区中管理,消除传统分区之间的空间浪费
- 弹性调整:可以在 OTA 升级时动态调整各分区大小,适配不同版本的容量需求
- 快照回滚:结合虚拟 A/B 和 COW,实现安全的 OTA 升级和回滚
- 元数据开销小:整个元数据通常仅占用 64KB~128KB,相对分区大小可以忽略不计
15.2 技术演进趋势
| Android 版本 | 动态分区相关变化 |
|---|---|
| Android 10 | 引入动态分区,super 作为物理容器 |
| Android 11 | 引入 Virtual A/B,COW 快照 |
| Android 12 | 支持压缩的 super 镜像(稀疏格式优化) |
| Android 13 | 完善 snapshot 机制,减少 OTA 空间要求 |
| Android 14+ | 进一步优化逻辑分区布局和空间效率 |
15.3 对开发者意味着什么
对于 Android 系统开发者、BSP 工程师和自定义 ROM 开发者而言,理解动态分区意味着:
- 编译系统配置:需要准确配置 BoardConfig.mk 中的分区参数
- 刷写流程变化:不再是逐个刷写 system/vendor,而是刷写 super 整体
- 调试复杂度增加:需要掌握 lpdump、lpunpack 等新工具
- OTA 测试要求更高:必须充分验证分区调整逻辑
bash
# 一条命令掌握 super 分区全貌
adb shell lpdump && \
adb shell dmsetup table | grep linear && \
ls -l /dev/block/dm-*
本文从数据结构到源码实现,从编译配置到调试排查,系统性地分析了 Android 动态分区(super)的完整技术栈。希望对从事 AOSP 开发、系统移植和省固件优化的读者有所帮助。
参考资料:
- Android 官方文档:Dynamic Partitions - source.android.com/docs/core/ota/dynamic_partitions
- AOSP 源码:system/liblp/ - android.googlesource.com/platform/system/liblp/
- AOSP 源码:build/make/tools/releasetools/ - android.googlesource.com/platform/build/
- Android Verified Boot - source.android.com/docs/security/features/avb