一、嵌入式Linux内存碎片化的本质与危害

很多做嵌入式开发的朋友,肯定都碰到过这么个头疼的事:设备跑了几个月甚至几周后,明明系统显示总内存还有剩,申请一块连续的内存却失败了。比如要申请一块2MB的连续内存做视频缓存,系统显示还有5MB空闲,就是申请不到,最后只能报错、重启甚至直接死机。这就是嵌入式Linux里最常见的内存碎片化问题。

简单说,内存碎片化就像家里的储物间:一开始都是整整齐齐的空货架,放东西、拿东西都方便;用久了之后,货架上的空位都是一块一块的小碎块,比如左边空10cm、右边空15cm、中间空8cm,总空位加起来能放一个50cm的箱子,但没有一块连续的空位够放,这就是碎片化。

嵌入式设备的内存本来就小,比如很多工业控制、智能家居设备的内存只有几十MB到几百MB,碎片化的危害比普通PC大得多:轻则导致功能异常(比如视频流断流、传感器数据丢失),重则直接让设备宕机,给生产或用户体验带来麻烦。

二、嵌入式Linux内存碎片化的常见解决方法

针对嵌入式Linux的内存碎片化问题,目前有几种经过大量实践验证的解决方法,我们一个个拆解,每个方法都会配具体的代码示例,方便大家直接套用。

2.1 预分配内存池法

预分配内存池是最常用的解决方法,核心思路是“把连续的大内存提前占住,自己管理”,避免被系统的碎片化搞乱。比如你需要经常申请16KB、32KB的内存,就提前从系统申请一块连续的大内存(比如1MB),自己划分成固定大小的块,需要用的时候从自己的池子里拿,用完再还回去,这样就不会出现系统级的碎片化问题。

这里我们用C语言写一个简单的预分配内存池示例,技术栈统一用C语言(嵌入式Linux最常用的开发语言)。

首先是内存池的实现代码:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// 内存池配置:总大小1MB,每个块大小16KB,总块数=1024KB/16KB=64块
#define MEM_POOL_SIZE (1024 * 1024)  // 内存池总大小:1MB
#define BLOCK_SIZE (16 * 1024)        // 每个块的大小:16KB
#define TOTAL_BLOCKS (MEM_POOL_SIZE / BLOCK_SIZE)  // 总块数:64

// 内存池结构体:管理内存池的状态
typedef struct {
    unsigned char pool[MEM_POOL_SIZE];  // 实际的内存池空间
    char block_used[TOTAL_BLOCKS];      // 记录每个块是否被占用:0=空闲,1=已占用
} MemPool;

// 初始化内存池:从系统申请内存,初始化状态
MemPool* mem_pool_init() {
    MemPool* pool = (MemPool*)malloc(sizeof(MemPool));
    if (!pool) {
        printf("系统内存不足,无法初始化内存池\n");
        return NULL;
    }
    // 初始化所有块为空闲
    memset(pool->block_used, 0, sizeof(pool->block_used));
    printf("内存池初始化成功:总大小1MB,共64个16KB块\n");
    return pool;
}

// 从内存池申请一个块
void* mem_pool_alloc(MemPool* pool) {
    if (!pool) {
        printf("内存池未初始化\n");
        return NULL;
    }
    // 遍历所有块,找第一个空闲的
    for (int i = 0; i < TOTAL_BLOCKS; i++) {
        if (pool->block_used[i] == 0) {
            pool->block_used[i] = 1;  // 标记为已占用
            printf("申请到第%d个内存块\n", i);
            // 返回对应块的起始地址
            return pool->pool + i * BLOCK_SIZE;
        }
    }
    printf("内存池已用完,无空闲块\n");
    return NULL;
}

// 释放内存块
void mem_pool_free(MemPool* pool, void* ptr) {
    if (!pool || !ptr) {
        printf("参数错误,无法释放\n");
        return;
    }
    // 计算要释放的块的索引
    int block_idx = (ptr - pool->pool) / BLOCK_SIZE;
    if (block_idx < 0 || block_idx >= TOTAL_BLOCKS) {
        printf("无效的内存地址,无法释放\n");
        return;
    }
    pool->block_used[block_idx] = 0;  // 标记为空闲
    printf("释放第%d个内存块\n", block_idx);
}

// 测试内存池的主函数
int main() {
    // 初始化内存池
    MemPool* pool = mem_pool_init();
    if (!pool) return -1;

    // 申请2个内存块
    void* ptr1 = mem_pool_alloc(pool);
    void* ptr2 = mem_pool_alloc(pool);

    // 释放第一个块
    mem_pool_free(pool, ptr1);

    // 再申请一个块,会复用第一个块的地址
    void* ptr3 = mem_pool_alloc(pool);

    // 释放所有块
    mem_pool_free(pool, ptr2);
    mem_pool_free(pool, ptr3);

    // 销毁内存池(实际项目中要调用,避免内存泄漏)
    free(pool);
    return 0;
}

这个内存池的运行结果大家可以自己跑一下,能看到申请、释放、复用的过程,整个过程中内存都是连续的,不会出现系统级的碎片化。

预分配内存池的优点很明显:实现简单、可控性强、完全避免系统级碎片化;缺点是如果预分配的内存太大,会占用系统内存,导致其他进程可用内存减少,而且如果块大小设计不合理,会造成浪费(比如你需要10KB的内存,却申请了16KB的块,剩下的6KB就浪费了)。

2.2 伙伴系统优化法

伙伴系统是Linux内核本身就带的内存分配算法,核心是把内存分成不同大小的块(比如2^0 KB、2^1 KB、2^2 KB…最大到2^10 KB),申请内存时找最匹配的块,释放时把相邻的空闲块合并成更大的块,从而减少碎片化。

嵌入式Linux的内核默认伙伴系统已经做了一些优化,但我们可以根据自己的设备调整内核参数,让伙伴系统更适配嵌入式场景。比如我们可以调整内核的“内存碎片整理阈值”,让内核更积极地整理内存碎片。

具体的调整方法是修改内核的配置,然后重新编译内核。我们先看需要修改的内核配置项(在menuconfig里可以找到):

# 开启内存碎片整理
CONFIG_COMPACTION=y
# 开启透明大页(可选,适合需要大连续内存的场景)
CONFIG_TRANSPARENT_HUGEPAGE=y
# 调整内存碎片整理的阈值(数值越小,整理越积极)
CONFIG_COMPACTION_SKIP=10

修改完配置后,重新编译内核,烧写到设备上。另外,我们还可以在设备运行时,通过sysfs接口手动触发内存碎片整理,比如:

# 触发内核内存碎片整理,整理完成后系统会返回整理结果
echo 1 > /proc/sys/vm/compact_memory

伙伴系统优化的优点是不需要修改应用代码,只需要调整内核配置,适合所有应用;缺点是内核的内存整理会占用CPU资源,在CPU性能弱的嵌入式设备上可能会影响其他功能的运行,而且整理效果受设备运行状态影响,不是100%可靠。

2.3 slab分配器优化法

slab分配器是Linux内核专门为小内存分配设计的算法,核心是把相同大小的内存块缓存起来,避免频繁申请和释放小内存导致的碎片化。比如内核经常需要申请16字节、32字节的小内存,slab分配器就会提前把这些大小的块缓存好,申请时直接拿,释放时放回缓存,减少系统级的碎片化。

对于嵌入式设备,我们可以调整slab分配器的参数,优化小内存的分配。比如调整slab缓存的大小,或者开启slab的合并功能。

具体的调整方法也是通过sysfs接口,比如我们可以查看slab缓存的状态,然后调整:

# 查看slab缓存的状态,能看到每个缓存的大小、已用、空闲情况
cat /proc/slabinfo

# 调整slab缓存的最大空闲数(数值越大,缓存的空闲块越多,减少碎片化)
echo 100 > /sys/kernel/slab/kmalloc-16/max_free

slab分配器优化的优点是专门针对小内存,能有效减少小内存分配导致的碎片化;缺点是只能优化内核级的小内存分配,应用级的小内存分配需要自己做优化,而且调整参数需要对slab的原理有一定了解,调整不当可能会导致内存浪费。

三、方法的应用场景与选择

不同的解决方法适合不同的场景,大家可以根据自己的设备情况选择:

  1. 预分配内存池:适合需要固定大小连续内存的场景,比如视频缓存、音频缓存、传感器数据缓冲区等,尤其是内存小、CPU弱的嵌入式设备,因为它不需要额外的CPU资源来整理内存。
  2. 伙伴系统优化:适合需要大连续内存(比如1MB以上)的场景,而且设备的CPU性能足够(至少是ARM Cortex-A7以上),因为内存整理会占用CPU。
  3. slab分配器优化:适合内核级小内存分配频繁的场景,比如设备上有很多驱动程序,频繁申请和释放小内存,这个方法能有效减少内核的碎片化。

四、注意事项

不管用哪种方法,都有几个需要注意的地方:

  1. 预分配内存池的块大小一定要设计合理,比如你需要10KB的内存,就把块大小设为12KB(不要设为16KB,避免浪费),或者设计可变大小的内存池(比如支持12KB、16KB两种块)。
  2. 伙伴系统的内存整理不要太频繁,比如不要每秒都触发整理,一般可以在设备空闲的时候触发(比如晚上设备没有用户操作的时候)。
  3. 所有的方法都要做充分的测试,比如预分配内存池要测试内存泄漏、申请失败的情况,伙伴系统优化要测试内存整理对设备性能的影响。
  4. 嵌入式设备的内存是有限的,不要过度优化,比如预分配的内存池不要超过总内存的30%,避免影响其他进程的运行。

五、文章总结

嵌入式Linux的内存碎片化问题是很多开发者的痛点,解决方法也有很多种,核心思路要么是“自己管理内存,避免系统级碎片化”,要么是“优化系统的内存管理算法,减少碎片化”。大家可以根据自己的设备情况,选择一种或多种方法结合使用,比如预分配内存池加伙伴系统优化,既能解决应用级的碎片化,又能解决内核级的碎片化。

最后要提醒大家,解决碎片化问题不是一劳永逸的,要根据设备的运行状态不断调整,比如设备的内存使用量增加了,就要调整内存池的大小或者伙伴系统的整理阈值,这样才能保证设备长期稳定运行。