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到底是怎么决定从哪个槽启动的。