嵌入式Linux内存优化:从OOM到稳定运行的6个实战技巧

17 阅读7分钟

嵌入式Linux内存优化:从OOM到稳定运行的6个实战技巧

嵌入式Linux开发最让人崩溃的事是什么?不是代码写不出来,是系统跑着跑着就OOM了。

更崩溃的是:你在开发环境怎么测都正常,一到客户现场跑了两天就挂了。客户拍来一张串口截图,上面写着"Out of memory: Killed process xxx"。

嵌入式系统的内存就跟房子的空间一样——你永远觉得不够,而且用着用着就满了。

今天这篇不讲大道理,直接给6个我在量产项目里用过的内存优化技巧。每一个都经过真实项目验证,能直接上板子用。


技巧一:先搞清楚"谁在吃内存"

很多人调内存问题的第一步就是"加swap"或者"调OOM killer参数"——这是错误的。你先得知道谁在吃内存。

三板斧:

# 1. 看系统整体内存状况
free -h
# 重点关注 available 这一行,比 free 更准

# 2. 看每个进程的内存占用
top -b -n 1 | head -20
# 或者用 htop(如果板子上有)

# 3. 最狠的一招:/proc/meminfo
cat /proc/meminfo
# 重点关注 MemTotal, MemAvailable, Buffers, Cached, MemFree

你可能会被结果吓到——很多时候内存大头不在你的应用里,而在内核的page cache文件系统缓存上。

graph TD
    A[系统内存] --> B[内核使用]
    A --> C[用户进程]
    A --> D[Page Cache/缓存]
    B --> E[Slab/内核对象]
    B --> F[驱动DMA内存]
    B --> G[内核堆栈]
    C --> H[主应用]
    C --> I[守护进程]
    C --> J[第三方库]

真实案例:我一个项目里系统隔几小时OOM一次,追了三天发现是内核的kmalloc-128 slab一直在增长——某个WiFi驱动的调试日志在每次收包时分配了一小块内存,但只有在日志缓冲区满时才释放。改了驱动里的一行dev_kfree_skb(),内存稳了。


技巧二:别让page cache吃掉你所有的"空闲内存"

嵌入式Linux开发者最容易掉进去的一个坑:看到free命令显示的可用内存很少,以为内存泄漏了。但你仔细看——大部分被"Buffers/Cached"占了。

Linux默认会尽量用空闲内存做文件缓存,以提高IO性能。这在服务器上是好事,在嵌入式设备上可能是灾难——因为你的"空闲"内存实际上被文件缓存占了,当应用真正需要时,系统才回收它们。

但回收也需要时间。如果应用突然申请大块内存,系统来不及回收page cache,就会触发OOM。

对策:限制page cache的上限

# 设置page cache上限(单位:字节)
# 假设总内存256MB,限制page cache最多占64MB
echo 67108864 > /proc/sys/vm/min_free_kbytes
echo 10 > /proc/sys/vm/vfs_cache_pressure
echo 50 > /proc/sys/vm/swappiness

更激进的做法(在启动脚本里加):

# 定期清理page cache,每10分钟清一次
while true; do
    sync && echo 3 > /proc/sys/vm/drop_caches
    sleep 600
done

注意drop_caches=3会同时清page cache、dentries和inode cache。如果频繁读取文件,性能会有损失。我的建议是只在内存紧张时才触发清理,不要按固定时间做。


技巧三:应用层的内存泄漏定位

嵌入式应用的内存泄漏是OOM的头号元凶。大部分人靠"目测代码"来找泄漏——效率极低。

我的工具组合:

# 方案1:Valgrind(如果板子内存够用)
valgrind --leak-check=full --show-leak-kinds=all ./my_app

# 方案2:AddressSanitizer(编译时打开)
# 在Makefile里加
CFLAGS += -fsanitize=address -g
LDFLAGS += -fsanitize=address -g

# 方案3:最轻量的——自己打内存快照
# 在代码关键位置加:
void print_mem_usage(const char *tag) {
    struct sysinfo si;
    sysinfo(&si);
    printf("[%s] total=%lu free=%lu used=%lu\n", 
           tag, si.totalram, si.freeram, si.totalram - si.freeram);
}

对于量产板子上Valgrind跑不动的场景(内存有限、CPU性能不足),我最推荐的是 mtrace

# 1. 代码里包含 <mcheck.h> 并在 main 开头调 mtrace()
# 2. 设置环境变量
export MALLOC_TRACE=/tmp/mtrace.log
./my_app
# 3. 分析结果
mtrace my_app /tmp/mtrace.log

这个工具能在不增加大量性能开销的情况下,帮你找到未释放的内存分配。


技巧四:静态分配优先,动态分配审慎

这可能是最"反现代编程"的建议,但做嵌入式的人都懂:堆是危险的。

我的三条铁律:

场景推荐做法原因
小数据结构(<1KB)栈上分配零碎片、零开销
中等数据结构(1KB-64KB)静态全局数组/池可预测性
大型/变长数据(>64KB)用内存池管理避免堆碎片

具体实现一个简单的内存池:

// mem_pool.h - 简单内存池
#define POOL_SIZE 10
#define BLOCK_SIZE 4096

struct mem_pool {
    void *blocks[POOL_SIZE];
    int used[POOL_SIZE];
};

void *pool_alloc(struct mem_pool *pool) {
    for (int i = 0; i < POOL_SIZE; i++) {
        if (!pool->used[i]) {
            pool->used[i] = 1;
            return pool->blocks[i];
        }
    }
    return NULL;  // 池满
}

void pool_free(struct mem_pool *pool, void *ptr) {
    for (int i = 0; i < POOL_SIZE; i++) {
        if (pool->blocks[i] == ptr) {
            pool->used[i] = 0;
            memset(ptr, 0, BLOCK_SIZE);  // 清空防信息泄漏
            return;
        }
    }
}

别笑,这个简单的内存池在我一个项目中把OOM从每周3次降到了零。 为什么?因为普通的malloc/free会带来堆碎片——系统运行一段时间后,堆被切成无数个小碎片,连一个4KB的连续块都分配不出来。


技巧五:内核层面的瘦身

有时候内存不是"泄漏"了,是"你的内核太胖了"。

一个默认配置的Linux内核,什么功能都打开,光内核映像就占10MB+ RAM。对于256MB以下的嵌入式设备,这是不可接受的。

必做的内核裁剪:

# 内核配置中重点检查
make menuconfig

# 1. 关掉所有不用的驱动
# 删除: CONFIG_SOUND, CONFIG_USB, CONFIG_WLAN(如果不用)
# 保留最低限度的块设备+文件系统+网络

# 2. 关掉调试选项
# 关: CONFIG_DEBUG_KERNEL, CONFIG_SLUB_DEBUG, CONFIG_LOCKDEP
# 这些在开发时有用,量产必须关

# 3. 调整内核内存参数
# General setup → Optimize for size (-Os)
# 关掉 CONFIG_SLUB_DEBUG_ON
# 设置 CONFIG_LOG_BUF_SHIFT=14(16KB日志缓冲区就够了)

# 4. 精简文件系统
# 用Buildroot而不是Yocto
# 关掉python、dbus、systemd-journald(如果不需要)
# 用busybox代替coreutils

我见过的最夸张的案例:某团队一个嵌入式项目,默认内核配置编译出来17MB,启动后占用45MB RAM。经过上面四步裁剪后,内核降到3.2MB,启动后占用11MB。省出了34MB给应用。 这34MB够跑一个完整的视频编码管道了。


技巧六:最容易被忽略的——ulimit + OOM策略

这是那些"偶尔才OOM一次"的场景最后一道防线。

# 在启动脚本里加
ulimit -v 196608  # 限制每个进程虚拟内存196MB(即192MB)
ulimit -d 49152   # 限制数据段大小48MB
ulimit -s 8192    # 限制栈大小8MB

# 设置OOM killer偏好
echo -17 > /proc/$(pidof my_app)/oom_adj
# -17 表示该进程永远不被OOM killer杀掉
# 适合核心进程(主控、安全监控等)

但是要注意:别把关键进程设成"不杀"就不管了。如果关键进程死锁了但内存不够,系统会杀掉别的进程——你可能失去更重要的功能。

更好的方案是做一个看门狗进程

// watchdog.c - 监控内存并及时重启
#include <sys/sysinfo.h>

int main() {
    while (1) {
        struct sysinfo si;
        sysinfo(&si);
        
        // 可用内存低于总内存10%时重启主应用
        unsigned long avail = si.freeram + si.bufferram + si.sharedram;
        if (avail < si.totalram / 10) {
            system("killall -9 my_app");
            system("sync && echo 3 > /proc/sys/vm/drop_caches");
            system("my_app &");
            syslog(LOG_WARNING, "内存不足,已重启应用");
        }
        sleep(30);  // 每30秒检查一次
    }
}

这个看门狗进程本身占内存很小(< 1MB),但能有效防止系统在内存不足时"慢慢死掉"的状态。


总结

嵌入式Linux内存优化,说到底是三个字:可预测

不是"内存越多越好",而是"在极端条件下内存消耗也是可控的"。

技巧适用场景效果
meminfo分析所有场景找到吃内存的元凶
限制page cache文件读写频繁的设备节省30-50%"可用内存"
泄漏定位内存不断增长从根源解决
内存池通信/音视频等高频分配消除堆碎片
内核裁剪所有嵌入式设备节省30-70%内核内存
ulimit+看门狗量产设备最后一道防线

最后说一句:不要在OOM已经发生了再去救。 好的内存管理是在设计阶段就规划好"系统在最高负载下到底需要多少内存",然后留出20%的余量。这样才能确保你的设备在现场跑一年、两年、五年——都不会因为内存问题而躺平。