1. 从21秒到16.5秒一次启动耗时优化的实战之旅大家好我是老王一个在嵌入式领域摸爬滚打了十多年的老兵。最近在基于RK3568芯片的OpenHarmony标准系统上做产品开发遇到了一个挺典型的问题开机时间太慢。从按下电源键到出现锁屏界面足足要21秒。这放在消费电子产品上用户可能早就失去耐心了。一开始我们尝试了最直观的“瘦身”方案比如裁剪掉一些非必要的子系统、禁用部分系统服务、精简预装应用。一顿操作猛如虎结果开机时间从21秒“优化”到了20.94秒只提升了0.06秒几乎可以忽略不计。这让我意识到常规的裁剪手段已经碰到了天花板必须深入到启动流程的骨髓里去寻找“病灶”。于是我把目光投向了系统启动的“总导演”——init进程。在Linux和类Unix系统中init是内核启动后创建的第一个用户空间进程它负责初始化系统环境、挂载文件系统、启动各种关键服务。OpenHarmony的启动流程也继承了这一经典设计。如果init进程在执行某个任务时“卡住”了那么整个系统的启动流程就会被阻塞时间自然就上去了。我的思路很简单分析init进程的日志寻找那些“时间断层”。所谓“时间断层”就是在连续的日志打印中突然出现一段长时间的空白期间init进程没有任何输出这说明它很可能被某个耗时操作阻塞了。通过定位并解决这些阻塞点就能有效压缩启动时间。最终我们成功将开机时间从21秒优化到了16.5秒提升了近21%。下面我就把这次“寻宝”和“排雷”的全过程掰开揉碎了分享给大家。2. 定位启动耗时的“罪魁祸首”日志分析实战优化启动时间第一步不是盲目改代码而是要学会“看日志”。OpenHarmony在启动阶段主要提供两种日志dmesg内核日志和hilogOpenHarmony框架日志。我们需要在开机时同时抓取这两种日志进行分析。这里有个小技巧因为hilog需要hilogd服务启动后才能打印所以它的启动时间点会稍晚于内核。我们可以通过对比dmesg中hilogd服务的启动时间和hilog的实际打印时间来校准我们的分析时间线。开机抓取日志的方法有很多比如通过串口调试工具或者使用hdc shell dmesg和hdc shell hilog命令在启动后抓取历史记录。我更喜欢用串口实时抓取信息更完整。抓取到日志后我们的核心任务就是在dmesg日志中筛选出pid1即init进程的所有记录然后像侦探一样仔细检查每一条日志的时间戳。一旦发现相邻两条日志的时间差异常巨大比如超过500毫秒这里就很可能存在一个“时间断层”也就是我们的优化切入点。2.1 第一个断层fsck文件系统检查在我的设备日志中我很快就发现了第一个明显的断层[ 4.947309] [pid1][BEGET][INFO][fstab_mount.c:78]Execute /system/bin/fsck.f2fs begin [ 7.196164] [pid1][BEGET][ERROR][fstab_mount.c:91]Command /system/bin/fsck.f2fs failed with status 1看从4.947309秒到7.196164秒init进程花了将近2.25秒的时间就干了一件事执行/system/bin/fsck.f2fs命令而且最后还执行失败了返回状态码1。fsck是“file system check”的缩写顾名思义它就是用来检查和修复文件系统一致性的。通常在系统异常关机比如突然断电后再次启动时会自动运行以确保数据分区比如/data的完整性。但在我们正常开机的场景下这个检查往往是不必要的尤其是它还执行失败了。更关键的是init进程在这里是同步执行这个命令的。这意味着init会一直傻等着fsck.f2fs执行完毕无论成功还是失败才会继续往下执行挂载/data分区、启动后续服务等操作。这2.25秒就成了整个启动链路上一个纯粹的“堵点”。优化思路既然这个检查在正常启动时非必需且耗时很长我们可以尝试将它改为异步执行。让init进程“派个小弟”fork子进程去执行这个检查自己则不用等待结果继续马不停蹄地执行后面的启动任务。这样这个2.25秒的阻塞就被消除了。2.2 第二个断层sdc命令与随机数生成解决了fsck的问题后重新抓取日志发现时间断层消失了但一个新的断层又浮现出来[ 5.236533] [pid1][Init][INFO][init_cmds.c:99]Sync exec: /system/bin/sdc [ 8.223549] [pid1][Init][INFO][init_cmds.c:112]Sync exec: /system/bin/sdc result 0 1这次是执行/system/bin/sdc命令耗时接近3秒。这个命令是做什么的呢通过追溯代码发现它是在执行init_global_key流程时被调用的核心任务是生成用于分区加密的随机密钥。问题就出在生成随机数这里。在Linux系统中高质量的随机数来源于系统的“熵池”entropy pool它收集各种硬件噪声如键盘敲击、鼠标移动、磁盘I/O时间差等作为随机性来源。/dev/random是一个阻塞式的随机数源当熵池中熵值不足时读取它的进程会被挂起直到收集到足够的熵。系统刚启动时硬件活动很少熵池几乎是空的。此时如果程序试图从/dev/random读取大量随机数就会陷入漫长的等待。我们的sdc命令正是在这个阶段被阻塞了。优化思路这个问题比fsck棘手因为生成加密密钥是安全相关的关键操作不能简单地改为异步执行否则可能导致分区无法解密。我们必须解决熵值不足的根本问题。常见的方案是引入一个熵值生成守护进程在系统启动早期快速填充熵池。有两个流行的工具rng-tools和haveged。我选择了haveged因为它更轻量且对AArch64架构支持良好。2.3 服务启动的隐藏耗时线程池与动画等待除了init主进程的阻塞系统服务的启动也是耗时大户。通过分析hilog我发现了两个关键点Ability启动线程池瓶颈foundation服务负责管理应用框架启动时会初始化大量的Ability能力组件。OpenHarmony默认只使用4个线程来串行初始化这些Ability。这就好比只有一个收银台的超市顾客Ability只能排长队等待。当Ability数量较多时这里就会形成瓶颈。从日志看AccountMgrService向AbilityManagerService发送消息就等待了3.3秒部分原因就是在排队等待AbilityManagerService初始化完成。Launcher等待开机动画AbilityManagerService在启动Launcher桌面时会等待一个名为bootevent.bootanimation.started的系统属性被设置为true。这个属性是由开机动画播放器bootanimation在播放完所有帧之后才设置的。RK3568的开机动画有150帧以30帧/秒的速度播放需要整整5秒这意味着即使系统核心服务都已就绪用户也得干等着动画播完才能看到桌面。优化思路针对线程池瓶颈可以扩大Ability初始化线程池的规模比如从默认的CPU核心数扩大到两倍让更多的Ability可以并行初始化。针对动画等待可以修改开机动画的退出逻辑不必等所有帧播完而是每播放一帧就检查一次是否满足退出条件例如系统核心服务已就绪从而让Launcher得以提前启动。3. 代码层面的“手术刀”精准优化实施理论分析清楚了接下来就是动手修改代码。我们基于OpenHarmony 3.2 Release版本的代码进行修改。3.1 fsck.f2fs的异步化改造首先找到fsck命令的执行位置。根据日志它是在/base/startup/init/interfaces/innerkits/fs_manager/fstab_mount.c文件的DoFsckF2fs函数中被调用的最终通过ExecCommand函数执行。原始的ExecCommand函数使用waitpid(pid, status, 0)等待子进程结束这里的0选项意味着父进程init会一直阻塞等待。我们的目标就是创建一个不等待的版本。修改步骤创建异步执行函数在fstab_mount.c文件中复制ExecCommand函数重命名为AsyncExecCommand。关键修改是将waitpid的第三个参数从0改为WNOHANG。这个选项告诉系统如果子进程还没结束就立刻返回不要阻塞父进程。// 新增的异步执行函数 static int AsyncExecCommand(int argc, char **argv) { // ... 前面的fork等逻辑不变 ... int status; // 关键修改使用WNOHANG非阻塞等待 waitpid(pid, status, WNOHANG); // ... 后续日志打印逻辑不变 ... return WEXITSTATUS(status); }替换调用点找到调用ExecCommand执行fsck.f2fs和resize.f2fs的地方分别在DoFsckF2fs和DoResizeF2fs函数中将它们改为调用新的AsyncExecCommand函数。// 在DoFsckF2fs函数中 // return ExecCommand(argc, argv); // 原代码 return AsyncExecCommand(argc, argv); // 修改后踩坑与修复这个改动上线后我们发现了一个问题设备重启后/data分区的大小偶尔会出错。经过排查原因是resize.f2fs命令在设备第一次启动时需要同步执行以正确扩展分区大小。如果第一次启动就异步执行可能扩展未完成就进行了后续操作导致分区表信息不一致。解决方案引入一个持久化的系统属性例如persist.sys.resize_data来标记/data分区是否已完成首次扩容。在DoResizeF2fs函数中先读取这个属性。如果是第一次启动属性不存在或为0则仍使用同步的ExecCommand执行扩容并在成功后设置属性为1否则使用异步的AsyncExecCommand。这样既保证了首次启动的正确性又让后续启动享受到了异步化的提速好处。3.2 解决sdc命令的熵池阻塞对于sdc命令的阻塞我们需要引入haveged工具来加速熵池的填充。操作步骤交叉编译haveged从官网或GitHub获取haveged源码针对RK3568的AArch64架构进行交叉编译生成可执行文件。预置haveged到系统镜像在OpenHarmony源码的/base/startup/init/目录下创建haveged文件夹。在该文件夹内创建BUILD.gn文件将编译好的haveged二进制文件声明为一个ohos_prebuilt_executable预置可执行文件组件并指定安装到system分区。修改/base/startup/init/bundle.json文件将我们新建的haveged组件添加到service_group中这样在构建系统时它就会被打包进镜像。在启动早期运行haveged修改init的启动配置文件如/base/startup/init/services/etc/init.cfg在pre-init这个job里添加一条命令exec /system/bin/haveged -F。-F参数表示让haveged在前台运行并填充熵池。临时关闭SELinux可选在开发调试阶段SELinux可能会阻止haveged的执行。我们可以在编译配置中临时将其关闭以便测试。找到/base/security/selinux/selinux.gni文件将selinux_enforce true改为false。注意生产环境中需要为haveged配置正确的SELinux策略而不是直接关闭。完成上述步骤并烧录新镜像后sdc命令的执行时间从接近3秒下降到了约0.5秒效果立竿见影。3.3 加速Ability启动与Launcher显示扩大Ability启动线程池找到foundation服务中负责管理Ability初始化的代码文件通常是safwk框架下的local_ability_manager.cpp。搜索concurrentThreads和线程池启动的地方。将默认的线程池大小从concurrentThreads获取的硬件并发线程数修改为2 * concurrentThreads直接翻倍。// 修改前 initPool_-Start(concurrentThreads); // 修改后 initPool_-Start(2 * concurrentThreads);这个简单的改动可以让Ability的初始化任务得到更充分的并行执行缩短foundation服务的整体启动时间。优化开机动画退出逻辑找到开机动画的源码通常在/foundation/graphic/graphic_2d/frameworks/bootanimation/src/boot_animation.cpp。原始逻辑是在Draw()函数中当播放完最后一帧picCurNo_ imgVecSize_ - 1时才调用CheckExitAnimation()来设置bootevent.bootanimation.started属性。我们要做的就是把CheckExitAnimation()的调用从“播放完最后一帧”的条件判断里拿出来放到每一帧绘制完成之后。这样动画开始播放后系统很快就能检测到bootevent.bootanimation.started被设置为true从而唤醒等待中的Launcher。void BootAnimation::Draw() { // ... 原有的帧递增和绘制逻辑 ... // 原逻辑播完才检查退出 // if (picCurNo_ (imgVecSize_ - 1)) { // picCurNo_ picCurNo_ 1; // } else { // CheckExitAnimation(); // 旧位置 // return; // } // 新逻辑每帧绘制后都检查是否可以退出 // ... 绘制当前帧 ... CheckExitAnimation(); // 新位置每帧结束后都检查 }同时为了不让动画戛然而止显得突兀你可能还需要微调开机动画资源本身让它即使提前结束在视觉上也是一个完整的闭环。4. 效果验证与深度思考经过上述一系列“组合拳”式的优化我们重新编译并烧录系统。再次测量开机时间稳定在了16.5秒左右相比最初的21秒提升了近4.5秒优化幅度超过20%。这个成绩在用户体验上是能明显感知到的提升。回顾整个优化过程我最大的体会是系统启动优化是一个系统工程不能只盯着一个点。它需要你具备从日志分析、代码追踪到系统机制如进程、文件系统、随机数、并发的全面理解能力。日志是导航仪学会看dmesg和hilog尤其是关注pid1的init进程的时间线是发现阻塞点的最直接方法。同步改异步是利器对于非关键路径且耗时的操作如非必要的文件系统检查异步化是立竿见影的手段但要注意异步操作可能带来的时序和状态问题如我们遇到的data分区扩容问题。理解系统机制是关键像熵池不足导致阻塞这种问题如果不了解Linux随机数生成机制就很难想到引入haveged这样的解决方案。全局视野很重要优化不能只停留在init阶段。服务启动的并行度线程池、模块间的依赖与等待如Launcher等动画都是影响最终“秒数”的重要环节。当然16.5秒远不是终点。OpenHarmony的启动流程中可能还存在其他可以并行化、懒加载或裁剪的环节。例如某些服务的启动顺序是否可以调整一些预加载的资源是否真的必要这些都需要结合具体的产品需求和硬件性能去做更深入的权衡和优化。希望这次RK3568上的实战经验能为你优化自己的OpenHarmony设备启动速度提供一个清晰的思路和可复现的路径。