本文主要包含动态分区的物理数据布局,内存核心数据结构和动态分区映射示例 3 个部分。
之前一直没有去了解动态分区,但随着最近重新研究 Android OTA 升级,发现动态分区是一个绕不过去的坎。
从 Android Q 引入动态分区,到 Android R/S 在动态分区之上增加虚拟分区管理, OTA 升级时需要对分区变更进行处理,不了解动态分区就无法深入 Android OTA 升级。
因此最近花了些时间阅读代码,学习 Android 动态分区。学习在 linux device mapper 机制之上,Android 系统对动态分区的各种操作处理。因此,这一系列没有包含 linux 底层 device mapper 驱动解读,后面看情况会考虑深入。
目前这几篇文章的大致规划如下,后续可能会有变动:
请原谅我总是忍不住想说一些空洞的废话。
动态分区管理的本质是什么?
分区数据是对磁盘上数据分布的描述,系统挂载磁盘时读取数据,在内存中建立相应数据结构,用于后续对磁盘进行管理。可见这里需要先提前生成分区数据,然后读取数据,一旦分区数据写入后就固定了。
动态分区,顾名思义就是分区是动态的,不是一成不变的,可以根据需要改变。动态分区管理的本质就是对分区数据的增删改查操作,操作的对象就是动态分区描述数据 metadata。
围绕 metadata 数据进行增删改查,就是 Android 动态分区管理的核心内容:
Android 动态分区最底层基于 Linux 的 device mapper 机制,将 super
分区的各个不同部分映射成 device mapper 的 linear 设备。因此,肯定有人会问,学习动态分区前需要先学习 device mapper 驱动吗?
答案是不需要,我就深入没有学习过 device mapper 驱动,但并不妨碍我对 Android 动态分区管理的理解。
注意:
我强调的是对Android 动态分区管理的理解,而不是动态分区底层机制的理解,如果你想知道动态分区最底层实现的细节,还是应该去读一读 device mapper 驱动。
Linux device mapper 相关驱动位于 drivers/md 目录下。
当然,在理解动态分区管理之前,能知道一些 device mapper 的基本原理就更好,我这里所谓的基本原理,是你能看懂下面这张图:
图 1. Device mapper 内核中各对象的层次关系
说下图 1 的重点:
Mapped Device
基于驱动 Target Driver
和内部的映射表 Mapping Table
来实现Mapped Device
可以由一个或多个 Target Device
映射组成,参与映射的 Target Device
本身也可能是虚拟设备所以,一个设备可能是真实的,也可能是虚拟的。对虚拟设备的访问会被 Target Driver
拦截,然后通过 Mapping Table
转发给另外一个设备。例如,在 Android Q 上,对 system_a
或 vendor_a
分区的访问被驱动拦截并转发为对 super
分区某个区域的访问。而这里的拦截以及转发对用户是透明的,用户根本不知道,也不需要知道他最终访问的到底是哪个设备。
如果不打算读 device mapper 驱动代码,但又希望在总体上对 device mapper 有比较全面的认识,推荐阅读《(转载)device mapper原理》这篇文章。
这篇文章源自 IBM 《Linux 内核中的 Device Mapper 机制》: https://www.ibm.com/developerworks/cn/linux/l-devmapper/,但该文的原始内容已经不可访问。
Android 从 Q 10 开始引入动态分区 super,将原来的 system_a
, system_b
, vendor_a
, vendor_b
等打包到到这个分区中。下面是 Android 官方的分区转换布局图:
这个图全网随处可见,好吧,我承认我绕不开这张图了~
来源: https://source.android.google.cn/devices/tech/ota/dynamic_partitions/implement
图 2. 转换为动态分区时的新物理分区表布局
既然新的 super 分区内部包含了多个原来的分区,那要如何才能识别这些内部的分区呢?答案就是引入数据来描述 super 分区的布局。
于是,在 super 分区开头存储用于描述分区布局的 metadata,和磁盘开头存储用于描述磁盘分区布局的 gpt 数据的道理一样。系统加载动态分区时读取 metadata,对其进行解析,在内存中建立 super 分区布局描述的 LpMetadata
结构体,就知道内部的各个分区都位于哪个地方。
换句话说,metadata
就是 LpMetadata
结构在 super
分区上的物理存储数据,而 LpMetadata
是 metadata
在内存中的数据结构。LpMetadata
中的信息会被转换成图 1 中的映射表Mappting Table
, 基于这个映射表,super
分区对应设备/dev/block/by-name/super
的不同部分被映射成多个虚拟设备,如/dev/block/mapper/system_a
, /dev/block/mapper/vendor_a
等。
在开始研究内存数据结构 LpMetadata 之前,先从宏观上看下物理存储的 metadata 是怎样的。
下图是我基于 Android 官方的布局图 (图 2. 转换为动态分区时的新物理分区表布局),对 metadata 部分细化后的的结构示意图。
图 3. 安卓动态分区布局及其 metadata
对 metadata,可以分层 3 个层次,见图 2 最右侧的 3 列:
第 1 层,super 分区(宏观)
第 2 层,metadata 数据(中观)
metadata 数据开始前预留了 4K 的空间,所以 metadata 数据从 4K 的偏移位置开始
metadata 数据由 Geometry 和 Metadata 两部分组成
为什么槽位有两个,但是这里的 Metadata 有 3 份呢?
我一开始也有这个疑惑,经过核对传递给 lpmake 的参数后发现确实是 3 份。
在 Android 编译生成 super.img 时,传递给 metadata 生成工具 lpmake 的参数 “
--metadata-slots 3
”,所以 Metadata 有 3 份。lpmake 工具只不过是一个干活的苦逼,上层的编译系统下指令说生成几份 Metadata 就生成几份。至于为什么要传递 “
--metadata-slots 3
” 而不是 “--metadata-slots 2
” 给 lpmake ?那就是builder_super_image.py
脚本的事情了,相关代码如下:def BuildSuperImageFromDict(info_dict, output): cmd = [info_dict["lpmake"], "--metadata-size", "65536", "--super-name", info_dict["super_metadata_device"]] ... if ab_update and retrofit: cmd += ["--metadata-slots", "2"] elif ab_update: cmd += ["--metadata-slots", "3"] else: cmd += ["--metadata-slots", "2"]
只有当 retrofit 为 true 时才为 2,为什么要这么做,去问谷大爷吧~
第 3 层,Geometry 和 Metadata(微观)
至于 Geometry 和 Metadata 内部每个数据结构的具体字段,请参考下一节的介绍。
super 分区前 4K 为保留数据,随后是 metadata 数据,从 1M 偏移开始的地方存储内部分区的 image 数据,每个内部分区的 image 存放位置都按照 1MB 对齐,因此 metadata 位于 super 分区 4KB ~ 1MB 的范围内
metadata 包括 Geometry 和 Metadata 两个部分,每个部分都有自己的 Primary 和 Bakcup 两组相同的数据,存放顺序是 Geometry(Primary), Geometry(Backup), Metadata(Primary), Metadata(Backup)。
对于 Metadata,其内部又有 3 份数据,分别对应于 Slot 0, Slot 1, Slot 2。当 Slot 0 启动时,读取 Slot 0 对应的 Metadata, 当 Slot 1 启动时,读取 Slot 1 对应的 Metadata,至于多出来的 Slot 2 什么时候会使用,目前没有系统研究。
所以,通常情况下,包括 Primary 和 Backup 数据一起,磁盘上可能有 6 份 Metadata。
OTA 升级时,如果当前在 Slot 0 上运行,要更新 Slot 1 分区,则在更新之前 Slot 1 前会更新 Slot 1 对应的 Metadata 以反应 Slot 1 的实际布局,反之亦然。
上一节说完 Android 动态分区描述数据在 super 分区的分布情况,这一节深入研究 metadata 具体的数据结构,不清楚的请转到 “3.3 metadata 数据小结
” 回顾一下。
动态分区的核心数据结构主要定义在以下文件中:
启动加载动态分区前,会读取动态分区的 metadata,解析 Geometry 数据,读取当前 Slot 对应的 Metadata,在内存中建立 LpMetadata 结构。
LpMetadata: Logical Partition Metadata
LpMetadata 的定义如下:
/* file: system/core/fs_mgr/liblp/include/liblp/liblp.h */
struct LpMetadata {
LpMetadataGeometry geometry;
LpMetadataHeader header;
std::vector partitions;
std::vector extents;
std::vector groups;
std::vector block_devices;
};
为了方便查看,我花了些时间画了个 LpMetadata 的结构示意框图:
图 4. 动态分区核心数据结构 LpMetadata
LpMetadata 主要分成两个部分,描述 metadata 整体结构的 geometry 和随后描述 metadata 细节部分,包括 header, partitions, extents, groups, block_devices。
接下来详细分析每一部分的结构。
LpMetadataGeometry 结构中最主要的数据有 3 个:
metadata_max_size
: 单个 Slot 对应的 Metadata 数据大小,默认是 64Kmetadata_slot_count
: 单组 Metadata 包含的 Metadata 数量,加上备份的 Metadata,所以分区一共有 metadata_slot_count
* 2 个 Metdata 数据logical_block_size
: 逻辑分区大小,确保各分区映射的原始数据按照 logical_block_size
大小对齐LpMetadataHeader 结构的 major_version
和 minor_version
字段定义了 metadata 数据结构的版本信息,目前 Android Q 版本为 10.0, Android R 和 Android S 的版本为 10.2,新版本在 header 的结尾添加了 flags 字段,并将 header 数据填充到 256 字节,两个版本之间的差别如下:
除了版本信息之外,LpMetadataHeader 最主要的还通过 LpMetadataTableDescriptor 子结构指出了随后的 partitions, extents, groups, block_devices 等数据所在的偏移位置(offset
), 数量(num_entries
) 以及单个数据的大小(entry_size
) 等信息。
LpMetadataPartition 结构存储了动态分区内部的分区信息,可以理解为动态分区的内部分区表。
除了 name
和 attributes
字段分别表示分区名字和属性外,group_index
用于表示分区所属的组别。
而 first_extent_index
和 num_extents
分别用于表示该分区第一个映射的 extents 的索引值,以及随后需要映射的 extents 数量,不过 Android super 分区内单个 image 连续存放,对应于 1 个 extent,所以相应分区的 num_extents
值为 1。
如果 system_a
的 image 在 super 分区内分两段存放,则此时针对这一单一的 image 就会有两个 extents, LpMetadataPartition 结构中的 num_extents
为 2。如果一下理解不了这两个字段到底是如何使用的,参考第 “5. Android 动态分区映射示例
” 节关于具体映射示例的说明。
LpMetadataExtent 结构中保存了 super 分区内部单个分区(如 system_a
) 在 super 内的信息。如果 super 分区中只包含了 syteam_a
和 vendor_a
的 image,则会有相应的两个 extent 数据;如果分区中 system_a
, system_b
, vendor_a
, vendor_b
的 image 都存在,则会有四个 extent 数据。
在 Android 动态分区中,单个 extent 的长度按照 512 byte 的 sector 为单位进行计算。
LpMetadataExtent 结构的成员有:
num_sectors
: 当前 extent 对应 image 包含的 sector 数量target_type
: 当前 extent 对应 image 映射的虚拟设备的类型,对于 Android 动态分区,其值为 0 (类型为 linear),即线性映射。target_data
: 当前 extent 对应 image 的在动态分区内的偏移地址(按 sector 数量计算)target_source
: 当前 extent 对应的 image 位于哪个动态分区设备中, 对应于 block_devices 数组的索引,Android 系统中默认只有一个设备 super,所以 block_devices 数组只有 1 个元素, 因此 target_source
取值为 0。如果你的动态分区中不只有 super 一个设备,则这里的 target_source
可能会是其他值。LpMetadataPartitionGroup 结构记录了动态分区内部的 group 信息,包括 group 的名称(name
),最大长度(maximum_size
) 和一些标识(flags
) 信息。
LpMetadataBlockDevice 结构记录了动态分区内的 device 信息,包括:
first_logical_sector
)alignment
和 alignment_offset
partition_name
和属性 flags
对于默认的 super 动态分区,则 :
first_logical_sector
= 2048, 即 1MB offset
alignment
= 1024576, alignment_offset
= 0,每个 image 按照 1MB 的边界进行对齐
size
, super 分区大小,按照 byte 进行计算
partition_name
= “super”
flags
, super 分区的设备属性信息,参考各种 LP_BLOCK_DEVICE_*
标识
这里以 Broadcom 平台某个产品在 Android Q 下 super 分区的生成和加载作为示例演示动态分区是如何生成,又是如何映射为虚拟设备的。
总体而言,该流程如下面的 图 5 所示,后面会对这个流程的各个部分进行解释:
图 5. 安卓动态分区映射转换示例
在 Android 的编译过程中,系统通过脚本 build/tools/releasetools/build_super_image.py
内部去调用 lpmake 工具生成 super.img 文件。
所以,在编译的 log 中查找 lpmake 就直接看到系统是如何去生成 super.img 的。
$ source build/envsetup.sh
$ lunch inuvik-userdebug
$ make PRODUCT-inuvik-userdebug dist -j40 2>&1 | tee make.log
$ grep -ni "lpmake" make.log
实际上系统中会多次调用 lpmake 去生成相应的 super.img,这几个操作间有什么区别留给你去发现了~这里不再赘述,只选取其中一个生成 super.img 的命令:
$ grep -ni lpmake make.log
979:2022-02-28 12:52:57 - common.py - INFO : Running: "lpmake --metadata-size 65536 --super-name super --metadata-slots 3 --device super:3028287488 --group bcm_ref_a:1509949440 --group bcm_ref_b:1509949440 --partition system_a:readonly:1077702656:bcm_ref_a --image system_a=out/target/product/inuvik/system.img --partition system_b:readonly:0:bcm_ref_b --partition vendor_a:readonly:104992768:bcm_ref_a --image vendor_a=out/target/product/inuvik/vendor.img --partition vendor_b:readonly:0:bcm_ref_b --sparse --output out/target/product/inuvik/super.img"
整理一下这里对 lpmake 的调用,如下:
lpmake --metadata-size 65536 --super-name super --metadata-slots 3 \
--device super:3028287488 \
--group bcm_ref_a:1509949440 --group bcm_ref_b:1509949440 \
--partition system_a:readonly:1077702656:bcm_ref_a \
--image system_a=out/target/product/inuvik/system.img \
--partition system_b:readonly:0:bcm_ref_b \
--partition vendor_a:readonly:104992768:bcm_ref_a \
--image vendor_a=out/target/product/inuvik/vendor.img \
--partition vendor_b:readonly:0:bcm_ref_b \
--sparse --output out/target/product/inuvik/super.img
所以,完全可以不用管安卓下的各种动态分区配置,直接用这样的命令调用 lpmake 生成 super.img。
由于上一节生成 super.img 的命令中带有 --sparse
选项,所以生成的 super.img 为 sparse image 格式,在解析前需要使用工具 simg2img 将其转换成 raw 格式。
$ simg2img out/target/product/inuvik/super.img out/super_raw.img
拿到 super_raw.img
以后就可以使用各种工具甚至手动进行解析了,这里我直接使用 lpdump 进行解析,结果如下:
$ lpdump out/super_raw.img
Metadata version: 10.0
Metadata size: 592 bytes
Metadata max size: 65536 bytes
Metadata slot count: 3
Partition table:
------------------------
Name: system_a
Group: bcm_ref_a
Attributes: readonly
Extents:
0 .. 2104887 linear super 2048
------------------------
Name: system_b
Group: bcm_ref_b
Attributes: readonly
Extents:
------------------------
Name: vendor_a
Group: bcm_ref_a
Attributes: readonly
Extents:
0 .. 205063 linear super 2107392
------------------------
Name: vendor_b
Group: bcm_ref_b
Attributes: readonly
Extents:
------------------------
Block device table:
------------------------
Partition name: super
First sector: 2048
Size: 3028287488 bytes
Flags: none
------------------------
Group table:
------------------------
Name: default
Maximum size: 0 bytes
Flags: none
------------------------
Name: bcm_ref_a
Maximum size: 1509949440 bytes
Flags: none
------------------------
Name: bcm_ref_b
Maximum size: 1509949440 bytes
Flags: none
------------------------
对 super.img 进行简单的解析,lpdump 工具已经足够了。如果不能满足要求,也可以自己使用其它方式解析。
重点说下解析的结果:
解析结果我已经详细列出来放到前面的 图 5 中了。
Android代码中,这部分操作通过调用库 liblp 内的函数完成,结束后在内存中生成 LpMetadata 结构数据。
在解析 super.img 生成 LpMetadata 结构以后,libdm 库内的函数会基于分区 partitions 和 条带 extents 内的信息创建映射表。
对于 system_a
和 vendor_a
分区,分别有以下映射:
/* system_a */
{
0, 2104888,
"/dev/block/by-id/super",
2048
},
/* vendor_a */
{
0, 205064,
"/dev/block/by-id/super",
2107392
}
换个表述方式就是:
/dev/block/by-id/super
” 分区的 2048 开始的 2104888 个 sector 映射到 “/dev/block/mapper/system_a
” 设备的 0 位置。/dev/block/by-id/super
” 分区的 2107392 开始的 205064 个 sector 映射到 /dev/block/mapper/vendor_a
设备的 0 位置。然后 linux 系统的 device mapper 驱动基于每个设备的映射表生成相应的虚拟设备,这样就可以和虚拟出来的 “system_a”, “vendor_a” 分区尽情的玩耍了,而不用管这些设备到底是真实的还是虚拟的。
画图和组织文字前后花了几天时间,终于快写完了,到总结时还真没想好怎么用几句话把动态分区给总结出来,这里就列举一些重点吧。
super 分区前 4K 为保留数据,随后是 metadata 数据,从 1M 偏移开始的地方存储内部分区的 image 数据,每个内部分区的 image 存放位置都按照 1MB 对齐,因此 metadata 位于 super 分区 4KB ~ 1MB 的范围内
metadata 包括 Geometry 和 Metadata 两个部分,每个部分都有自己的 Primary 和 Bakcup 两组相同的数据,存放顺序是 Geometry(Primary), Geometry(Backup), Metadata(Primary), Metadata(Backup)。
对于 Metadata,其内部又有 3 份数据,分别对应于 Slot 0, Slot 1, Slot 2。当 Slot 0 启动时,读取 Slot 0 对应的 Metadata, 当 Slot 1 启动时,读取 Slot 1 对应的 Metadata,至于多出来的 Slot 2 什么时候会使用,目前没有系统研究。
所以,通常情况下,包括 Primary 和 Backup 数据一起,磁盘上可能有 6 份 Metadata。
OTA 升级时,如果当前在 Slot 0 上运行,要更新 Slot 1 分区,则在更新之前 Slot 1 前会更新 Slot 1 对应的 Metadata 以反应 Slot 1 的实际布局,反之亦然。
不清楚的地方去看本文图 3。
详细的数据结构,我也不知道说点啥好,请参考本文图 4。
build_super_image.py
内部调用 lpmake 工具生成 super.img 文件liblp
库函数解析 super.img 头部的metadata,在内存中建立 LpMetadata 数据结构fs_mgr
基于 LpMetadata 内的分区信息,得到映射表,然后通过 libdm
库调用 linux 的 device mapper 驱动映射设备详细的示例分析参考本文图 5。
好了,5 张图搞懂动态分区原理就到这里了,剩下几篇将会分模块对安卓动态分区进行分析。
洛奇工作中常常会遇到自己不熟悉的问题,这些问题可能并不难,但因为不了解,找不到人帮忙而瞎折腾,往往导致浪费几天甚至更久的时间。
所以我组建了几个微信讨论群(记得微信我说加哪个群,如何加微信见后面),欢迎一起讨论:
在工作之余,洛奇尽量写一些对大家有用的东西,如果洛奇的这篇文章让您有所收获,解决了您一直以来未能解决的问题,不妨赞赏一下洛奇,这也是对洛奇付出的最大鼓励。扫下面的二维码赞赏洛奇,金额随意:
洛奇自己维护了一个公众号“洛奇看世界”,一个很佛系的公众号,不定期瞎逼逼。公号也提供个人联系方式,一些资源,说不定会有意外的收获,详细内容见公号提示。扫下方二维码关注公众号: