3、启动时间优化:从uboot到launcher的启动链路分析,测量各阶段耗时。

车载系统的启动时间,说白了就是用户体验的第一道门槛。你想想看,用户上车挂倒挡,中控屏幕还在那转圈圈,这体验能好吗?我做过好几个量产项目,启动时间每优化100毫秒,用户的投诉率就能降一截。今天咱们就来聊聊,怎么把这条启动链路彻底摸透。

3.1 启动链路全景图

一个典型的车载系统启动,大致分这么几个阶段:

  • BootROM:芯片上电后,固化在ROM里的代码先跑。这阶段我们基本动不了。
  • SPL/uboot:初始化DDR、时钟、存储控制器。然后加载uboot主体。
  • uboot:加载设备树、内核镜像,然后跳转到内核。
  • Kernel:解压、初始化驱动、挂载根文件系统。
  • Init进程:启动用户空间的第一个进程,通常是systemd或者Android的init。
  • SurfaceFlinger/WindowManager:图形合成服务启动,准备显示。
  • Launcher:桌面应用加载完成,用户可交互。

嗯,这里要注意,不同平台的具体阶段名称可能不一样,但本质都是这么个链路。

3.2 测量各阶段耗时

我个人习惯,第一步不是直接上手优化,而是先测量。没有数据,你都不知道瓶颈在哪。

3.2.1 硬件级测量:GPIO + 示波器

这是最准的方法。我在项目中遇到过,软件打日志的方式会引入额外延迟,尤其是串口打印,有时候能差几十毫秒。所以,我建议在关键节点拉GPIO:

// uboot阶段:在start_armboot()入口拉高GPIO
gpio_set_value(GPIO_PIN, 1);

// kernel启动完成:在init/main.c的start_kernel()末尾拉低
gpio_set_value(GPIO_PIN, 0);

// Launcher启动完成:在Activity.onCreate()中拉高另一个GPIO
findViewById(R.id.root).post(() -> {
    gpio_set_value(GPIO_PIN_2, 1);
});

用示波器抓这几个GPIO的跳变沿,时间差一目了然。我曾经用这个方法,发现uboot阶段竟然占了总启动时间的40%,后来一查,是DDR初始化参数配错了。

3.2.2 软件级测量:printk时间戳 + bootchart

如果手头没有示波器,也可以用软件手段。内核里有个很实用的功能:printk.time。在kernel cmdline里加上:

printk.time=1

这样每条printk都会带上时间戳。然后通过dmesg就能看到每个驱动的初始化耗时。

另外,bootchart是个好东西。它能把整个启动过程画成一张图,每个进程的CPU占用、磁盘IO、启动顺序都清清楚楚。我建议每个做启动优化的工程师,都先跑一遍bootchart看看。

关键指标参考

阶段目标耗时(冷启动)备注
BootROM → uboot< 500ms主要取决于芯片
uboot → Kernel< 300ms设备树加载、镜像解压
Kernel启动< 1.5s驱动初始化是重头戏
Init → Launcher< 2s服务启动、资源加载
总启动时间< 5s行业普遍要求

3.3 各阶段常见瓶颈与优化思路

3.3.1 uboot阶段

uboot的优化空间其实不小。我见过很多项目,uboot里做了大量不必要的初始化。比如:

  • 不必要的驱动加载:有些网卡、USB控制器在车载场景根本用不到,可以去掉。
  • 冗余的延时:有些工程师为了“稳定”,在初始化之间加了大量mdelay()。其实很多延时是可以缩短甚至去掉的。
  • 镜像加载方式:从eMMC加载内核时,如果使用4位模式而不是8位模式,速度会差一倍。

小技巧:uboot里可以用bootstage命令来测量各子阶段的耗时。比如:

# 在uboot命令行执行
bootstage report

它会打印出每个阶段的耗时,精确到微秒级。

3.3.2 Kernel阶段

内核启动的优化,说白了就是“少加载、晚加载、异步加载”。

  • 少加载:把不需要的驱动编译成模块,或者直接去掉。我见过一个项目,内核里编译了200多个驱动,实际用到的不到30个。
  • 晚加载:有些驱动不是必须的,可以放到用户空间去加载。比如蓝牙、Wi-Fi,完全可以等Launcher启动后再加载。
  • 异步加载:内核支持probe deferral,但有些驱动还是同步加载的。可以改成异步,或者用request_module_nowait()

另外,内核解压也是个耗时点。如果用的是gzip压缩,解压速度比较慢。我建议换成LZ4或者LZO,解压速度能快30%以上。代价就是镜像体积会大一点,但车载存储一般不是瓶颈。

3.3.3 用户空间阶段

用户空间的优化,重点在服务启动顺序资源预加载

我曾经优化过一个项目,Launcher启动花了3秒多。后来一查,是因为Launcher依赖的一个服务启动太慢。调整了init.rc里的启动顺序,把依赖服务提前启动,Launcher的启动时间直接降到了1.2秒。

还有一个常见问题:资源加载。很多Launcher会在启动时加载大量图片、字体、布局文件。这些IO操作如果串行执行,会很慢。我建议:

  • 把资源文件放在eMMC的快速分区上,或者用dm-verity的缓存。
  • 使用异步加载,先显示一个简单的占位界面,后台慢慢加载资源。
  • 对于常用资源,可以考虑在init阶段就预加载到内存里。

注意:不要为了追求启动速度,牺牲了系统的稳定性。我见过有人把关键服务的启动延时去掉,结果系统启动后频繁崩溃。优化要讲究平衡,先保证稳定,再追求速度。

3.4 实战:一个完整的启动时间测量案例

好了,理论说了这么多,咱们来点实际的。假设你手头有一块开发板,怎么一步步测量启动时间?

  1. 准备工具:示波器(或者逻辑分析仪)、串口线、SD卡(用于更新uboot)。
  2. 打GPIO点:在uboot入口、kernel入口、init入口、Launcher入口各拉一个GPIO。
  3. 抓波形:上电,用示波器抓取GPIO的跳变沿。
  4. 记录数据:把每个阶段的耗时记录下来。
  5. 分析瓶颈:看哪个阶段耗时最长,然后针对性地优化。

我记得有一次,我测出来uboot阶段花了1.2秒,远超预期。后来发现是uboot里有个usb start命令,它尝试枚举所有USB设备,超时时间设成了1秒。去掉这个命令后,uboot阶段直接降到了200毫秒。

你看,有时候问题就这么简单。但如果你不测量,你永远不知道问题在哪。

3.5 小结

启动时间优化,说白了就是“测量-分析-优化-再测量”的循环。没有捷径,但有方法。我个人建议,每个项目都建立一个启动时间基线,每次修改后都重新测量,确保优化没有引入新的问题。

下一章,咱们聊聊内存优化。车载系统的内存,那可是寸土寸金啊。