4. 双分区架构设计:A/B分区原理、双分区切换逻辑、分区表设计
双分区架构,圈内人常叫它 A/B 系统。说白了,就是设备里准备两套完整的系统固件,一套在跑,另一套闲着。我最早接触这个设计是在做车机 OTA 的时候,客户要求升级失败不能变砖,还得能回滚。嗯,A/B 分区就是干这个的。
4.1 A/B 分区原理
传统单分区升级,就像在独木桥上换轮胎。升级过程中一旦断电或写错,系统就起不来了。A/B 分区不一样,它把存储分成两个完全独立的槽位(Slot)。
- Slot A(当前槽):正在运行的固件,包含 boot、system、vendor 等分区。
- Slot B(备用槽):存放新固件或旧固件的备份,平时不参与启动。
升级时,系统在后台把新固件写入 Slot B。写完后,通过 Bootloader 切换启动槽位。下次重启,设备就从 Slot B 启动了。如果 Slot B 启动失败,Bootloader 自动回退到 Slot A。
核心思想:升级过程不影响当前运行的系统。用户甚至感觉不到设备在升级。
我在项目中遇到过一个问题:某款 IoT 网关,Flash 只有 256MB。A/B 分区要存两套系统,空间捉襟见肘。后来我们裁剪了内核模块,把不必要的驱动全去掉,才勉强塞下。所以,选型时一定要算好 Flash 容量。
4.2 双分区切换逻辑
切换逻辑是 Bootloader 的活。我习惯把切换逻辑分成三个步骤:标记、尝试、确认。
- 标记新槽位:升级完成后,系统在 misc 分区写入一个标志,告诉 Bootloader 下次启动用 Slot B。
- 尝试启动:Bootloader 读取标志,从 Slot B 加载内核。如果内核启动成功,用户空间的服务会发一个「健康确认」信号。
- 确认切换:健康服务收到信号后,把 misc 分区里的「尝试计数」清零,正式确认切换。如果启动失败,Bootloader 把计数加 1,超过最大尝试次数(比如 3 次),就回退到 Slot A。
避坑指南:我曾经把「尝试计数」放在文件系统里,结果文件系统挂载失败,计数读不出来,Bootloader 直接回滚了。后来我改成放在独立的 misc 分区,用裸读写,再也没出过问题。
为什么会设计「尝试计数」?因为有些故障不是立即暴露的。比如 Wi-Fi 驱动加载后,要等几秒才能连上网络。如果连不上,用户可能觉得设备坏了。所以,我一般给 3 次尝试机会,每次重启都重新尝试。
下面这张图展示了 A/B 分区的切换流程:
4.3 分区表设计
分区表是 A/B 架构的骨架。设计不好,后面全是坑。我一般把分区表分成三类:核心系统分区、数据分区、元数据分区。
| 分区名称 | 大小(典型值) | 作用 | 是否 A/B |
|---|---|---|---|
| boot_a / boot_b | 32MB | 存放内核和 dtb | 是 |
| system_a / system_b | 256MB - 1GB | 根文件系统 | 是 |
| vendor_a / vendor_b | 64MB | 硬件驱动和固件 | 是 |
| misc | 1MB | 存储启动标志、尝试计数 | 否 |
| data | 剩余空间 | 用户数据、应用配置 | 否 |
| cache | 64MB | 升级包缓存、日志 | 否 |
注意:misc 分区不能放在 A/B 槽里。如果 misc 也做双备份,Bootloader 就不知道该从哪个槽启动了。这是个死循环。
分区表在代码里怎么定义?以 U-Boot 为例,我习惯用 CONFIG_PARTITION_UUIDS 配合 分区表。下面是一个简化版的分区表定义:
// 以 512 字节扇区为单位
// 假设 Flash 总大小 4GB
partitions {
compatible = "fixed-partitions";
#address-cells = <2>;
#size-cells = <2>;
// misc 分区,固定偏移
partition@0 {
label = "misc";
reg = <0x0 0x200000>; // 2MB
};
// Slot A
partition@200000 {
label = "boot_a";
reg = <0x0 0x2000000>; // 32MB
};
partition@2200000 {
label = "system_a";
reg = <0x0 0x20000000>; // 512MB
};
// Slot B
partition@22200000 {
label = "boot_b";
reg = <0x0 0x2000000>; // 32MB
};
partition@24200000 {
label = "system_b";
reg = <0x0 0x20000000>; // 512MB
};
// 数据分区
partition@44200000 {
label = "data";
reg = <0x0 0xFFFFFFFF>; // 剩余空间
};
};
你想想看,如果分区大小没对齐 Flash 的擦除块(比如 NAND 的 128KB 块),写分区时就会跨块,性能下降不说,还可能写坏。我建议分区起始地址和大小都按擦除块对齐。
个人经验:我习惯在分区表里预留 1-2 个空分区,叫 reserved_a 和 reserved_b。万一后期要加新功能(比如安全固件分区),不用重新烧写整个分区表。
最后说一句,分区表设计不是一次性的。产品迭代过程中,分区大小可能会变。我见过一个团队,升级包写死了分区偏移,结果新版本固件大了 10MB,直接覆盖了后面的 data 分区。嗯,数据全丢了。所以,分区表最好用 ,支持动态调整,别用古老的 MBR。