做 OpenHarmony 开发,最容易让人一头雾水的不是上层应用 API,而是开机那几秒钟的日志和分区。我以前拿到一块新板卡,习惯把 boot、system、vendor 几个镜像通过 fastboot 一刷,然后信心满满上电,结果屏幕要么一直停在开机动画,要么干脆刷到一半就报 partition not found。折腾几次之后才明白,启动链路和分区是一张地图,不看地图刷机,就是在盲人摸象。这篇文章从实际调试的角度,把 OpenHarmony 的启动链路和分区规划讲透,适合正在搭 OpenHarmony 开发环境、移植新板子、或者折腾开源鸿蒙 x86 PC 版的读者。
很多人以为分区只是安装系统时选一下盘符、设置一下容量,但 OpenHarmony 设备上的分区和启动过程是强耦合的。Bootloader 要找到 boot 分区,内核要找到 system 分区,init 又要挂载 vendor 和 userdata,任何一个分区缺失、偏移、或者文件系统损坏,都会让整个启动链路中断。所以我会先讲启动链路的分阶段逻辑,再讲分区表如何与之对接,最后给出一套排查启动失败的操作方法。
1. 为什么建议先搞懂启动链路和分区
1.1 启动链路是系统运行的“安全检查单”
启动链路可以理解成一条流水线:每个阶段都有明确的输入和输出,上一个阶段完成后,才把控制权交给下一个阶段。在 OpenHarmony 标准系统里,最简化的链路是 Bootloader -> Kernel -> init -> appspawn -> 桌面/应用。任何一步断了,设备表现都不一样,有的会直接黑屏,有的卡在Logo,有的无限重启。
我调试过一块 RK 系列的开发板,问题现象是上电后串口打印几行内核日志就停住,没有任何报错。当时我第一反应是内核没起来,后来对照日志发现内核已经完成解压,也打印了 init 进程 PID,但 system 分区挂载失败。这说明问题不在内核本身,而在分区表或者文件系统。假如我当时对启动链路没有整体概念,肯定会把时间浪费在内核配置上。
所以,先花点时间把启动链路的每个阶段理清楚,比急于烧录和改代码更重要。链路里的每一个环节,都会反映成不同的日志特征,只要你知道正常日志应该长什么样,就能在异常时快速定位是哪一段出了问题。
1.2 分区是启动链路的“物料仓库”
分区表不只是一张“磁盘切分图”,它更像是启动链路各阶段约定好的物料仓库。Bootloader 从固定位置读取镜像,内核根据 cmdline 里的 root 参数寻找根文件系统,init 再按照 fstab 配置去挂载 vendor、system、userdata。如果某个仓库的物理位置偏了,或者里面放的东西格式不对,启动链路自然就断了。
我见过有人为了省空间,把 system 分区从 2GB 缩到 1.2GB,结果 system.img 实际体积 1.8GB,烧录时 fastboot 直接报“data too large”。这种问题看似是分区容量计算失误,本质上是没把分区当作启动链路的一环去规划。分区不只是空间容器,它还承载了启动顺序、只读/可写属性、文件系统类型、以及 A/B 版本切换等关键信息。
日常维护中经常听到的“分区卸载”,也属于这个范畴。比如 system 分区在运行期是只读挂载的,你不能在用户态随便 umount,否则系统核心服务和库文件突然就找不到入口,紧接着就是各种进程崩溃。理解分区的含义之后,你会懂得哪些分区可以动,哪些分区不能碰,以及为什么不能碰。
2. 启动链路的核心路径与阶段拆解
2.1 Bootloader 阶段:引导器把控制权交给谁
芯片上电后,首先执行的是固化在 ROM 里的代码,然后由 ROM 加载 Bootloader。OpenHarmony 在不同硬件平台上的 Bootloader 并不统一,ARM 平台常见的是 U-Boot,有些芯片方案也会用 ATF+U-Boot 的组合,x86 平台上则更多走 UEFI 引导,甚至直接用 EFI stub 启动内核。
Bootloader 要做的事情很多,包括初始化 DDR、时钟、串口、存储控制器,然后根据启动键状态判断进入刷机模式还是正常启动模式。正常启动时,它读取 GPT 分区表中的 boot 分区,把内核镜像加载到内存,再跳转过去。x86 场景下,UEFI 固件会读取 ESP 分区里的引导文件,再由引导文件加载内核。
这一阶段最容易出问题的点有三个:一是 boot 分区里的镜像与 Bootloader 不匹配,二是分区表被破坏导致 Bootloader 找不到 boot 分区,三是启动参数 cmdline 配置错误。比如 cmdline 里指定了 root=/dev/mmcblk0p12,但 GPT 里的 system 分区实际在 p10,内核自然找不到根文件系统。
所以,当你拿到一块新板子时,我建议先确认该平台的 Bootloader 代码和分区表配置是否对应。不要拿着一份 A 平台的 GPT xml,烧到 B 平台,这种“移植”最容易在第一阶段就翻车。
2.2 内核启动:从压缩内核到根文件系统
Bootloader 跳转到内核后,内核会先解压自身,初始化 CPU、内存管理、中断、驱动模型等基础子系统。之后会根据设备树或 ACPI 表来识别硬件,并尝试挂载根文件系统。在 OpenHarmony 标准系统中,内核通常不是直接挂载 system 分区,而是先加载一个 initrd/ramdisk,由 ramdisk 里的脚本或 init 进程去完成后续操作。
为什么要多一层 ramdisk?因为 system 分区可能需要额外的驱动才能访问,比如某些 eMMC 控制器驱动或加密模块驱动。没有 ramdisk,内核可能因为无法识别存储设备而挂载不了 system。举个例子,在 x86 PC 上安装 OpenHarmony 时,如果内核不包含对应的 NVMe/SATA 驱动,不加载 initrd 就无法从系统盘启动。这就是为什么 PC 版镜像必须预留一个 init_boot 或 initrd 分区。
内核阶段如果失败,最常见的日志是 Kernel panic - not syncing: VFS: Unable to mount root fs。看到这条日志,优先检查三件事:cmdline 中的 root 参数是否正确、ramdisk 是否烧录成功、存储设备驱动是否工作正常。我遇到过 root 分区号写错的情况,改一下 cmdline 就恢复了。
2.3 init 进程与系统服务:OpenHarmony 用户态如何接管
内核层起来之后,用户态的第一个进程是 init。OpenHarmony 的 init 进程和很多 Linux 发行版思路类似,但有自己的行为:它会解析 /system/etc/init 下的配置,加载 SELinux 策略,然后按照 fstab 挂载 vendor、system、userdata 等分区。
这一步非常关键,因为 OpenHarmony 的分区挂载并不全放在内核 cmdline 里,而是由 init 根据分区表信息统一处理。init 会先挂载只读的 vendor/system,再检查必要的服务目录,最后拉起 appspawn 进程来孵化应用进程。如果 vendor 分区里某个硬件抽象服务起不来,系统可能一直停在开机 Logo,却没有内核 panic 日志。
日志特征也不同。内核日志会先出现,然后出现 init 日志,再出现 service 启动日志。你可以看到类似“Start init service ***”或“Service *** failed to start”的信息。如果某个服务反复被拉起又退出,大概率是它依赖的分区挂载失败,或者 SELinux 上下文不对。这时候不要只盯着服务本身,还要回头看看 fstab 里的挂载点和分区文件系统状态。
3. 分区体系:每个分区承担的启动职责
3.1 一块开发板上的典型分区表
OpenHarmony 的标准系统在不同平台上的分区命名会有差异,但核心职责是相近的。以下是我在开发板上常看到的一套 GPT 分区布局,格式比较典型:
| 分区名 | 常见挂载点/用途 | 文件系统 | 可否擦除 |
|---|---|---|---|
| loader | Bootloader 镜像 | raw | 尽量不擦 |
| boot | 内核镜像 | raw | 可重烧 |
| init_boot | initrd/ramdisk | raw | 可重烧 |
| vendor_boot | 厂商内核/驱动补丁 | raw | 可重烧 |
| system | 系统框架、基础库、应用框架 | ext4/erofs | 可重烧 |
| vendor | 厂商适配、硬件服务 | ext4/erofs | 可重烧 |
| userdata | 用户应用数据 | f2fs/ext4 | 可擦除 |
| misc | 启动模式与升级信息 | raw | 可擦除 |
| recovery | 恢复系统 | ext4/raw | 建议保留 |
| param | 启动参数与持久化配置 | raw | 谨慎擦除 |
为什么需要这么多分区?因为不同分区有不同的访问频率和写入需求。system 和 vendor 在运行期几乎只读,系统升级时整包替换,因此用只读文件系统更合适;userdata 需要频繁读写,文件系统要具备掉电保护和磨损平衡能力;misc 通常只是几个字节的 raw 区域,用于告诉 Bootloader“下一次启动进入 recovery 还是正常启动”。
很多从 Linux 桌面转过来的开发者,会习惯性地问:能不能直接把 system、vendor、userdata 合成一个大分区?从启动角度讲,这么做不是完全不行,但会带来两个麻烦:系统升级时的差分更新很难做,恢复模式也无法独立于主系统运行。OpenHarmony 在标准系统里默认拆分这些分区,本质上还是为了升级和安全启动的可靠性。
3.2 分区大小与文件系统选择
分区大小不能拍脑袋定,更不能所有分区统一大小。我一般会把分区大小和镜像实际体积之间保留 15% 到 20% 的余量,尤其是 system 和 vendor。因为新版本系统会加功能,硬件服务也会增加,分区太小会导致后续版本升级时烧不进去。
文件系统选择要结合分区用途:system/vendor 建议用 ext4 或 EROFS,纯只读场景下 EROFS 有更好的压缩率,也避免意外写入破坏系统;userdata 用 f2fs 会更适合闪存设备,对随机写入和磨损均衡都有优化。我见过有人把 userdata 也做成 ext4,短时间没问题,长期跑下来掉电场景下的恢复速度会明显不如 f2fs。
还需要关注 4K 对齐。无论是 eMMC、UFS 还是 SSD,颗粒擦写的最小单位决定了分区起始扇区最好是 4K 的整数倍。用 gdisk/parted 建分区时,如果不设置对齐参数,分区起始扇区可能落在非对齐位置,烧录后性能会比较差,极端情况下还会出现奇怪的 IO 错误。命令行里我习惯用parted -a optimal来保障对齐。
3.3 Fastboot 烧写与分区表对齐
OpenHarmony 开发板最常用的烧录方式是 fastboot。进入 fastboot 后,先执行fastboot devices确认设备连接,然后按顺序烧写分区。以下是一组我常用的命令顺序:
fastboot flash boot_linux boot_linux.img fastboot flash init_boot init_boot.img fastboot flash vendor vendor.img fastboot flash system system.img fastboot flash userdata userdata.img fastboot erase misc fastboot reboot注意我为什么在烧完后要 erase misc。misc 分区如果残留上一次的启动模式标记,可能导致设备反复进入 fastboot 或 recovery,而不是正常启动。清掉它可以让 Bootloader 判断为普通冷启动。
如果 fastboot 报partition 'xxx' not found,不要急着重新烧镜像,先检查烧录工具使用的分区表文件名是否和实际设备一致。很多平台的分区表不是保存在 Bootloader 里,而是在烧录工具 cfg/xml 中定义。你改了 GPT,但工具里的分区名还是旧的,fastboot 就找不到目标分区。这个坑出现的频率非常高,我建议在工程目录里放一份当前使用的分区表快照,和镜像一起管理。
4. 实操:从启动日志定位分区问题
4.1 串口日志快速抓取
启动链路问题,没有日志等于盲调。ARM 开发板一般有调试串口,用一根 USB 转串口线连接开发板和电脑,然后在 Ubuntu 上执行:
sudo apt install minicom sudo minicom -D /dev/ttyUSB0串口参数通常设为 115200 8N1。如果接上之后没有任何输出,先确认波特率是否匹配,再检查串口接线是否交叉。这里多说一句,有些开发板需要按着特定按键才能进入烧录模式,否则串口只打印 Bootloader 顶部信息就断了。
在 x86 设备上不一定有调试串口,可以临时在内核 cmdline 里加上console=tty0,把内核日志输出到显示器。不过很多 PC 厂商默认隐藏了启动细节,建议先用 OpenHarmony PC 版的安装盘看能否进入引导界面,再逐步排查。
抓日志时,我一般会开启完整输出,尽量把 fasthash 和 init 阶段的每一行都保留。启动成功后,使用dmesg | grep -i mount查看分区挂载相关记录,使用cat /proc/partitions查看内核识别的分区情况,使用ls -l /dev/block/by-name/查看分区软链接是否完整。这三个命令组合起来,基本能看清楚分区在系统内部是否已经注册。
4.2 常见启动失败模式速查
我把这几年遇到过的启动分区问题整理成了一张速查表,方便读者在卡住时快速对照:
| 故障现象 | 可能原因 | 优先排查动作 |
|---|---|---|
| 反复重启,无法进入系统 | system/vendor 镜像损坏,或 userdata 文件系统异常 | 重新烧写 system/vendor,fastboot erase userdata |
| 卡在开机 Logo,串口无新打印 | init 拉起服务失败,vendor 分区里有服务崩了 | 检查 init 日志,确认 vendor 挂载是否成功 |
| Kernel panic: Unable to mount root fs | cmdline 根设备参数错误,init_boot 未烧录 | 检查 cmdline、init_boot 分区、存储驱动 |
| x86 启动找不到 EFI 分区 | 安装时没有创建 ESP,或引导文件未放入 EFI | 用 gdisk 确认 EF00 类型分区是否存在 |
| 第一次能开机,重启后就挂 | misc/param 分区数据异常,或 A/B slot 信息缺失 | fastboot erase misc,必要时fastboot set_active a |
| 烧录时报 partition not found | 烧录工具分区表与设备 GPT 不一致 | 对比 xml/gpt 配置,重新生成分区表 |
每次遇到启动问题,先不要乱刷所有分区。我的习惯是“从后往前查”:先确认 Bootloader 是否启动,再看内核日志是否到达 init,最后才动手动分区。盲目的fastboot erase会把可复现的问题掩盖掉,反而增加排查难度。尤其是 param 和 misc,虽然可以擦,但擦掉之前最好先备份。
5. 分区工具与日常维护经验
5.1 开发机上处理镜像的常用工具
很多读者是从 Windows 桌面过来的,提到分区就想到傲梅分区助手之类的工具。如果只是处理普通 PC 磁盘,这类图形工具没问题;但 OpenHarmony 镜像文件不是物理磁盘,它是包含 GPT 分区的镜像文件,我更习惯在 Linux 开发机上用命令行工具,比如 parted、gdisk、losetup 和 dd。
查看一个 system.img 里的分区表信息,可以这样:
gdisk -l system.img如果你需要把镜像里的某个分区挂载出来查看内容,用 losetup 比较安全:
sudo losetup -Pf system.img ls /dev/loop0p* sudo mount /dev/loop0p1 /mnt/system_partlosetup 的-P参数会自动扫描分区表,生成 /dev/loop0p1、/dev/loop0p2 等节点。这样就不用手动计算 dd 的 skip 和 count 了,效率高很多,也不容易把偏移量算错。
如果你确实需要手动提取某个分区,可以用 dd,但要注意换算。GPT 分区起始扇区通常不是 0,要从gdisk -l的输出里找到 Sector 起始位置。例如起始 sector 是 4096,扇区大小是 512 字节,那么文件的偏移量是 4096 * 512 = 2097152 字节。
dd if=system.img of=extracted.img bs=512 skip=4096 count=1048576这种手动方式适合提取分区内容,但如果你只想检查或修改,losetup 显然更稳妥。
5.2 常见坑位与规避建议
第一,不要随意删除 recovery 分区。OpenHarmony 的升级和恢复模式依赖 recovery 分区,删了之后,系统可能进不了 OTG 刷机,也没法在系统崩溃时恢复。开发阶段为了省空间删掉它,等砖了才后悔。
第二,不要在生产环境把 userdata 做成只读文件系统。userdata 承载用户数据,需要可写。有人为了让系统更“安全”,把 userdata 也改成 erofs,结果是应用一写数据就报错,系统根本没法正常使用。
第三,不要直接用 Windows 分区软件调整设备卡分区。开发板 eMMC 里的 GPT 一旦被通用分区工具改动,虽然可能保持分区数量不变,但其中某些分区的 GUID 或属性会变,Bootloader 的安全校验就会失败,导致无法启动。这类问题往往要重新完整烧录才能恢复,代价很高。
第四,A/B 分区场景下,要注意当前 active slot。OpenHarmony 有些设备使用 boot_a/boot_b、system_a/system_b 这种 A/B 架构。如果 misc 分区里没有正确的 slot 标记,Bootloader 可能不知道启动哪套分区。排查时可以用 fastboot 命令查看:
fastboot getvar current-slot如果返回空或者不是 a/b,就需要fastboot set_active a重新指定。很多人刷完 A/B 镜像之后忘记设置 active slot,设备就反复重启,其实是这个原因。
第五,修改分区表后必须同步烧录工具配置。开发过程中,你可能因为功能需求给 system 扩容 512MB,于是改了 gpt.ini。但烧录工具里的分区列表还是旧版本,结果 fastboot 烧 system 的时候写入位置和预期不一致,导致 vendor 分区被覆盖。这种错误很隐蔽,表面上是 vendor 挂载失败,实际上分区已经被破坏了。所以我每次改完分区表,都会单独跑一遍分区表导出命令,把结果和烧录配置放一起核对。
第六,日常维护时尽量依赖/dev/block/by-name而不是裸节点。不同平台裸节点号会变,比如mmcblk0p12可能在某次分区调整后变成mmcblk0p14,但 by-name 下的软链接则稳定指向分区名。脚本里写死裸节点,很容易在升级后失效。
最后分享一个小习惯:拿到新板子,我会先把/dev/block/by-name下的完整链接列表、/proc/partitions的内容、以及当前有效率的分区表配置文件一起打包存到工程目录里。这些文件平时不起眼,但遇到启动问题时,它们就是最可靠的排查地图。这个习惯帮我省下了很多返工时间,也让我在团队里解决分区类问题时总是最快的那个。