第3章 VxWorks内存管理:内存分区、内存池、虚拟内存管理与泄漏检测

内存管理这个话题,说大不大,说小不小。在VxWorks里搞开发,你要是没跟内存打过几次交道,那都不好意思说自己做过嵌入式。我这些年踩过的坑,十有八九都跟内存有关。今天咱们就把这块掰开揉碎了聊一聊。

3.1 内存分区(Memory Partition)—— 最基础的内存分配方式

内存分区,说白了就是一块连续的内存区域,你可以在上面做动态分配和释放。VxWorks里最常用的就是memPartLib这套接口。

我个人习惯,在系统初始化的时候就创建好分区。为什么?因为运行时再创建,万一内存碎片化严重,可能就分配不出来了。

/* 创建一个内存分区 */
PART_ID myPartition;
char *pBuf;

myPartition = memPartCreate((char *)0x100000, 0x10000);  // 64KB分区
if (myPartition == NULL) {
    printf("分区创建失败!\n");
    return ERROR;
}

/* 从分区中分配内存 */
pBuf = (char *)memPartAlloc(myPartition, 1024);
if (pBuf == NULL) {
    printf("分配失败,分区可能已满或碎片化\n");
}

这里有个坑,我必须要提醒你。分区创建时指定的地址,如果是0,系统会自动从堆里分配。但如果你指定了固定地址,那这块内存就不能被其他模块用了。我在一个项目中就吃过这个亏——两个驱动模块各自创建了分区,结果内存重叠了,查了三天才找到原因。

注意:分区内存不会自动回收。如果你创建了分区但忘了释放,那就是内存泄漏。我曾经见过一个产品,运行一个月后内存耗尽,最后发现是某个模块每次请求都新建分区,用完不删。

3.2 内存池(Memory Pool)—— 固定大小分配的利器

内存池跟分区不一样。池子里每个块大小是固定的,分配和释放速度极快。你想想看,如果你的系统里频繁分配同样大小的数据包,用内存池再合适不过了。

我记得有一次做网络协议栈优化,数据包分配占了CPU时间的15%。换成内存池后,直接降到了2%。为什么?因为内存池没有碎片问题,分配就是取个链表头节点,释放就是挂回去,O(1)复杂度。

/* 创建内存池,每个块256字节,共100个块 */
MEM_POOL_ID myPool;
void *pBlock;

myPool = memPoolCreate(256, 100);
if (myPool == NULL) {
    printf("内存池创建失败\n");
    return ERROR;
}

/* 从池中取一个块 */
pBlock = memPoolAlloc(myPool);
if (pBlock != NULL) {
    memset(pBlock, 0, 256);
    /* 使用... */
    memPoolFree(myPool, pBlock);  /* 用完归还 */
}
小技巧:内存池的大小要选好。太小了不够用,太大了浪费。我一般会统计实际使用情况,然后加20%的余量。比如实际最大需要80个块,我就创建100个。

3.3 虚拟内存管理(VxVMI)—— 高级玩家的玩具

VxVMI是VxWorks的虚拟内存接口。说实话,在大多数嵌入式系统里,你根本用不上它。但如果你在做MMU相关的开发,或者需要保护关键内存区域,那VxVMI就是你的好帮手。

虚拟内存的核心作用有两个:地址映射权限控制。你可以把物理内存映射到任意虚拟地址,还可以设置读、写、执行权限。

/* 将物理地址0x200000映射到虚拟地址0xA0000000,大小4KB,可读写 */
STATUS status;
status = vmVirtToPhysMap(
    0xA0000000,      /* 虚拟地址 */
    0x200000,        /* 物理地址 */
    0x1000,          /* 大小4KB */
    VM_STATE_MASK_VALID | VM_STATE_MASK_WRITABLE  /* 有效且可写 */
);

if (status != OK) {
    printf("映射失败!检查MMU是否开启\n");
}

嗯,这里要注意。VxVMI要求系统必须开启了MMU。我遇到过有人直接在没开MMU的板子上调用这些接口,结果系统直接挂掉。所以用之前,先确认一下sysPhysMemDesc配置是否正确。

3.4 内存泄漏检测基础—— 别让内存悄悄溜走

内存泄漏,说白了就是申请了内存但忘了释放。在VxWorks里,这个问题特别隐蔽。因为嵌入式系统通常长时间运行,泄漏一点点可能几个月后才爆发。

我常用的检测方法有三种:

  1. 使用memShow()查看全局内存状态
  2. 使用memPartShow()查看分区使用情况
  3. 自己写钩子函数,记录每次分配和释放

先看最简单的memShow()

/* 在shell中执行 */
-> memShow

   FREE LIST:
   addr       size     tag
   ---------- -------- ----
   0x100100   1024     free
   0x100500   2048     free
   ...
   Number of free blocks: 12
   Number of allocated blocks: 8
   Total free bytes: 16384
   Total allocated bytes: 8192

如果allocated blocks一直在增长,而free blocks在减少,那基本可以确定有泄漏了。

我曾经在一个项目中,用memPartShow()定位到了一个隐藏了两年的泄漏。那个模块每处理1000个数据包就泄漏4个字节,平时根本看不出来。直到有一天系统连续运行了45天,内存耗尽重启了。

实战建议:在关键模块中,自己维护一个分配计数器。每次分配加1,释放减1。如果计数器不为0,说明有泄漏。我习惯在模块的show()函数里把这个值打印出来,方便调试。
/* 简单的泄漏检测封装 */
int g_allocCount = 0;

void *safeMalloc(size_t size) {
    void *p = malloc(size);
    if (p != NULL) {
        g_allocCount++;
    }
    return p;
}

void safeFree(void *p) {
    if (p != NULL) {
        free(p);
        g_allocCount--;
    }
}

/* 在定时任务中检查 */
if (g_allocCount != 0) {
    printf("警告:当前有%d个未释放的分配!\n", g_allocCount);
}
避坑指南:我曾经在中断服务程序里调用了malloc(),结果系统随机死机。因为malloc不是中断安全的。记住:中断里绝对不要做动态内存分配!如果非要用,用memPoolAlloc()这种确定性的接口。

3.5 总结一下

内存分区适合管理大块连续内存,内存池适合固定大小的高频分配,VxVMI适合需要地址映射和权限控制的场景。至于泄漏检测,别等到出问题了再查,从一开始就养成好习惯。

我最后再啰嗦一句:VxWorks的内存管理接口虽然多,但核心思想就一个——谁分配,谁释放。只要这个原则不乱,大部分内存问题都不会找上你。