ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

OpenHarmony标准系统内核启动链路全解析:从Bootloader到init进程

OpenHarmony标准系统内核启动链路全解析:从Bootloader到init进程 前两周同事拿了一块标准系统的开发板来找我说内核起不来串口只打了Bootloader的logo就没了下文。我坐下来看了看他的cmdline第一眼就发现root写成了ramdisk这种错误在OpenHarmony标准系统开发初期非常典型。说实话OpenHarmony标准系统的内核启动链路很多人稀里糊涂地用了很久一遇到起不来就只会加打印加完打印还是看不懂。这篇东西就是把OpenHarmony标准系统内核启动这件事从头到尾拆开讲清楚Linux内核从Bootloader交棒到init进程这个阶段到底做了什么、常见死法有哪些、怎么定位最有效率。1.1 标准系统与轻量、小型系统的边界OpenHarmony生态里经常听到三种形态轻量系统、小型系统、标准系统。这三者的分界线本质上是硬件资源更本质上是内核选型。轻量系统面向MCU级别设备内存通常在128KB到1MB之间跑的是LiteOS-M没有完整的进程隔离也不需要MMU。小型系统面向内存1MB到128MB的设备可以跑LiteOS-A或者裁剪过的LinuxPOSIX接口不一定完整。而标准系统要求内存普遍在128MB以上使用完整Linux内核OpenHarmony主线用的是Linux 5.10 LTS具备完整进程管理、虚拟内存、SELinux等能力。为什么这个话题值得单独拿出来讲因为标准系统一上来的复杂度比前两者高一个量级——你要面对真正的Bootloader迁移、设备树、根文件系统、init进程以及驱动加载顺序。很多人有个误解以为OpenHarmony标准系统是自己的内核其实不是。它是在Linux 5.10内核之上做了一套面向OpenHarmony的配置和补丁集代码路径通常在kernel/linux/linux-5.10目录下通过自定义defconfig和patch实现OpenHarmony所需的特性比如HDI驱动框架、应用沙箱相关内核能力、以及面向标准系统的SELinux策略。所以当你问OpenHarmony os是用什么语言编写的时标准系统内核部分就是C和少量汇编用户态框架才是C/C为主。标准系统的启动链路可以宏观概括为Bootloader加载内核镜像和设备树与ramdisk内核解压自举随后执行start_kernel初始化核心子系统最终拉起第一个用户态进程OpenHarmony下是init再由init解析cfg脚本拉起appspawn、foundation等服务。内核启动这一步是否可靠直接决定后面所有服务的稳定性XTS认证种有很多fail项追根溯源都会回到内核启动阶段。1.2 内核启动之前Bootloader必须交代清楚的事很多人排查内核启动问题时忽略了一个关键点Bootloader向内核传递的上下文是否完整。标准系统产品上最常见的Bootloader是U-Boot也有部分全志、瑞芯微、海思平台使用芯片厂商定制的ABL或miniloader。Bootloader的核心职责有三件事第一初始化DDR、时钟、串口、存储等基础硬件第二把内核镜像、设备树dtb、ramdisk读入内存第三组合cmdline并跳转到内核入口时把相关寄存器传递过去通常是x0指向dtb地址。这中间任何一环断裂内核根本不会开始执行。我实际调试中发现一个高频问题很多人改了设备树里的参数但U-Boot把dtb加载到内存之后又强制覆盖了某些属性比如chosen节点里的bootargs。你在内核侧怎么改defconfig都没用因为bootargs最终以bootloader传入产成为准。OpenHarmony标准系统常见做法是通过cmdline里的配置打开调试例如consolettyFIQ0,115200n8 root/dev/mmcblk0p15 rootwait loglevel8 initcall_debug earlycon不同的芯片平台console参数不一样瑞芯微常用ttyFIQ0海思常用ttyAMA0全志常用ttyS0。这些细节不弄清楚内核打印可能一条都出不来但内核其实早就开始跑了。另一个容易被忽略的是ramdisk大小。bootloader普遍按固定大小分配内存块来加载ramdisk如果你的标准系统userdata或system分区规划太大ramdisk压缩包超过bootloader的预分配空间就会出现解压CRC错误或populate_rootfs失败现象是启动卡在根文件系统挂载之前。这类问题在串口日志里非常好识别但很多人一看到CRC错误就以为是镜像损坏来回重刷固件浪费半天。2. 内核自解压到start_kernel这一段时间的初始化细节2.1 汇编入口head.S里做了什么Bootloader跳转到内核入口后既不是立刻进入C代码也不是从头开始执行Linux的main函数。arm64平台上如果内核镜像是压缩包Image.gz前面还有一段自解压代码负责在原地解压。很多问题恰恰出现在这里——解压所需内存不足或者解压后覆盖了dtb所在的物理内存。进入真正的内核入口head.S后关键动作是建立早期页表、开启MMU、设置异常向量表再跳转到C代码。这些汇编操作业界习惯称为内核自举bootstrap。为什么要用汇编因为此刻还没有C运行环境栈指针都没设定只有最原始的物理地址操作能力。arm64上还有一个细节KASLR。如果内核配置了KASLR每次启动会随机化内核镜像的虚拟地址偏移好处是安全坏处是排查问题困难——你看到的符号地址每次都不一样。OpenHarmony标准系统开发阶段我建议先把KASLR关掉或者至少保证串口日志能抓到/proc/kallsyms否则你拿到的崩溃栈很难对应到具体函数。head.S里最容易踩的坑是CONFIG_ARM64_16K_PAGES这类页大小配置。OpenHarmony标准系统大多数平台用的是4KB页如果你把内核配置文件里页大小改成了16KB有些硬件平台虽然在MMU早期页表创建时能跑过去但后续DMA、IOMMU相关驱动大概率出现问题表现形式就是启动到某个驱动节点时偶发卡死或者内存分配失败。2.2 setup_arch设备树和内存布局的落地从汇编跳到第一个C函数后早期初始化的核心是setup_arch。这个函数做两件大事解析设备树以及建立物理内存布局memblock。设备树在启动阶段的作用可以理解为一份硬件资源的清单。CPU有几核、中断控制器注册在哪、内存起始地址和大小是多少、串口寄存器地址在哪全部通过设备树节点描述。内核通过这些节点在启动早期就把必要的信息登记到内存管理、中断管理、时钟框架中。memblock是内核在buddy allocator生效之前的早期内存分配器。内核刚启动时不能立刻使用完整的内存分配器因为数据结构本身需要内存来初始化先要用memblock记录哪些内存可用、哪些被保留reserved。设备树里的reserved-memory节点以及bootloader传给内核的reserved内存都会在这里落地。如果设备树里预留的内存与内核镜像加载地址冲突就会出现荒谬的现象内核启动一下就死但串口没有任何panic信息因为崩溃点在串口初始化之前。我建议所有标准系统开发者至少把Ramoops配置打开。Ramoops利用设备树里预留的一段内存保存内核panic日志重启后还能读到上一次崩溃的现场对定位早期启动的随机死机有奇效。开发阶段芯片平台的defconfig里不一定默认开启需要检查CONFIG_RAMOOPS和reserved-memory节点。3. 从start_kernel到init进程驱动、rootfs、第一个用户态程序3.1 initcall机制驱动按什么顺序被召唤start_kernel执行完所有核心子系统初始化调度器、中断、时间、内存管理之后会走到rest_init。rest_init创建两个内核线程一个是kernel_init这是后续用户态init进程的前身另一个是kthreadd负责创建其他内核线程。真正的驱动加载发生在kernel_init执行时调用的do_basic_setup。Linux内核把驱动、模块初始化函数按级别组织成initcall级别从early_initcall到core_initcall、postcore_initcall、arch_initcall、subsys_initcall、fs_initcall、device_initcall、late_initcall依次调用。OpenHarmony标准系统下HDI驱动框架的初始化通常挂在subsys_initcall或device_initcall级别camera、sensor这类硬件服务往往在这个阶段注册到hdf框架。为什么启动顺序这么讲究因为驱动之间有依赖。比如存储驱动必须先于根文件系统挂载就绪网络驱动可以稍晚但显示驱动又需要在启动动画出现之前完成。initcall的调用顺序在链接脚本里通过__initcall_start和__initcall_end之间的section排列决定这也就是为什么你看到的每个驱动都会出现在某个initcall段里。排查启动卡死时initcall_debug参数极其好用。加了它之后内核会在每个initcall调用前后打印函数名和时间你能精确定位卡在哪个驱动的init函数里。少看点玄学直接对着日志找最后一个打印就可以了。3.2 ramdisk、rootfs与init执行顺序OpenHarmony标准系统的启动过程中ramdiskinitrd不是可选项它在根文件系统真正挂载之前提供了一个临时用户空间。早期内核需要先把ramdisk解压到rootfs——通常基于tmpfs——然后由ramdisk里的脚本或程序完成硬件检测、分区挂载等准备工作最后切换到真正的根文件系统。有人把root参数和ramdisk混为一谈这是个新手高频错误。ramdisk是启动早期加载到内存的文件系统镜像而root指向的是真正的系统盘分区。OpenHarmony标准系统典型做法是分两个阶段第一阶段通过ramdisk里的init或脚本负责初始化存储、解密、挂载system分区第二阶段执行真正的init进程切换到用户态完整环境。挂载根文件系统之前的等待也是一个经典卡死点。如果你启用了rootwait内核会一直等待根设备出现不超时。这时候如果存储驱动没加载成功或者分区编错了系统会卡在Waiting for root device这一天长地久。反过来如果你没加rootwait设备还没准备好内核就直接尝试挂载并报错然后panic。多数OpenHarmony发行版的defconfig里默认带上rootwait所以调试时见到Waiting for root device先别急着加打印把storage节点和分区表检查一遍经常是设备树里mmc节点status被设成disabled。最后一步就是kernel_init里执行init进程。OpenHarmony标准系统的init是用户态第一进程它会读取/etc/init/*.cfg配置按依赖关系拉起appspawn、foundation、musl等组件。如果内核启动本身没问题但init脚本崩溃表现形式不是无输出而是日志不断滚动又重启说明陷入init进程反复启动的循环。这时候加大日志级别看用户态崩溃在哪一条通常比怀疑内核链更有效率。4. 实测启动卡住的四种典型场景与定位手段4.1 场景一Bootloader有输出内核完全无打印这是最让人头疼的现象你看到Bootloader正常选择启动项但是串口在内核阶段一片死寂。优先怀疑的是console串口参数不匹配。不同平台的早期串口名不一样而且内核在earlycon启用之前使用的console参数如果没有正确配置后续所有printk都无处可去。排查方法很简单先把console和earlycon参数加上。earlycon的格式和平台相关比如earlyconuart8250,mmio32,0xfeb50000这个0xfeb50000地址必须与芯片的UART寄存器地址一致不同平台差异很大。只要earlycon起来了你最起码能看到内核第一条打印后面再调整其他参数都心里有底。另外确认Bootloader是否开启了secure boot或签名校验。有些平台的出厂固件强制校验内核签名你刷了自编译内核后Bootloader会干脆不加载表现就是看似没反应。实则是校验失败后回退到了其他启动分区。4.2 场景二日志停在内核解压完成之后如果能看到内核版本、内存信息这些早期打印但突然中断重点查两处设备树解析初始化和内存布局。最常见的原因是设备树里内存节点大小与实际硬件不符。OpenHarmony标准系统的ramdisk解压需要较大连续内存如果内存节点把可用内存写小了解压失败或内存分配失败就会卡在setup_arch之后某一步。另一个高频点是cache颜色对齐问题——这属于太底层的范畴这里不展开但如果你在arm64平台上遇到启动阶段偶发死机可以检查设备树里cache节点是否完整。经验做法在这种卡住状态抓Ramdump或使用JTAG连接直接看PC寄存器停在哪个函数。没有调试器的话至少把早期打印级别调到最高以内核里每一个printk都是救命稻草。在开发阶段加log_buf_len参数扩大环形缓冲尽量保留更早的日志也是值得的。4.3 场景三卡在根文件系统挂载之前或等待根设备这类问题在串口日志里的辨识度很高[ 1.234567] Waiting for root device /dev/mmcblk0p15...不必慌。这个场景最坑的地方在于内核有时不打印我找不到根设备而是不断刷VFS: Cannot open root device。你需要Check三点第一分区路径是否存在第二存储控制器驱动是否注册成功第三如果根文件系统是ext4内核是否编译了对应的文件系统支持。OpenHarmony标准系统的分区布局各厂商都有定制不要想当然用表里的分区下标。最快做法是回到Bootloader环境下用mmc list、mmc part命令确认分区编号。我在实际项目里不止一次遇到把mmcblk0p15写错成mmcblk1p15的情况因为板子上既有eMMC又有SD卡设备节点编号容易混乱。还有一类比较隐蔽如果你的根文件系统镜像使用了overlayfs或erofs而内核里没开对应config内核会告诉你Unknown block device或root cause不明确。先把CONFIG_OVERLAY_FS、CONFIG_EROFS_FS这些确保开启。4.4 场景四init进程崩溃并反复重启用户态init进程崩溃的典型特征是串口刷出一堆类似init: Failed to mount /dev/misc然后系统进入重启循环。很多人在这一步还在怀疑内核其实内核链路已经跑通了问题在用户态环境。解决办法是把cmdline里的loglevel调高抓取用户态日志。OpenHarmony的init日志通常在hilog里但启动早期hilog可能还没就绪这时候通过内核printk打印出的init错误、以及init的stdout/stderr重定向到console能看到相对完整的信息。另一个小技巧是在init脚本里临时加-s进入单用户模式绕过部分依赖逐步确认哪些挂载或服务出问题。这里顺带提一个网上经常搜到的关联词代码53出自Windows内核调试器的提示和OpenHarmony内核启动没有关系。看到这种报错先确认你站在哪个系统环境里别把其他系统的错误提示套在OpenHarmony上。5. 启动提速与XTS认证视角下的内核配置经验5.1 裁剪和延迟加载能带来多少收益内核启动速度直接决定了产品的开机体验。实测中同一块标准系统板卡内核initcall部分在关闭所有调试选项后可以节省300ms到500ms网络、摄像头等非关键驱动改成模块方式符合条件再加载整体能再压缩一部分。我的常规手法是先用initcall_debug参数获取一份所有驱动的耗时矩阵然后按耗时排序。表格整理一下大概是这样驱动/initcall耗时是否可延后mmc前等待120ms否根文件系统依赖显示驱动fb80ms视产品需求摄像头camera hdi60ms可延后网络协议栈40ms可延后wifi驱动35ms可延后对可以延后的模块优先把它们编译成内核模块或调整initcall级别让它们晚一点执行。但要注意OpenHarmony标准系统对模块加载有SELinux策略限制改动模块加载时间的同时也要验证ili策略是否许可。另外要特别警惕调试选项对启动速度的透支。CONFIG_DEBUG_*、KASAN、UBSAN这类选项在开发初期能救命但它们的代价是启动时间显著变长、内存占用变大。上线前必须全部关闭否则XTS认证的性能、功耗相关用例过不去。5.2 SELinux、kptr_restrict与启动开发的平衡OpenHarmony标准系统默认开启SELinux这是一把双刃剑。开发阶段很多启动失败其实是SELinux策略拒绝引起的比如init进程尝试访问某个设备节点被拒绝、驱动加载后注册节点权限不足内核日志里会出现avc: denied的打印。我见过太多人在这个阶段选择粗暴地把SELinux设成permissive甚至是disabled。开发期临时permissive确实可以理解但务必记得真机验证时恢复enforcing否则你在发布前突然打开会冒出一堆隐藏的问题。还有一个值得注意的参数kptr_restrict。它控制非特权用户能否看到内核符号地址。OpenHarmony标准系统为了安全默认会把它设置成1即只有具备CAP_SYSLOG权限的进程才能读取kptr。调试时如果发现/proc/kallsyms里全是0地址就是kptr_restrict在起作用。临时调成0能看到全部符号但发布前必须改回去。XTS认证对内核的关注点主要是系统属性和安全特性。里面很多用例会检测内核config比如是否开启SELinux、是否启用某些安全模块、HDI接口是否具备对应权限管控。所以如果你想顺利通过XTS内核配置不能只图能开机就行还得严格对照兼容性清单逐个确认。与其最后返工不如在第一次切内核配置时就做一张excel把认证要求、defconfig、实际生效三列拉齐。从底层启动通路一路看到init进程说实话OpenHarmony标准系统的内核启动链路并不比普通嵌入式Linux复杂多少难的是它把Linux内核、设备树、ramdisk、SELinux、HDI框架全部拧在一起变成了一个跨层级的大迷宫。我个人在实际项目里的体会是排查启动问题最大的敌人不是看不懂代码而是漏掉中间交接的细节——bootloader传的dtb对不对、cmdline的console参数对不对、内核config里有没有打开对应的文件系统和驱动。每次拿到一块内核起不来的板子先确认这三件事再往下查通常能少走一半弯路。最后再分享一个小习惯任何一次成功的、完整的内核启动日志都值得按日期存一份。有了这个基线下次出问题时打开diff一下输出的最后位置定位速度会快很多。
返回列表