2. BSP基础认知

做嵌入式BSP这么多年,我有个很深的体会:启动时间优化,往往是产品经理和用户最先感知到的性能指标。你想想看,一个智能摄像头开机要等30秒,或者车载中控启动后还得转圈加载,用户的第一印象直接就崩了。

我个人习惯把启动过程拆成三段来看:Bootloader阶段、内核阶段、用户空间阶段。每一段都有各自的优化空间和坑。今天我们就从头到尾捋一遍,把每一毫秒都抠出来。

核心观点:启动时间优化不是玄学,是系统工程。你需要先测量,再分析,最后优化。没有测量数据就动手,那是瞎忙活。

启动时间优化三段式流程 Bootloader 阶段 ~100-500ms 内核阶段 ~500-2000ms 用户空间阶段 ~500-3000ms 硬件初始化 → 加载内核 驱动加载 → 文件系统挂载 init → 服务启动 → 界面 测量工具:bootgraph / bootchart / printk time

2.1 Bootloader启动流程分析

Bootloader是系统上电后第一个跑起来的程序。它的任务很简单:初始化硬件,加载内核到内存,然后跳转过去。但就是这几步,往往藏着不少优化空间。

常见的Bootloader有U-Boot、GRUB、以及一些芯片厂商自研的loader。以U-Boot为例,它的启动流程大致如下:

  1. SPL(Secondary Program Loader):芯片内部ROM加载的第一段代码,非常小,负责初始化DDR和存储控制器。
  2. U-Boot proper:完整的U-Boot,负责加载内核镜像到内存。
  3. 跳转到内核:设置好启动参数,跳转到内核入口。

我的经验:很多项目在SPL阶段浪费了大量时间。我曾经在一个项目上发现,SPL里有个for循环在等待某个外设就绪,默认超时设了500ms。实际上那个外设100ms内就能准备好。把超时时间改小,直接省了400ms。

Bootloader优化的几个关键点:

  • 裁剪不必要的驱动:U-Boot里默认会初始化很多外设,比如网卡、USB、SD卡等。如果你的产品不需要从网络启动,就把网卡驱动去掉。
  • 优化DDR初始化:DDR training是Bootloader里最耗时的操作之一。如果硬件设计固定,可以考虑使用快速training模式或者保存training参数,避免每次冷启动都重新training。
  • 减少打印信息:串口打印虽然方便调试,但每打印一行字符都要几十毫秒。量产版本建议关闭或大幅减少打印。
  • 使用压缩内核:如果存储读取速度是瓶颈,可以考虑用压缩内核(如LZ4),虽然解压需要时间,但读取时间更短,整体可能更快。

注意:Bootloader优化要谨慎。我曾经见过一个团队把U-Boot的延时全部砍掉,结果在某些批次的主板上DDR初始化不稳定,导致启动失败。优化后一定要做充分的压力测试。

2.2 内核启动耗时分解

内核启动是整条链路里最复杂的部分。它涉及驱动加载、设备初始化、文件系统挂载等一系列操作。我个人习惯用printk time来测量内核各阶段的耗时。

开启printk time的方法很简单:在内核启动参数里加上printk.time=1,或者在内核配置里打开CONFIG_PRINTK_TIME。这样每条内核日志前面都会带一个时间戳,单位是秒。

# 内核启动参数示例
console=ttyS0,115200 root=/dev/mmcblk0p2 rw printk.time=1

拿到带时间戳的日志后,我们可以把内核启动拆成几个关键阶段:

阶段 典型耗时 说明
解压内核 50-200ms 取决于压缩算法和CPU速度
架构初始化 10-50ms setup_arch、内存初始化等
驱动初始化 200-1000ms 这是大头,尤其是存储和显示驱动
文件系统挂载 50-300ms 取决于文件系统类型和存储设备速度
启动第一个用户进程 10-50ms 通常是init进程

驱动初始化往往是耗时最多的部分。我建议重点关注以下几点:

  • 驱动模块化:把非必需的驱动编译成模块,延后加载。内核启动时只加载必要的驱动。
  • 异步probe:Linux内核支持异步驱动probe,可以让多个驱动同时初始化,而不是串行等待。
  • 设备树裁剪:设备树里不要包含实际不存在的设备节点,否则内核会花时间去尝试初始化它们。
  • 使用initramfs:如果根文件系统在慢速存储上(如NAND Flash),可以考虑先用initramfs启动,再挂载真正的根文件系统。

实战技巧:我曾经在一个ARM平台上做优化,发现MMC驱动初始化花了800ms。后来查了代码,发现驱动里有个循环在等待卡检测信号,默认超时是1秒。实际上我们的硬件设计里卡是固定的,不需要检测。把这段代码跳过,直接省了700ms。

2.3 用户空间初始化优化

内核启动完成后,系统会执行第一个用户进程——通常是/sbin/init。从这一刻开始,就进入了用户空间初始化阶段。

用户空间初始化的主要任务是:启动各种服务、挂载文件系统、配置网络、启动GUI等。这个阶段的优化空间往往比内核还大。

常见的优化手段:

  • 使用轻量级init系统:比如busybox init、systemd(虽然功能强大但启动慢)、OpenRC等。对于嵌入式设备,我建议用busybox init或者自己写一个极简的init脚本。
  • 并行启动服务:如果使用systemd,可以配置服务的依赖关系,让不相关的服务并行启动。
  • 延迟启动非关键服务:比如日志服务、远程管理服务等,可以等系统完全启动后再启动。
  • 预链接和预加载:对于动态链接的可执行文件,预链接可以减少运行时链接的时间。
  • 使用readahead:提前把启动需要的文件读入内存缓存,减少磁盘I/O等待。

我的习惯:在用户空间优化时,我会先做一个最小启动集——只保留系统启动必须的服务,其他全部去掉。然后逐个加回来,看每个服务对启动时间的影响。这样能清楚地知道哪些服务是真正的瓶颈。

举个例子,一个典型的嵌入式Linux系统,用户空间启动流程可能是这样的:

# 极简init脚本示例
#!/bin/sh

# 挂载必要的文件系统
mount -t proc none /proc
mount -t sysfs none /sys
mount -t tmpfs none /tmp

# 设置主机名
hostname mydevice

# 启动核心服务
/sbin/syslogd
/sbin/klogd

# 启动网络(如果需要)
# /sbin/ifconfig eth0 up
# /sbin/udhcpc

# 启动应用程序
/usr/bin/my_app &

这个脚本里,我故意把网络启动注释掉了。为什么?因为很多嵌入式设备在启动时不需要网络,可以等应用起来后再按需启动网络。这样能省下几百毫秒。

2.4 测量工具:bootgraph与bootchart

没有测量就没有优化。我强烈建议在开始优化之前,先用工具把启动过程完整地记录下来。

bootgraph

bootgraph是内核自带的一个工具,它可以把内核启动的各个阶段绘制成一张时序图。使用方法很简单:

# 1. 在内核配置中打开
CONFIG_FUNCTION_TRACER=y
CONFIG_BOOT_TRACER=y

# 2. 启动时添加参数
initcall_debug printk.time=1

# 3. 启动后获取数据
cat /proc/boot_trace > /tmp/boot_trace.txt

# 4. 使用脚本生成图表
scripts/bootgraph.pl /tmp/boot_trace.txt > bootgraph.svg

生成的SVG图会清晰地显示每个initcall的耗时。我一般会重点关注那些耗时超过10ms的initcall,它们往往是优化的目标。

bootchart

bootchart是一个用户空间的工具,它可以记录从init开始到系统完全启动的整个过程。它比bootgraph更全面,因为它能记录用户空间的服务启动情况。

# 安装bootchart
apt-get install bootchart

# 配置bootchart在启动时运行
# 在/etc/init.d/bootchart中添加启动脚本

# 启动后查看结果
# 生成的日志在 /var/log/bootchart.tgz
# 使用pybootchartgui查看
pybootchartgui /var/log/bootchart.tgz

bootchart的输出是一张彩色的时序图,每个进程的启动时间、CPU使用率、磁盘I/O都一目了然。我个人特别喜欢用它来发现那些串行启动的服务——明明可以并行,却因为依赖关系写错了,导致一个等一个。

避坑指南:我曾经在一个项目上发现bootchart记录的时间比实际启动时间短了很多。后来查了半天,发现是bootchart本身在启动时占用了CPU资源,导致其他服务变慢了。所以,测量工具本身也会影响测量结果。建议在优化前后使用相同的测量条件,对比相对值而不是绝对值。

2.5 综合优化策略

好了,前面我们把启动过程拆成了三段,每段都讲了优化方法。现在来总结一下,一个完整的优化流程应该是怎样的:

  1. 先测量:用bootgraph和bootchart记录完整的启动过程,拿到基线数据。
  2. 分析瓶颈:找出耗时最长的阶段,是Bootloader?内核驱动?还是用户空间服务?
  3. 针对性优化:根据瓶颈所在,选择对应的优化手段。
  4. 验证效果:再次测量,对比优化前后的数据。
  5. 反复迭代:启动时间优化很少能一步到位,通常需要多次迭代。

我的经验:有一次做车载信息娱乐系统的启动优化,目标是把启动时间从12秒降到5秒以内。我们团队花了三周时间,最终做到了4.2秒。其中最大的收益来自两个地方:一是把U-Boot的DDR training从完整模式改成快速模式(省了1.5秒),二是把用户空间的GUI启动从串行改成并行(省了2秒)。你看,找准瓶颈,效果立竿见影。

最后说一句:启动时间优化不是一锤子买卖。产品迭代过程中,每次添加新功能、新驱动,都可能影响启动时间。建议把启动时间测试纳入CI/CD流程,每次构建后自动跑一遍,一旦启动时间超标就报警。这样能防止优化成果被后续开发破坏。