13、多系统启动:Dual Boot原理、使用Bootloader引导多个系统、Kexec硬重启技术
多系统启动,说白了就是一台设备上装两个甚至多个操作系统。你按电源键,然后选今天翻谁的牌子。嗯,这个场景在PC上很常见,但在Android设备上,其实也有不少玩法。
我记得刚入行那会儿,有个项目需要在同一台平板上跑Android和Linux。客户要求一键切换,不能刷机。当时我第一反应是——这不就是Dual Boot嘛。但真正做起来,坑比想象的多。
13.1 Dual Boot的基本原理
Dual Boot的核心思路很简单:多个系统共存,但同一时刻只有一个在运行。它们共享硬件,但各自拥有独立的分区。
在Android设备上,典型的双系统布局是这样的:
| 分区 | 用途 | 说明 |
|---|---|---|
| boot_a | 系统A内核+ramdisk | 主系统启动镜像 |
| boot_b | 系统B内核+ramdisk | 备用系统启动镜像 |
| system_a | 系统A用户空间 | /system 分区 |
| system_b | 系统B用户空间 | 另一个系统的/system |
| userdata | 用户数据 | 两个系统共用或独立 |
你可能会问:为什么要有两套boot和system分区?因为这样切换时不需要覆盖任何东西。Bootloader只需要决定加载哪一套。
关键点:Dual Boot不是虚拟化,不是容器。它是物理分区级别的隔离。两个系统互不知晓对方的存在。
13.2 使用Bootloader引导多个系统
Bootloader是Dual Boot的调度员。它负责在开机时读取某个标志位,然后决定加载哪个boot分区。
最常见的做法是——利用misc分区。misc分区里存了一个小结构体,里面有个字段叫bootloader_message。Bootloader启动时先读这个字段:
// 伪代码:Bootloader中的多系统引导逻辑
struct bootloader_message {
char command[32]; // "boot-linux" 或 "boot-android"
char status[32];
char recovery[768];
};
void bootloader_main() {
struct bootloader_message msg;
read_misc_partition(&msg);
if (strcmp(msg.command, "boot-linux") == 0) {
boot_linux_kernel(); // 加载 boot_linux 分区
} else if (strcmp(msg.command, "boot-android") == 0) {
boot_android_kernel(); // 加载 boot_a 或 boot_b
} else {
boot_default(); // 默认启动 Android
}
}
我在项目中遇到过一个问题:用户切换系统后,misc分区被Recovery模式意外覆盖了。结果设备卡在启动选择界面,两个系统都进不去。后来我加了一个校验机制——misc分区写入前先计算CRC,Bootloader启动时校验CRC,不合法就回退到默认系统。
个人建议:不要把misc分区当成唯一的决策依据。最好在boot分区头部也存一个magic number,Bootloader先读boot分区头,确认镜像有效再跳转。这叫双重保险。
13.3 Kexec硬重启技术
传统的Dual Boot有个痛点:必须重启。你想从Android切到Linux,得关机、重启、Bootloader重新选。这个过程少说十几秒,用户体验很差。
Kexec就是为了解决这个问题而生的。它允许你在当前系统运行时,直接加载并跳转到另一个内核。说白了就是——软重启,但硬切换。
Kexec的工作原理分三步:
- 加载新内核:当前系统(比如Android)通过kexec工具,把目标内核(比如Linux)加载到内存的指定位置。
- 关闭外设:kexec会调用设备驱动的shutdown回调,把硬件恢复到安全状态。
- 跳转执行:CPU直接跳转到新内核的入口点,开始执行。
来看一个实际的kexec命令示例:
# 在Android系统中,加载Linux内核
kexec -l /data/linux_kernel \
--initrd=/data/initrd.img \
--command-line="console=ttyS0,115200 root=/dev/mmcblk0p5"
# 执行跳转
kexec -e
执行kexec -e后,Android系统就彻底退出了。Linux内核接管硬件。整个过程不需要Bootloader参与,也不需要重启硬件。
注意:Kexec不是万能的。它要求所有设备驱动都能正确shutdown。我曾经在一个项目上踩过坑——GPU驱动没有实现shutdown回调,kexec跳转后新内核一初始化GPU就死机。最后只能硬着头皮给GPU驱动打补丁。
13.4 三种方案的对比
我把常见的多系统启动方案整理了一下,方便你对比:
| 方案 | 切换速度 | 硬件要求 | 复杂度 | 典型场景 |
|---|---|---|---|---|
| 传统Dual Boot(重启) | 慢(10-30秒) | 低 | 低 | 开发板、双系统手机 |
| Kexec硬重启 | 快(1-3秒) | 中(驱动需支持) | 中 | 嵌入式Linux、快速切换场景 |
| KVM虚拟化 | 即时 | 高(硬件虚拟化支持) | 高 | 服务器、容器化场景 |
我个人更倾向于Kexec方案。原因很简单——它不需要修改Bootloader,也不需要额外分区。只要内核支持,就能在用户空间完成切换。对于Android设备来说,这意味着你可以保留原厂Bootloader不变,只在系统层做文章。
13.5 多系统启动的完整流程
下面这张图展示了从按下电源键到系统启动的完整链路:
这个流程里,最关键的决策点就是misc分区的标志位。你可以在用户空间通过一个简单的app来修改这个标志位,然后触发重启。下次开机,Bootloader就会按照你的选择来引导系统。
避坑指南:我曾经遇到一个情况——用户频繁切换系统,导致misc分区写入次数过多,Flash块损坏。后来我改用persistent属性分区来存标志位,配合wear-leveling算法,问题就解决了。嗯,Flash的写入寿命是个容易被忽略的点。
13.6 实际项目中的经验总结
做了这么多年底层,我总结了几条关于多系统启动的铁律:
- 分区规划要提前做——一旦量产,分区大小就改不了了。我建议给每个系统预留至少20%的余量。
- Bootloader要支持回滚——如果新系统启动失败,Bootloader应该能自动切回旧系统。这个机制叫
bootctrl,A/B分区方案里就有。 - Kexec不是银弹——它依赖驱动支持。如果你的设备有闭源驱动,大概率用不了kexec。这时候老老实实走传统Dual Boot。
- 用户数据要隔离——两个系统共用userdata分区很容易出问题。比如文件系统版本不一致、权限错乱。我建议每个系统独立data分区,或者用子卷挂载。
最后说一句:多系统启动看起来简单,但真正落地时,每个细节都可能让你加班到深夜。嗯,这就是底层开发的魅力所在。
公众号:蓝海资料掘金营,微信deep3321