3、A/B分区布局详解:系统分区、启动分区、数据分区的A/B布局、分区表设计
好,咱们接着聊。上一节我把A/B切换的核心逻辑讲清楚了,说白了就是两套系统互相备份。但具体这两套系统怎么在存储里摆?每个分区叫什么名字?大小怎么定?这才是今天要啃的硬骨头。
我刚开始接触A/B分区时,也犯过迷糊。当时拿着芯片厂给的参考分区表,一看密密麻麻几十个分区,头都大了。后来自己动手画了几张图,才真正搞明白。嗯,今天我就把这几张图「翻译」成文字,带你走一遍。
3.1 分区布局的「三驾马车」
一个典型的A/B系统,核心就三类分区:系统分区、启动分区、数据分区。你想想看,车机启动需要什么?引导程序、操作系统内核、用户数据。这三样东西,分别对应着boot、system、userdata。
在A/B方案里,除了userdata,其他两个都得准备双份。为什么?因为用户数据是动态的,不能随便切换版本。而系统和启动分区是静态的,可以随时切。
核心原则:系统分区和启动分区必须A/B双备份,数据分区保持单份。
3.2 分区命名规则:别小看这个细节
分区怎么命名?各家有各家的习惯。我见过最直观的命名方式是这样的:
boot_a -> 启动分区A副本
boot_b -> 启动分区B副本
system_a -> 系统分区A副本
system_b -> 系统分区B副本
userdata -> 用户数据分区(单份)
misc -> 杂项分区(存储切换状态)
我个人习惯在分区名后面加个下划线加字母,这样一看就知道是哪个槽位。有些厂商喜欢用「_a」「_b」后缀,也有些用「-a」「-b」。其实都行,只要bootloader能认出来。
我记得有一次,合作方的分区表里把boot_a写成了boota,结果bootloader死活找不到分区。查了半天,原来是命名不规范。这种坑,踩过一次就记住了。
3.3 分区表设计:一张表说清楚
下面这张表,是我在实际项目中用过的分区布局。以64GB eMMC为例,你可以参考一下:
| 分区名称 | 起始地址 | 大小 | 说明 |
|---|---|---|---|
| boot_a | 0x00000000 | 64MB | 启动分区A,存放u-boot |
| boot_b | 0x00400000 | 64MB | 启动分区B,与A完全对称 |
| system_a | 0x00800000 | 2GB | 系统分区A,存放Android/Linux系统 |
| system_b | 0x08800000 | 2GB | 系统分区B,与A完全对称 |
| vendor_a | 0x10800000 | 512MB | 厂商分区A,存放驱动和硬件抽象层 |
| vendor_b | 0x12800000 | 512MB | 厂商分区B |
| userdata | 0x14800000 | 剩余空间 | 用户数据分区,单份 |
| misc | 0xE8000000 | 16MB | 杂项分区,存储切换状态 |
小提示:分区大小不是随便定的。system分区至少得能装下完整系统镜像,还要留20%的余量。我一般按镜像大小的1.5倍来分配。
3.4 为什么boot和system必须对称?
你可能会问:为什么boot_a和boot_b必须一样大?为什么system_a和system_b必须完全对称?
原因很简单:A/B切换的本质是「原地交换」。当系统决定从A槽切换到B槽时,它只是修改了misc分区里的一个标志位。bootloader启动时读这个标志,决定从哪个槽启动。如果两个槽的大小不一样,那地址映射就乱了。
我曾经见过一个项目,为了省空间,把boot_b分得比boot_a小。结果切换过去之后,系统启动到一半就崩了。为什么?因为boot_b装不下新版本的u-boot。从那以后,我坚持一个原则:对称分区,大小必须一致。
3.5 数据分区为什么不做A/B?
这个问题,我经常被问到。数据分区(userdata)为什么不做双备份?
说白了,没必要。用户数据是动态的,每天都在变。如果做A/B,那每次切换时还得同步用户数据,复杂度直线上升。而且用户数据出问题,大不了恢复出厂设置。但系统分区出问题,车就开不了了。
所以行业共识是:系统分区做A/B保证可靠性,数据分区做单份保证简单性。
注意:虽然userdata是单份,但建议在升级前做一次数据备份。万一升级过程中掉电,至少能恢复到上一个版本。这个我在后面「异常处理」章节会详细讲。
3.6 分区表在代码里怎么定义?
分区表通常定义在bootloader的配置文件中。以U-Boot为例,一般写在include/configs/目录下的头文件里。代码大概长这样:
/* 分区表定义 */
#define PARTS_DEFAULT \
"uuid_disk=${uuid_gpt_disk};" \
"name=boot_a,size=64MiB,uuid=${uuid_gpt_boot_a};" \
"name=boot_b,size=64MiB,uuid=${uuid_gpt_boot_b};" \
"name=system_a,size=2GiB,uuid=${uuid_gpt_system_a};" \
"name=system_b,size=2GiB,uuid=${uuid_gpt_system_b};" \
"name=vendor_a,size=512MiB,uuid=${uuid_gpt_vendor_a};" \
"name=vendor_b,size=512MiB,uuid=${uuid_gpt_vendor_b};" \
"name=userdata,size=-,uuid=${uuid_gpt_userdata};"
注意最后一行size=-,表示占用所有剩余空间。这样设计的好处是:不管eMMC容量多大,userdata都能自动适配。
3.7 避坑指南:分区对齐
最后说一个容易忽略的点:分区对齐。eMMC的擦除块大小通常是512KB或1MB。如果分区起始地址没有对齐到擦除块边界,写性能会大幅下降。
我曾经在一个项目里,因为分区起始地址没对齐,导致OTA升级时写system分区花了20分钟。后来对齐之后,同样的数据只用了5分钟。差距就这么大。
所以我的建议是:所有分区起始地址都按1MB对齐。虽然会浪费一点点空间,但换来的是稳定的性能,值了。
好,这一节的内容就到这儿。分区布局是A/B切换的物理基础,搞懂了它,后面讲切换流程时你就不会晕。下一节,我们聊聊「启动流程中的A/B选择逻辑」,看看bootloader到底是怎么决定从哪个槽启动的。