嵌入式Linux启动优化实战:从10秒到2秒的6个技巧

一句话总结:嵌入式Linux启动慢,90%的情况不是内核太慢,而是你没找对瓶颈——从uboot到用户态,本文给6个可验证的提速技巧。

做嵌入式Linux产品的人,迟早会被客户问一个问题:“为什么你们的设备开机要10秒?隔壁老王家用RTOS的3秒就亮了。”

每次听到这个问题我都想叹气。Linux是个通用操作系统,它启动要干的事情比RTOS多太多了——初始化MMU、枚举PCIe设备、解析设备树、挂载根文件系统、启动systemd、加载各种服务……每个环节都在消耗时间。

但叹气归叹气,客户的要求你得满足。好在过去这些年我优化过好几个量产项目的启动时间,总结下来核心思路只有一条:不要盲目优化,先找到瓶颈在哪。


准备:先测量,再动手

优化之前,第一步是搞清楚时间花在哪。别靠猜,靠数据。

用printk time stamp看内核启动

# 在kernel cmdline里加这个参数
printk.time=1

# 启动后用dmesg看每条打印的时间戳
dmesg | head -20

你会看到类似这样的输出:

[    0.000000] Booting Linux...
[    0.850000] PCI: bus0: Fast back to back transfers enabled
[    2.340000] mmc0: new high speed SDHC card at address 1234
[    3.210000] EXT4-fs (mmcblk0p2): mounted filesystem
[    4.560000] Freeing unused kernel memory: 1024K
[    6.780000] systemd[1]: Starting System...

看到问题了吗?从内核启动到挂载根文件系统花了3.2秒,从挂载到systemd启动花了1.5秒。 这就是你的优化目标。

用bootgraph.py画启动时间图

内核源码里带了一个好用的工具:

# 配置内核
CONFIG_PRINTK_TIME=y
CONFIG_BOOT_PRINTK_DELAY=n
CONFIG_BOOT_CONFIG=y

# 启动后生成启动图
cd /path/to/kernel/source
scripts/bootgraph.pl < /var/log/dmesg > bootgraph.svg

这个SVG图能清晰地展示内核初始化的每个阶段花了多少时间。一眼就能看出哪个驱动在"磨洋工"。


技巧一:减少uboot的无谓等待

很多人忽略了uboot阶段的耗时。默认配置下,uboot会等你按键中断启动,这个等待时间通常是3秒。

# uboot环境变量:把bootdelay设为0
setenv bootdelay 0
saveenv

如果你需要偶尔进uboot命令行,改成这样:

# 只等1秒,或者检测特定GPIO电平决定是否等待
setenv bootdelay 1
setenv preboot 'if gpio input 98; then setenv bootdelay -1; fi'

这个改动立竿见影——直接省掉2-3秒

另一个容易忽略的点:uboot从存储介质读内核镜像的时间。如果你的内核镜像有8MB,从MMC读可能花0.5-1秒。考虑:

# 把内核镜像放到uboot可以快速读取的位置
# 或者用multi-fit image减少读取次数
setenv loadaddr 0x42000000
setenv fdtaddr 0x44000000
setenv kernel_size 0x800000

# 优化读取方式:连续读取比分次读取快得多
mmc read ${loadaddr} 0x1000 0x4000

用户态阶段 (典型3-10秒)

内核阶段 (典型2-5秒)

Uboot阶段 (典型2-4秒)

超时

上电

BootROM

uboot SPL

uboot main

bootdelay等待

加载内核镜像

解压内核

设备树解析

驱动初始化

挂载根文件系统

init/systemd启动

服务并行加载

网络配置

应用启动完成


技巧二:精简内核——把不需要的驱动统统干掉

这是最直接也最有效的方法。很多人习惯了用发行版内核的.config——里面打开了几千个驱动选项,启动时自然慢。

# 查看当前内核配置中有多少驱动被编译为模块或内置
grep -c '=m' .config    # 模块数量
grep -c '=y' .config    # 内置数量

# 对于一个嵌入式设备,你应该的目标是:
# =m 不超过50个(实际加载的模块更少)
# =y 不超过200个(真正需要的驱动)

我用过的一个案例:一个IP camera项目,从默认的kernel defconfig开始,驱动数量是=y有842个,=m有376个。启动时间4.8秒。

花了一天时间,只保留平台需要的驱动:

# 先make localmodconfig(基于当前系统加载的模块生成配置)
make localmodconfig

# 然后手动过一遍,关掉不需要的
make menuconfig

# 重点关掉的:
# - 所有其他平台的CPU支持(ARM64就只留ARM64)
# - 不需要的文件系统(只留你用的,比如ext4 + squashfs)
# - 不需要的网络协议(IPv6如果不用就关)
# - 不需要的驱动(声卡、显卡、WiFi、蓝牙……)

优化后=y降为186个,=m降为42个。启动时间从4.8秒降到了2.1秒——省了2.7秒,一分钱没花。


技巧三:用squashfs + overlayfs替代ext4做根文件系统

你可能习惯了用ext4做根文件系统。但在嵌入式场景下,ext4不是最优选择。

问题: ext4在挂载时需要对整个文件系统做fsck检查(虽然有fastboot标志,但依然有开销)。而且ext4的日志写操作在慢速存储(eMMC、SD卡)上会让启动变慢。

解决方案: 用squashfs(只读压缩文件系统)+ overlayfs(可写覆盖层)。

# 构建squashfs根文件系统
mksquashfs rootfs/ rootfs.squashfs -comp xz -b 256K

# 内核配置
CONFIG_SQUASHFS=y
CONFIG_SQUASHFS_XZ=y
CONFIG_OVERLAY_FS=y

# 内核cmdline配置
root=/dev/mmcblk0p2 rootfstype=squashfs

squashfs的好处:

  • 挂载速度快(不需要fsck)
  • 体积小(压缩后通常是ext4的40%-60%)
  • 不需要日志

加上overlayfs层做写入:

# 初始化脚本里
mount -t squashfs /dev/mmcblk0p2 /ro
mount -t tmpfs tmpfs /rw
mkdir /rw/work /rw/upper
mount -t overlay overlay -o lowerdir=/ro,upperdir=/rw/upper,workdir=/rw/work /

这个改动让根文件系统挂载时间从原来的0.8秒降到了0.1秒。


技巧四:systemd优化——延迟非关键服务

很多嵌入式项目直接用了发行版的systemd配置。这意味着大量你根本不需要的服务会被启动。

# 查看当前所有启用的服务
systemctl list-unit-files | grep enabled

# 关掉不需要的
systemctl disable NetworkManager-wait-online.service  # 这个服务经常卡30秒
systemctl disable systemd-resolved
systemctl disable systemd-timesyncd

# 如果不需要多用户,可以关掉getty
systemctl disable getty@tty1

对于确实需要的服务,用After=Wants=控制依赖关系,让不关键的服务延迟启动:

# /etc/systemd/system/my-app.service
[Unit]
Description=Main Application
After=basic.target
Wants=network.target

[Service]
ExecStart=/usr/bin/my-app
Type=simple

[Install]
WantedBy=multi-user.target
# /etc/systemd/system/log-upload.service(不关键的服务,延迟启动)
[Unit]
Description=Log Upload
After=network-online.target
# 加个延迟,让关键服务先跑
ExecStartPre=/bin/sleep 10

加速启动还有个利器:使用systemd-analyze来分析启动瓶颈:

# 查看各阶段的耗时
systemd-analyze time
# 输出示例:
# Startup finished in 1.2s (kernel) + 3.4s (initrd) + 5.6s (userspace) = 10.2s

# 查看每个服务的启动时间
systemd-analyze blame
# 看哪个服务最慢,针对性优化

技巧五:内核解压加速——用LZ4替代gzip

内核镜像是压缩后存到存储介质上的。uboot加载后解压,这个解压过程是CPU密集型的。

不同压缩算法的对比(基于Cortex-A7 @ 1GHz):

压缩算法 镜像大小 解压时间 解压速度
gzip 4.2MB 0.85s 基准
xz 3.1MB 2.10s 慢2.5倍
lz4 5.8MB 0.18s 快4.7倍
zstd 4.0MB 0.35s 快2.4倍

如果存储空间不是极端紧张,用LZ4压缩是最佳选择——虽然镜像大了40%,但解压时间只有gzip的五分之一。

# 内核配置
CONFIG_KERNEL_LZ4=y

# uboot需要支持LZ4解压(一般默认支持)
# 或者在内核里built-in LZ4解压代码

对于一个8MB的内核镜像,从gzip换成LZ4,能省0.6-0.7秒。


技巧六:异步探测(Async Probe)——让慢驱动不阻塞启动

这是最容易被忽略的技巧。有些硬件外设的初始化很慢——比如USB控制器需要枚举设备、WiFi模块需要加载固件、TSC/ADC需要自校准。

默认情况下,这些驱动是在do_initcalls阶段顺序初始化的。一个慢驱动会把所有后续驱动都堵住。

解决方法:让这些慢驱动异步探测。

// 在驱动代码里
static int my_slow_driver_probe(struct platform_device *pdev)
{
    // ... 初始化代码
}

static struct platform_driver my_driver = {
    .probe = my_slow_driver_probe,
    .driver = {
        .name = "my-slow-device",
        .of_match_table = match_table,
        .probe_type = PROBE_PREFER_ASYNCHRONOUS,  // 关键行!
    },
};

或者在设备树里标注:

&usb_controller {
    status = "okay";
    async-probe;  // 这个驱动异步初始化
};
# 也可以通过内核cmdline让指定的驱动异步探测
driver_async_probe=my_slow_driver,usb-storage,mmc_block

效果: 如果有一个驱动占用了1.2秒初始化,异步探测可以让它跟其他初始化并行执行。启动时间直接减去这1.2秒(只要其他初始化的总时间不超过这个值)。


效果汇总

以下是一个真实项目的优化前后对比(ARM Cortex-A7 @ 1.2GHz, eMMC, squashfs):

优化项 优化前 优化后 节省时间
uboot bootdelay 3.0s 0s 3.0s
精简内核驱动 4.8s 2.1s 2.7s
squashfs替代ext4 0.8s 0.1s 0.7s
LZ4替代gzip 0.85s 0.18s 0.67s
systemd服务优化 5.6s 1.8s 3.8s
异步探测 1.2s(阻塞) 0s(并行) 1.2s(隐式)
总计 ~10.5s ~2.1s ~8.4s

从一个让客户嫌弃的10秒开机的产品,变成了2秒"秒开"的设备。


一个小提醒

启动优化做到3秒以内后,再往下优化性价比就急剧下降。为了从2秒降到1秒,你可能要花几周时间,换来的是一个在用户感知上没有明显差异的结果。

我通常的建议是:2-3秒是嵌入式Linux产品的"甜区"——够快,又不需要对系统做伤筋动骨的改动。如果你非要做到1秒以内,可能得考虑PREEMPT_RT + 混合内核架构,那就是另一个故事了。

先把上面这6个技巧试一遍再说。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐