
1. 项目概述与核心价值最近在搞一个基于米尔MYD-YG2LX开发板的项目客户对系统启动时间提出了非常苛刻的要求。原本觉得启动优化是老生常谈但真刀真枪地压榨每一毫秒时才发现这里面门道深得很。MYD-YG2LX这块板子核心是NXP的i.MX 8M Mini性能不错但默认的Yocto系统启动流程里“水分”也不少从按下电源键到应用完全就绪动辄十几二十秒在很多对实时性有要求的工业控制、边缘计算场景里这个延迟是完全不能接受的。这个应用笔记就是记录了我们团队如何将MYD-YG2LX的启动时间从近20秒优化到5秒以内的全过程。这不仅仅是跑个分、炫个技背后涉及到对Bootloader、内核、文件系统、应用启动链路的深度理解和系统性裁剪。如果你也在用类似的NXP i.MX系列平台或者任何基于ARM Cortex-A的嵌入式Linux系统并且被启动速度问题困扰那么这篇笔记里的思路、工具和具体操作应该能给你提供一条清晰的路径。我们踩过的坑、验证过的有效手段都会在这里毫无保留地分享出来。2. 启动时间分析找到瓶颈在哪里优化之前必须先测量。盲目地东改西改往往事倍功半。我们的第一要务是建立一个精确的、可重复的启动时间测量体系并定位出耗时最长的阶段。2.1 建立精确的启动时间测量基线在嵌入式Linux中测量启动时间有多种方法各有优劣。我们最终采用了组合方案硬件GPIO点灯法最直观、最可靠这是我们的基准方法。在U-Boot和内核的各个关键节点以及我们的应用程序入口通过代码控制一个未被使用的GPIO引脚输出高/低电平。同时用一台带高速采样功能的示波器或者逻辑分析仪捕获这个GPIO的波形。波形上的时间戳差值就是各个阶段精确的耗时。这种方法完全不受软件开销影响精度可达微秒级。实操要点选择一个容易焊接测试点的GPIO。在U-Boot中通常使用gpio命令或直接操作寄存器在内核中可以使用gpiodAPI在应用层就更简单了。确保点灯代码本身耗时极短几条汇编指令。内核启动日志时间戳在内核命令行中添加printk.time1参数这样内核的每条打印信息都会附带一个从启动开始的时间戳单位是秒。通过分析dmesg输出可以清晰地看到内核初始化各个子系统的时间点。注意事项这个时间戳的零点通常是内核解压后开始执行的时刻不包含Bootloader的时间。而且打印信息本身也有耗时对于毫秒级精度的分析可能不够但用于分析秒级的内核初始化瓶颈非常有效。systemd-analyze针对使用systemd的系统如果根文件系统使用了systemd那么systemd-analyze blame和systemd-analyze critical-chain命令是分析用户空间服务启动顺序和耗时的神器。它能清晰地告诉你哪个服务拖了后腿。我们的情况MYD-YG2LX的默认Yocto镜像使用了systemd这个工具成为了我们优化用户空间启动的核心依据。2.2 初始耗时分解与瓶颈定位通过上述方法我们对优化前的系统进行了全面测量得到了一个典型的启动时间分布图启动阶段大致耗时说明ROM Code - ATF (ARM Trusted Firmware)~200ms芯片内部ROM加载并运行第一级Bootloader耗时固定基本无法优化。ATF - U-Boot~150msATF进行基础安全初始化后跳转U-Boot。U-Boot 初始化~1200ms包含DDR初始化、外设扫描、环境变量加载等。优化重点。内核解压与早期初始化~400ms解压内核镜像进行最基础的硬件初始化。内核主要初始化~3000ms驱动探测、文件系统挂载、网络初始化等。耗时大户。根文件系统挂载与用户空间Init~1000ms挂载根文件系统启动systemd。systemd服务启动至目标应用~12000ms串行启动各种系统服务网络、蓝牙、显示管理等最后启动我们的应用。最大瓶颈。核心发现用户空间的systemd服务启动链是绝对的耗时大头占了总时间的60%以上。其次是内核初始化和U-Boot。这为我们指明了主攻方向并行化与裁剪用户空间服务同时精简内核与U-Boot配置。3. U-Boot启动优化为内核铺好快车道U-Boot作为内核的加载器其优化能直接减少内核等待硬件就绪的时间。我们的目标不是深度魔改U-Boot而是在其框架内做合理的配置调整。3.1 裁剪不必要的驱动与命令默认的U-Boot配置为了兼容性编译进了大量你可能用不到的驱动和命令。例如我们的产品不需要USB Host功能、不需要HDMI显示、不需要音频编解码那么相关的驱动就可以去掉。进入U-Boot配置菜单在Yocto中通常通过bitbake -c menuconfig virtual/bootloader来配置U-Boot。精准裁剪设备驱动 (Device Drivers)关闭所有确定不用的外设驱动如USB Host support,Video support中的显示输出Sound support等。命令集 (Command line interface)关闭调试类或不必要的命令如Boot timing,Test utilities下的部分命令。但boot,load,env等核心命令必须保留。文件系统支持 (Filesystem support)如果你只从EXT4或FAT分区启动可以关闭UBIFS,JFFS2等支持。网络支持 (Networking support)如果不使用U-Boot阶段的网络引导TFTP可以整个关闭能省下不少空间和时间。注意裁剪时要非常小心最好在改动前备份好配置文件。每次裁剪后都要测试能否正常启动到内核。建议采用“增量裁剪法”即每次只关闭一小部分功能测试通过后再继续。3.2 优化环境变量与启动脚本U-Boot启动时会处理bootcmd环境变量。默认的脚本可能包含一些延迟或条件判断。简化bootcmd查看printenv bootcmd。确保它是最直接的加载-启动流程。例如移除不必要的sleep命令将重复的变量检查合并。预计算内核加载地址避免在bootcmd中进行复杂的地址计算。将最终的内核加载地址、设备树加载地址、根文件系统参数等直接设置为明确的环境变量。使用静态MAC地址如果使用网络在环境变量中直接设置ethaddr避免U-Boot在启动时去读取EEPROM或生成随机MAC这会产生微小延迟。优化后的bootcmd示例# 更简洁直接的版本 setenv loadkernel load mmc 0:1 ${loadaddr} /boot/zImage setenv loadfdt load mmc 0:1 ${fdt_addr} /boot/imx8mm-myboard.dtb setenv bootargs console${console} root${mmcroot} rootwait rw setenv bootcmd run loadkernel; run loadfdt; bootz ${loadaddr} - ${fdt_addr}这个版本移除了任何分支判断直接执行加载和启动。3.3 关闭串口输出与调试信息U-Boot在启动时向串口输出大量信息会消耗时间。在产品发布版本中可以大幅减少甚至关闭输出。降低控制台日志级别在U-Boot源码的include/log.h或相关配置中将默认的LOGLEVEL从LOGLL_INFO(4) 降低到LOGLL_ERR(2) 或LOGLL_CRIT(1)。这样只会打印错误或严重信息。关闭特定模块的调试在U-Boot配置中搜索并关闭DEBUG相关的选项特别是你已确认稳定的驱动模块。实测效果经过上述裁剪和优化我们将U-Boot阶段的耗时从约1200ms减少到了约600ms几乎节省了一半的时间。4. Linux内核启动深度优化内核是启动过程中的另一个重量级角色。优化内核的核心思想是只加载必需的驱动只初始化当前硬件需要的功能。4.1 内核配置的极致裁剪与U-Boot类似内核的默认配置 (defconfig) 也是大而全的。我们需要为其“瘦身”。使用make savedefconfig先获取当前板级配置的基线make imx_v8_defconfig(以i.MX8为例)然后make savedefconfig生成一个最小化的.config文件。这个文件只记录与默认配置不同的选项是裁剪的好起点。进入菜单进行外科手术式裁剪执行make menuconfig重点关照以下区域General setup -关闭Kernel .config support和Enable access to .config through /proc/config.gz(发布版本不需要)。Device Drivers -这是重灾区。根据你的硬件清单关闭所有不存在的硬件驱动。例如没有PCIE设备关闭PCI support。没有音频设备关闭Sound card support。没有摄像头关闭Multimedia support下的V4L2相关项。仔细核对网络、输入设备、GPU、显示控制器等。File systems -只保留你根文件系统使用的类型如EXT4以及proc,sysfs,tmpfs等虚拟文件系统。可以关闭Btrfs,XFS,F2FS等。Networking support -如果你不需要内核级网络功能如防火墙、路由可以大幅裁剪。但基本的TCP/IP协议栈需要保留。Kernel hacking -全部关闭。这是调试信息的大本营对生产系统毫无用处且会显著增加内核大小和初始化时间。模块化 vs 内建对于不确定是否需要的驱动可以编译为模块 (m)而不是内建 (y)。模块是在用户空间需要时动态加载的不占用内核启动时间。但核心的、启动必须的驱动如MMC/SD卡、系统时钟必须内建。4.2 内核引导参数优化传递给内核的命令行参数 (bootargs) 也能影响启动速度。控制台静默添加quiet参数。这会抑制内核启动阶段的大部分信息输出能节省一些时间。但为了调试初期可以先不加。禁用不必要的控制台如果只有一个串口控制台可以指定consoletty1(假设是第一个串口)而不是多个console参数。根文件系统等待rootwait参数会让内核一直等待根设备就绪。如果确定你的存储设备初始化很快可以尝试去掉它但风险是可能因设备未就绪而挂载失败。我们选择保留但通过优化驱动加载来缩短等待时间。关闭内核模块自动加载添加modprobe.blacklist参数可以黑名单一些可能被自动加载的不必要模块。更彻底的方法是在用户空间配置。4.3 利用Initramfs进行预初始化可选高级技巧对于更极致的优化可以考虑使用Initramfs初始RAM文件系统。它的核心思想是将一个极简的根文件系统打包进内核镜像。内核启动后直接在这个内存文件系统中运行可以并行执行一些耗时的硬件初始化如等待网络稳定、解密加密分区然后再切换到真正的磁盘根文件系统。创建极简Initramfs包含busybox、必要的驱动模块、挂载脚本即可。修改内核配置启用Initial RAM filesystem and RAM disk (initramfs/initrd) support并指定你的Initramfs镜像路径。修改启动流程U-Boot加载包含Initramfs的内核内核启动后执行Initramfs中的/init脚本完成预初始化后再通过pivot_root或switch_root切换到真正的根文件系统。我们的权衡这种方法优化效果显著但增加了系统复杂性。考虑到我们的主要瓶颈在用户空间且硬件初始化不复杂本次优化没有采用此方案但它是一个非常重要的备选手段。5. 用户空间与systemd服务优化主战场这是启动优化中收益最高的部分也是工作量最大的部分。目标是让必需的服务尽快启动让非必需的服务延迟启动或不启动。5.1 使用systemd-analyze进行性能剖析首先我们需要一张清晰的“热力图”来知道时间花在哪了。# 查看整体启动时间分解 systemd-analyze time # 输出示例 # Startup finished in 2.345s (kernel) 12.678s (userspace) 15.023s # 查看每个服务的启动耗时按耗时排序 systemd-analyze blame # 输出会列出像 network-online.target, bluetooth.service, avahi-daemon.service 等及其耗时。 # 查看关键服务的启动链及其阻塞关系 systemd-analyze critical-chain graphical.target # 或者你的目标target如multi-user.target通过blame命令我们立刻发现了一些“罪魁祸首”NetworkManager-wait-online.service(等待网络就绪)systemd-networkd-wait-online.service 一些蓝牙服务 以及一些硬件管理服务如udisks2。5.2 禁用与延迟非关键服务彻底禁用对于产品完全用不到的服务直接systemctl disable --now servicename。例如我们的设备是工业网关不需要蓝牙、打印机服务(cups)、桌面管理器(gdm,lightdm)等。sudo systemctl disable bluetooth.service sudo systemctl disable cups.service sudo systemctl disable avahi-daemon.service # mDNS服务通常也用不到屏蔽单元文件更彻底通过systemctl mask可以防止服务被手动或间接启动。sudo systemctl mask bluetooth.service修改服务文件使其异步启动或解除依赖这是更精细的操作。以NetworkManager-wait-online.service为例它阻塞了network-online.target导致所有依赖网络的服务都被延迟。方案A推荐让我们自己的应用不依赖network-online.target而是依赖network.target。network.target只在网络管理服务NetworkManager或systemd-networkd启动后就绪不等待实际链路连通。我们的应用可以在启动后自己处理网络重连逻辑。方案B修改等待超时。编辑/etc/systemd/system/network-online.target.wants/NetworkManager-wait-online.service(如果是symlink需要找到源文件)在[Service]部分添加TimeoutStartSec10s让它最多等10秒而不是默认的很久。5.3 并行化服务启动systemd本身支持服务并行启动但很多服务通过After和Requires指令定义了严格的顺序。我们需要检查并优化这些依赖。分析服务依赖使用systemctl list-dependencies --before servicename和systemctl list-dependencies --after servicename查看一个服务被谁依赖以及它依赖谁。将顺序依赖(After)改为弱依赖(Wants)如果服务B只是希望在服务A之后启动但并不强制要求A成功可以将AfterA.service改为WantsA.service。这样systemd会尝试同时启动它们。使用Typeoneshot和RemainAfterExityes对于只执行一段脚本就退出的服务使用Typeoneshot可以让systemd在启动脚本执行完毕后立即认为该服务“已激活”而不需要等待一个常驻进程。结合RemainAfterExityes可以保持服务状态为活跃。5.4 优化我们自己的应用服务为我们自己的主应用编写一个高效的.service文件至关重要。优化前的服务文件示例可能存在的问题[Unit] DescriptionMy Application Afternetwork-online.target # 强依赖网络在线 Requiresnetwork-online.target [Service] Typesimple ExecStart/usr/bin/my_app --complex-arg-1 --complex-arg-2 Restarton-failure [Install] WantedBymulti-user.target优化后的服务文件[Unit] DescriptionMy Application Aftersysinit.target local-fs.target network.target # 在基础系统、本地文件系统和网络管理启动后启动 Wantsnetwork.target # 希望网络管理启动但不强制等待链路连通 # 移除了对 network-online.target 的依赖 [Service] Typeexec # 使用exec类型systemd会直接执行ExecStart的命令不经过shell解析更快。 ExecStart/usr/bin/my_app # 将参数固化到应用内部或使用环境文件 # 使用环境文件存储参数避免在命令行解析上浪费时间 EnvironmentFile/etc/default/my_app # 限制资源加快启动判断 LimitNOFILE1024 TimeoutStartSec10s # 设置启动超时避免卡死 [Install] WantedBymulti-user.target关键改动将Afternetwork-online.target改为Afternetwork.target和Wantsnetwork.target。Typeexec比simple启动稍快。将命令行参数移到环境文件减少启动字符串处理开销。设置合理的资源限制和超时帮助systemd更快判断服务状态。5.5 使用systemd的“模板服务”与“即时服务”对于需要启动多个实例的服务或者非常轻量级的服务可以考虑模板服务(.service)避免为每个实例写一个单独的文件。即时服务(.socket.service)对于网络服务使用socket激活。当第一个连接到来时systemd才启动服务进程可以避免在启动时预加载所有服务。6. 文件系统与存储优化存储设备的读取速度直接影响内核加载和应用程序启动。6.1 选择更快的根文件系统EXT4 with journaling disabledEXT4是可靠的标准选择。对于嵌入式系统如果对数据完整性要求不是极端苛刻可以在挂载时使用datawriteback或dataordered选项甚至关闭日志 (journal_async_commit或创建文件系统时-O ^has_journal)。警告这会增加断电丢数据的风险请根据产品需求谨慎评估。F2FS (Flash-Friendly File System)如果根文件系统在eMMC或SD卡上F2FS是为闪存设计的文件系统在小文件读写和随机写入上通常比EXT4有优势可能带来启动速度提升。需要内核支持。SquashFS (只读)对于完全不需要写入的系统分区可以考虑使用SquashFS。它是一个高度压缩的只读文件系统内核可以直接挂载。将根文件系统做成SquashFS镜像能减少从存储设备读取的数据量从而加快加载速度。通常需要结合OverlayFS来实现可写的上层。我们的选择考虑到可靠性和写入需求我们保留了EXT4但优化了挂载参数和文件布局。6.2 优化文件布局与访问模式关键文件前置确保内核镜像(zImage)、设备树(.dtb)、内核模块、以及应用依赖的核心库在存储介质eMMC/SD上物理位置尽量靠前低LBA地址。这可以通过在制作镜像时优先放置这些文件来实现。有些工具如genext2fs可以指定文件顺序。减少文件系统碎片一个连续存放的文件比碎片化的文件读取更快。在开发后期文件系统内容相对稳定后可以进行一次碎片整理虽然EXT4在线整理支持有限但可以通过备份-恢复镜像的方式实现。使用readahead或e4defrag对于已知的启动时需要读取的关键文件可以使用blockdev --setra设置块设备的预读值或者尝试用e4defrag对关键文件进行整理针对EXT4。6.3 利用内存文件系统 (tmpfs)将频繁读写的临时目录挂载为tmpfs可以避免对闪存的读写提升速度。常见的做法是在/etc/fstab中添加tmpfs /var/log tmpfs defaults,size10M 0 0 tmpfs /tmp tmpfs defaults,size50M 0 0注意要合理设置size参数避免耗尽内存。7. 综合调优与效果验证在完成上述所有环节的优化后需要进行系统性的测试和微调。7.1 构建与部署优化后的系统在Yocto项目中我们的优化最终体现在以下几个层面U-Boot配方 (u-boot-imx_%.bbappend)通过SRC_URI添加我们的补丁文件或者通过FILESEXTRAPATHS提供自定义的defconfig和boot.cmd。Linux内核配方 (linux-imx_%.bbappend)提供极致裁剪后的内核配置文件.config和可能的内核命令行参数补丁。根文件系统配置创建自定义的systemd单元文件、服务覆盖文件并打包到镜像中。通过IMAGE_INSTALL变量精确控制安装的软件包只装必需的。使用INITRAMFS_IMAGE如果决定使用Initramfs需要在Yocto中配置相应的镜像。7.2 最终效果测量与对比我们使用GPIO点灯法结合systemd-analyze进行了最终测量。优化前后的对比如下启动阶段优化前耗时优化后耗时优化手段U-Boot~1200ms~600ms裁剪驱动/命令优化环境变量与脚本内核初始化~3400ms~1800ms深度裁剪内核配置关闭调试优化引导参数用户空间Init~1000ms~400ms精简init流程systemd服务启动~12000ms~2200ms核心优化禁用非关键服务并行化解除网络强依赖优化应用服务单元总计~17600ms~5000ms整体优化约71%从接近18秒优化到了5秒以内效果非常显著。其中用户空间服务的优化贡献了最大部分节省了近10秒。7.3 常见问题与排查技巧实录在优化过程中我们遇到了不少问题这里记录下最典型的几个服务启动失败系统卡住现象优化后系统启动卡在某个服务无法进入shell。排查在U-Boot内核命令行中添加systemd.log_leveldebug和systemd.log_targetconsole参数。这样systemd会输出详细的调试信息到控制台可以看到具体是哪个服务失败以及原因。解决通常是依赖关系被破坏或服务文件语法错误。根据日志修复。网络服务启动慢但应用需要网络现象移除了network-online.target依赖后应用启动快了但有时启动后网络还没好导致应用连接失败。解决在应用内部实现网络等待和重连逻辑。例如应用启动后先检查网络接口状态如果未就绪则休眠片刻后重试而不是依赖systemd。这样将网络等待的“阻塞”转移到了应用内部且可以与其他初始化任务并行。内核裁剪过度硬件功能缺失现象优化后某个外设如USB无法工作。排查使用lsmod查看已加载模块使用dmesg | grep usb(举例)查看内核日志中关于该设备的探测信息。如果根本没有相关日志很可能是驱动被裁剪掉了。解决重新配置内核确保对应驱动被编译内建或模块。建议使用“增量回溯法”即逐步恢复可能相关的配置选项直到功能正常。启动时间波动大现象多次测量启动时间结果有几百毫秒的差异。原因可能是存储介质如SD卡的读取速度波动或者内核调度、中断处理的随机性。也有可能是某些服务如systemd-journald在压缩日志。缓解对于存储波动选择质量更好的eMMC芯片。对于日志可以考虑关闭systemd-journald的持久化存储Storagevolatile或减少保留时间。最后的建议启动优化是一个迭代和权衡的过程。没有一劳永逸的“银弹”。最好的方法是建立持续的测量体系比如在CI/CD流水线中集成启动时间测试每次改动都进行测量确保优化是有效的且没有引入新的问题。记住优化的黄金法则先测量再优化然后再测量。