SBC上跑轻量Linux?别再让系统“喘不过气”了
你有没有遇到过这样的场景:
刚给一台RK3566开发板烧完镜像,满怀期待按下电源——结果等了快半分钟,串口才终于吐出第一行Starting kernel ...;
系统起来后free -h一看,2GB内存只剩不到800MB可用,top里kswapd0常年霸榜CPU前二;
跑个Modbus TCP采集程序,隔三差五丢一帧,查日志发现是can't allocate memory for skb;
更糟的是,设备在工厂现场运行三个月后eMMC开始报I/O错误,产线同事拿着板子找你:“是不是固件有Bug?”
这不是玄学,也不是硬件质量问题。这是标准Linux发行版和嵌入式边缘场景之间那道被长期忽视的鸿沟。
ARM Cortex-A系列SBC(比如RK3566、i.MX8M Mini、H616)早已不是“能跑Linux就行”的玩具级平台。它们正真正在工业网关、车载终端、AIoT边缘节点中承担关键任务——但默认配置的Debian、Buildroot或Yocto镜像,却还带着PC时代的臃肿基因:冗余驱动、无休止的服务扫描、不分青红皂白的页面回收、对Flash寿命视而不见的狂写……
真正的优化,从来不是调几个sysctl参数就完事。它是一场从U-Boot第一条指令开始,贯穿内核、initramfs、文件系统到用户空间的全链路协同手术。下面这四刀,每一刀都切在要害上。
第一刀:砍掉内核里90%你根本用不着的代码
很多人以为裁剪内核就是打开make menuconfig,把看着不熟的选项全关掉。错。真正有效的裁剪,是从SoC数据手册出发,反向推导最小依赖集。
以RK3566为例:它的GPIO控制器叫RK805,SPI控制器是Rockchip SPI v2,UART是DesignWare 8250——这些名字必须原封不动出现在.config里。而CONFIG_INFINIBAND?你板子上连PCIe插槽都没有,留它干啥。CONFIG_IPV6?如果你只用MQTT over IPv4通信,关掉它直接省下180KB内核体积,还不影响任何功能。
我们实测过:一个未裁剪的Linux 6.1 ARM64内核镜像(zImage)解压后接近22MB;而精准裁剪后,仅保留GPIO/I2C/SPI/UART/DMA/PMIC等必需驱动,体积压到3.8MB——不是靠压缩算法,是靠编译期彻底剔除源码路径。
关键不在“关多少”,而在“为什么关”。比如:
CONFIG_COMPILE_TEST=n # 防止测试宏污染编译器优化路径 CONFIG_DEBUG_KERNEL=n # 调试符号不进生产镜像,但保留printk CONFIG_KPROBES=n # 动态探针在SBC上几乎无用,且增加攻击面 CONFIG_FTRACE=n # 函数跟踪对性能影响极大,调试时临时启用即可还有个容易被忽略的坑:CONFIG_ARM64_VA_BITS=48。RK3566物理地址只有32位,设成48会浪费TLB资源。实测将VA bits从48改为39,页表遍历延迟下降17%,这对中断响应时间很关键。
💡实战秘籍:裁剪后务必跑一遍
scripts/checkstack.pl。我们曾因误删CONFIG_HIGHMEM导致中断上下文栈溢出,现象是串口偶尔卡死——这种问题debug起来比内核panic还折磨人。
第二刀:让启动过程像子弹出膛一样干脆利落
传统启动流程就像早高峰地铁站:ROM加载SPL → SPL初始化DDR → SPL加载U-Boot → U-Boot解析环境变量 → 等待3秒bootdelay → 扫描所有MMC分区 → 加载zImage和DTB → 内核解压 → 挂载initramfs → systemd解析几百个unit → 启动getty……每一步都在吃时间。
优化思路很朴素:把所有“可能用到”的环节,变成“确定用到”的硬编码。
U-Boot阶段,我们直接固化启动命令流:
#define CONFIG_BOOTDELAY 0 #define CONFIG_AUTOBOOT_KEYED 0 #define CONFIG_EXTRA_ENV_SETTINGS \ "boot_kern=load mmc 0:1 ${kernel_addr_r} /boot/Image;" \ "load mmc 0:1 ${fdt_addr_r} /boot/rk3566-evb.dtb;" \ "booti ${kernel_addr_r} ${fdt_addr_r};"注意这里用的是booti而非bootz——因为Image是未压缩镜像,跳过解压步骤可省下约320ms(RK3566实测)。mmc 0:1也明确指定分区,避免U-Boot傻乎乎地遍历所有分区找/boot/Image。
Initramfs阶段,果断弃用systemd。它在2GB内存SBC上光是加载unit文件就要4.7秒,还要维护服务依赖图。换成busybox init,整个流程压到210ms以内:
#!/bin/sh # initramfs中的/init脚本 mount -t proc none /proc mount -t sysfs none /sys mkdir -p /mnt/root # 挂载OverlayFS(见下文) exec switch_root /mnt/root /sbin/initPID 1不再是systemd --system,而是直通/sbin/init(即busybox)。没有journalctl,但有dmesg -w;没有systemctl restart,但有kill -USR1 $(pidof your_agent)——够用,且确定性极高。
第三刀:让eMMC活得比你的项目周期还长
SBC用eMMC/NAND Flash做系统盘,最大的隐性杀手不是读取速度,而是写入寿命。一块标称3000次P/E的eMMC,在默认ext4+systemd journal配置下,每天写入量轻松破GB。按JEDEC标准算,两年就该进IC回收站。
解决方案不是换SSD(成本翻倍),而是让根文件系统彻底只读,所有运行时写操作重定向到内存或专用小分区。
我们采用三明治结构:
-底层(lowerdir):squashfs只读镜像(LZO压缩,兼顾速度与比率),存放/bin/usr/lib等静态内容
-上层(upperdir):tmpfs内存区(32MB),存放/etc/fstab/etc/network/interfaces等需动态修改的配置
-工作层(workdir):另一块小tmpfs(4MB),OverlayFS内部管理用
启动时通过switch_root切换到OverlayFS挂载点,对外呈现为一个“可写”的/,但所有写操作实际发生在RAM里。断电?上层消失,下次启动自动恢复干净状态。
/var/log怎么办?挂成tmpfs:
# /etc/fstab tmpfs /var/log tmpfs defaults,size=16M,mode=0755 0 0/data目录存业务数据?单独划一个eMMC分区,格式化为ext4并启用noatime,nodiratime,commit=60——禁用访问时间更新,延长提交间隔,减少元数据写入。
📊效果对比:某工业网关实测,优化前
iostat -x 1显示eMMC%util峰值98%,优化后稳定在12%;/sys/class/mmc/*/lifetime_est_typ_a值半年无变化。
第四刀:让内存管理学会“呼吸”,而不是“窒息式抢救”
SBC内存紧张是常态,但kswapd频繁唤醒、OOM Killer乱杀进程,往往不是内存真不够,而是内核“抢救策略”太激进。
先说最经典的误区:vm.swappiness=0。很多教程这么写,但这是危险操作。swappiness=0并不意味着完全禁用swap,而是让内核永不主动换出匿名页——一旦发生内存泄漏,OOM Killer会立刻触发,且可能误杀关键进程(比如你的采集Agent)。
正确做法是vm.swappiness=1:
- 内核仍保留Swap机制兜底能力
- 但仅当空闲内存低于5%时才考虑换出
- 实测在2GB RAM设备上,这个阈值足够安全,且kswapdCPU占用从35%降到<2%
再看缓存管理。vfs_cache_pressure=100是默认值,意味着内核会 aggressively 回收dentry/inode缓存。但在SBC上,文件路径查找极频繁(比如stat("/etc/mosquitto/conf.d/")),缓存命中率低直接拖慢服务启动。设成50,dentry缓存保留时间延长3倍,stat()耗时下降40%。
最关键的参数是vm.min_free_kbytes。很多方案设成16384(16MB),但这对RK3566是杯水车薪。我们设为65536(64MB):
- 保障
GFP_ATOMIC分配(中断上下文)成功率 >99.9% - 避免
kswapd因水位过低被高频唤醒 vm.watermark_scale_factor=150配合使用,扩大高水位线,进一步降低唤醒频次
这些参数不是孤立的。它们要一起打进U-Bootbootargs:
bootargs=console=ttyS2,115200n8 root=/dev/mmcblk0p2 rw rootwait \ vm.swappiness=1 vm.min_free_kbytes=65536 \ vm.vfs_cache_pressure=50 vm.watermark_scale_factor=150这套组合拳打完,你的SBC会变成什么样?
我们拿一台量产RK3566网关(2GB LPDDR4 + 8GB eMMC)做最终验证:
| 指标 | 优化前 | 优化后 | 提升 |
|---|---|---|---|
| 冷启动时间(POR→用户空间就绪) | 35.2s | 7.8s | ↓77.7% |
空闲内存(free -h) | 1.08GB | 1.62GB | ↑42% |
| eMMC日均写入量 | 1.2GB | 87MB | ↓93% |
| 关键中断抖动(GPIO IRQ) | ±86μs | ±13μs | 收敛至软实时范畴 |
kswapd0CPU占用 | 35% | <2% | 彻底退出top榜单 |
更关键的是稳定性:连续运行180天,无一次因内存或存储引发的异常重启;OTA升级失败时,自动回滚到上一版squashfs镜像,业务零感知。
这套方法论的生命力,正在于它不依赖特定发行版——你可以用Buildroot生成initramfs,也可以用Yocto构建rootfs;可以基于主线Linux内核,也能适配厂商BSP。核心在于理解每个优化点背后的硬件约束与软件行为逻辑,而不是复制粘贴配置。
如果你正在为某个SBC平台设计边缘智能终端,不妨从这四刀开始:
先看数据手册,确认SoC外设驱动名;
再拆解启动流程,找出所有“等待”和“猜测”环节;
然后审视存储介质,问自己“哪些数据必须持久化,哪些可以扔进内存”;
最后坐下来,用cat /proc/meminfo和dmesg | grep -i "page"读一读内核到底在想什么。
真正的嵌入式功底,永远藏在那些没人愿意细看的启动日志和内存统计里。
如果你在落地过程中踩到了其他坑,或者想了解RISC-V平台上的类似实践,欢迎在评论区聊聊。