行业资讯
嵌入式Linux启动优化实战:从10秒到2秒的6个技巧
嵌入式Linux启动优化实战从10秒到2秒的6个技巧一句话总结嵌入式Linux启动慢90%的情况不是内核太慢而是你没找对瓶颈——从uboot到用户态本文给6个可验证的提速技巧。做嵌入式Linux产品的人迟早会被客户问一个问题“为什么你们的设备开机要10秒隔壁老王家用RTOS的3秒就亮了。”每次听到这个问题我都想叹气。Linux是个通用操作系统它启动要干的事情比RTOS多太多了——初始化MMU、枚举PCIe设备、解析设备树、挂载根文件系统、启动systemd、加载各种服务……每个环节都在消耗时间。但叹气归叹气客户的要求你得满足。好在过去这些年我优化过好几个量产项目的启动时间总结下来核心思路只有一条不要盲目优化先找到瓶颈在哪。准备先测量再动手优化之前第一步是搞清楚时间花在哪。别靠猜靠数据。用printk time stamp看内核启动# 在kernel cmdline里加这个参数printk.time1# 启动后用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_TIMEyCONFIG_BOOT_PRINTK_DELAYnCONFIG_BOOT_CONFIGy# 启动后生成启动图cd/path/to/kernel/source scripts/bootgraph.pl/var/log/dmesgbootgraph.svg这个SVG图能清晰地展示内核初始化的每个阶段花了多少时间。一眼就能看出哪个驱动在磨洋工。技巧一减少uboot的无谓等待很多人忽略了uboot阶段的耗时。默认配置下uboot会等你按键中断启动这个等待时间通常是3秒。# uboot环境变量把bootdelay设为0setenv bootdelay0saveenv如果你需要偶尔进uboot命令行改成这样# 只等1秒或者检测特定GPIO电平决定是否等待setenv bootdelay1setenv prebootif 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# 优化读取方式连续读取比分次读取快得多mmcread${loadaddr}0x1000 0x4000用户态阶段 (典型3-10秒)内核阶段 (典型2-5秒)Uboot阶段 (典型2-4秒)超时上电BootROMuboot SPLuboot mainbootdelay等待加载内核镜像解压内核设备树解析驱动初始化挂载根文件系统init/systemd启动服务并行加载网络配置应用启动完成技巧二精简内核——把不需要的驱动统统干掉这是最直接也最有效的方法。很多人习惯了用发行版内核的.config——里面打开了几千个驱动选项启动时自然慢。# 查看当前内核配置中有多少驱动被编译为模块或内置grep-cm.config# 模块数量grep-cy.config# 内置数量# 对于一个嵌入式设备你应该的目标是# m 不超过50个实际加载的模块更少# y 不超过200个真正需要的驱动我用过的一个案例一个IP camera项目从默认的kernel defconfig开始驱动数量是y有842个m有376个。启动时间4.8秒。花了一天时间只保留平台需要的驱动# 先make localmodconfig基于当前系统加载的模块生成配置makelocalmodconfig# 然后手动过一遍关掉不需要的makemenuconfig# 重点关掉的# - 所有其他平台的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-compxz-b256K# 内核配置CONFIG_SQUASHFSyCONFIG_SQUASHFS_XZyCONFIG_OVERLAY_FSy# 内核cmdline配置root/dev/mmcblk0p2rootfstypesquashfssquashfs的好处挂载速度快不需要fsck体积小压缩后通常是ext4的40%-60%不需要日志加上overlayfs层做写入# 初始化脚本里mount-tsquashfs /dev/mmcblk0p2 /romount-ttmpfs tmpfs /rwmkdir/rw/work /rw/uppermount-toverlay overlay-olowerdir/ro,upperdir/rw/upper,workdir/rw/work /这个改动让根文件系统挂载时间从原来的0.8秒降到了0.1秒。技巧四systemd优化——延迟非关键服务很多嵌入式项目直接用了发行版的systemd配置。这意味着大量你根本不需要的服务会被启动。# 查看当前所有启用的服务systemctl list-unit-files|grepenabled# 关掉不需要的systemctl disable NetworkManager-wait-online.service# 这个服务经常卡30秒systemctl disable systemd-resolved systemctl disable systemd-timesyncd# 如果不需要多用户可以关掉gettysystemctl disable gettytty1对于确实需要的服务用After和Wants控制依赖关系让不关键的服务延迟启动# /etc/systemd/system/my-app.service [Unit] DescriptionMain Application Afterbasic.target Wantsnetwork.target [Service] ExecStart/usr/bin/my-app Typesimple [Install] WantedBymulti-user.target# /etc/systemd/system/log-upload.service不关键的服务延迟启动 [Unit] DescriptionLog Upload Afternetwork-online.target # 加个延迟让关键服务先跑 ExecStartPre/bin/sleep 10加速启动还有个利器使用systemd-analyze来分析启动瓶颈# 查看各阶段的耗时systemd-analyzetime# 输出示例# Startup finished in 1.2s (kernel) 3.4s (initrd) 5.6s (userspace) 10.2s# 查看每个服务的启动时间systemd-analyze blame# 看哪个服务最慢针对性优化技巧五内核解压加速——用LZ4替代gzip内核镜像是压缩后存到存储介质上的。uboot加载后解压这个解压过程是CPU密集型的。不同压缩算法的对比基于Cortex-A7 1GHz压缩算法镜像大小解压时间解压速度gzip4.2MB0.85s基准xz3.1MB2.10s慢2.5倍lz45.8MB0.18s快4.7倍zstd4.0MB0.35s快2.4倍如果存储空间不是极端紧张用LZ4压缩是最佳选择——虽然镜像大了40%但解压时间只有gzip的五分之一。# 内核配置CONFIG_KERNEL_LZ4y# uboot需要支持LZ4解压一般默认支持# 或者在内核里built-in LZ4解压代码对于一个8MB的内核镜像从gzip换成LZ4能省0.6-0.7秒。技巧六异步探测Async Probe——让慢驱动不阻塞启动这是最容易被忽略的技巧。有些硬件外设的初始化很慢——比如USB控制器需要枚举设备、WiFi模块需要加载固件、TSC/ADC需要自校准。默认情况下这些驱动是在do_initcalls阶段顺序初始化的。一个慢驱动会把所有后续驱动都堵住。解决方法让这些慢驱动异步探测。// 在驱动代码里staticintmy_slow_driver_probe(structplatform_device*pdev){// ... 初始化代码}staticstructplatform_drivermy_driver{.probemy_slow_driver_probe,.driver{.namemy-slow-device,.of_match_tablematch_table,.probe_typePROBE_PREFER_ASYNCHRONOUS,// 关键行},};或者在设备树里标注usb_controller { status okay; async-probe; // 这个驱动异步初始化 };# 也可以通过内核cmdline让指定的驱动异步探测driver_async_probemy_slow_driver,usb-storage,mmc_block效果如果有一个驱动占用了1.2秒初始化异步探测可以让它跟其他初始化并行执行。启动时间直接减去这1.2秒只要其他初始化的总时间不超过这个值。效果汇总以下是一个真实项目的优化前后对比ARM Cortex-A7 1.2GHz, eMMC, squashfs优化项优化前优化后节省时间uboot bootdelay3.0s0s3.0s精简内核驱动4.8s2.1s2.7ssquashfs替代ext40.8s0.1s0.7sLZ4替代gzip0.85s0.18s0.67ssystemd服务优化5.6s1.8s3.8s异步探测1.2s(阻塞)0s(并行)1.2s(隐式)总计~10.5s~2.1s~8.4s从一个让客户嫌弃的10秒开机的产品变成了2秒秒开的设备。一个小提醒启动优化做到3秒以内后再往下优化性价比就急剧下降。为了从2秒降到1秒你可能要花几周时间换来的是一个在用户感知上没有明显差异的结果。我通常的建议是2-3秒是嵌入式Linux产品的甜区——够快又不需要对系统做伤筋动骨的改动。如果你非要做到1秒以内可能得考虑PREEMPT_RT 混合内核架构那就是另一个故事了。先把上面这6个技巧试一遍再说。
郑州网站建设
网页设计
企业官网