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的工作原理分三步:

  1. 加载新内核:当前系统(比如Android)通过kexec工具,把目标内核(比如Linux)加载到内存的指定位置。
  2. 关闭外设:kexec会调用设备驱动的shutdown回调,把硬件恢复到安全状态。
  3. 跳转执行: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 多系统启动的完整流程

下面这张图展示了从按下电源键到系统启动的完整链路:

多系统启动流程 1. 设备上电 2. Bootloader 启动 3. 读取 misc 分区标志位 选择系统 系统A (Android) 系统B (Linux) 加载 boot_a 分区 加载 boot_b 分区

这个流程里,最关键的决策点就是misc分区的标志位。你可以在用户空间通过一个简单的app来修改这个标志位,然后触发重启。下次开机,Bootloader就会按照你的选择来引导系统。

避坑指南:我曾经遇到一个情况——用户频繁切换系统,导致misc分区写入次数过多,Flash块损坏。后来我改用persistent属性分区来存标志位,配合wear-leveling算法,问题就解决了。嗯,Flash的写入寿命是个容易被忽略的点。

13.6 实际项目中的经验总结

做了这么多年底层,我总结了几条关于多系统启动的铁律:

  • 分区规划要提前做——一旦量产,分区大小就改不了了。我建议给每个系统预留至少20%的余量。
  • Bootloader要支持回滚——如果新系统启动失败,Bootloader应该能自动切回旧系统。这个机制叫bootctrl,A/B分区方案里就有。
  • Kexec不是银弹——它依赖驱动支持。如果你的设备有闭源驱动,大概率用不了kexec。这时候老老实实走传统Dual Boot。
  • 用户数据要隔离——两个系统共用userdata分区很容易出问题。比如文件系统版本不一致、权限错乱。我建议每个系统独立data分区,或者用子卷挂载。

最后说一句:多系统启动看起来简单,但真正落地时,每个细节都可能让你加班到深夜。嗯,这就是底层开发的魅力所在。


公众号:蓝海资料掘金营,微信deep3321