2. 内存池与分区管理实战

VxWorks 的内存管理核心是“分区(partition)”模型。系统启动时,内核从物理内存中划出一块区域建立系统内存分区,之后所有 malloc/free 默认都走这个分区。但在工程实践中,把全部动态内存都压在系统分区上是非常危险的:一个模块的泄漏会拖垮整个系统,不同生命周期的对象混在一起会加剧碎片,实时任务还可能因为一次慢速分配而被阻塞。因此,VxWorks 提供了 memPartCreate 系列接口,允许我们按用途建立独立的内存分区,把“谁用内存、用多少、何时释放”这三件事显式管理起来。

一、分区、块与空闲链表:先看清对象结构

一个内存分区由三部分组成:一段连续的物理内存(分区本体)、一个分区控制结构 PART_ID、以及若干条按块大小组织的空闲链表。分区创建后,内部内存被切分为若干空闲块,块头记录块大小和链表指针。分配时,memPartAlloc 从合适的空闲链表中摘下一块;释放时,memPartFree 把块归还链表,并尝试与相邻空闲块合并(coalesce)。这个“合并”动作是碎片控制的关键——如果相邻块能合并,大块需求仍可满足;如果被已分配块隔开,就会形成无法利用的碎片。

内存分区结构与分配/释放路径 分区本体(连续物理内存) 已分配 空闲 已分配 空闲 已分配 空闲 已分配 空闲 空闲链表(按块大小) 小链:60B → 70B → 80B 大链:105B memPartAlloc memPartFree 碎片控制:相邻空闲块合并 释放后若左右邻居均为空闲 → 合并为大块,避免外部碎片累积 若被已分配块隔开 → 形成不可用碎片,需靠分区隔离或固定块池规避

二、memPartCreate:把内存按用途切开

memPartCreate 的原型是 PART_ID memPartCreate(char *pPool, unsigned poolSize)。它接收一段已经存在的内存首地址和字节数,在这段内存上建立分区控制结构,返回分区 ID。注意:它不会自己申请内存,所以调用者必须先准备好内存。常见做法有两种:一是用 malloc 从系统分区申请一大块再交给 memPartCreate;二是用静态数组或 memPartAddToPool 把一段固定地址空间纳入管理。

/* 示例:为采集任务建立 256KB 独立分区 */
#define ACQ_POOL_SIZE  (256 * 1024)
static char acqPool[ACQ_POOL_SIZE] __attribute__((aligned(8)));

PART_ID acqPartId;
acqPartId = memPartCreate(acqPool, ACQ_POOL_SIZE);
if (acqPartId == NULL) {
    logMsg("acq partition create failed\n", 0,0,0,0,0,0);
}

工程上建议按“生命周期 + 访问频率 + 大小分布”三个维度切分。例如:采集任务每 10ms 分配一次小缓冲区,适合独立小分区;协议栈收发缓冲区大小相对固定,适合固定块池;文件系统缓存生命周期长、块较大,适合单独大分区。这样即使某个分区碎片化,也不会污染其他模块。

三、memPartAlloc 与 memPartFree:分配与释放的配对纪律

分区内分配使用 memPartAlloc(PART_ID partId, unsigned nBytes),释放使用 memPartFree(PART_ID partId, char *pBlock)。两者必须严格配对,且 memPartFree 传入的分区 ID 必须与分配时一致——把 A 分区分配的块还给 B 分区,会破坏 B 分区的空闲链表,属于典型致命错误。

/* 采集任务:分配-使用-释放 */
char *buf = (char *)memPartAlloc(acqPartId, 512);
if (buf != NULL) {
    /* 填充采集数据 */
    acqRead(buf, 512);
    process(buf, 512);
    memPartFree(acqPartId, buf);   /* 必须归还到同一分区 */
}

分配失败时返回 NULL,工程代码必须判断,不能直接使用。若在中断服务程序中分配,应使用 memPartAlloc 的中断安全版本或改用固定块池,避免在中断上下文里做链表遍历和合并操作。

四、碎片控制策略:从“能分配”到“长期能分配”

碎片分为内部碎片和外部碎片。内部碎片来自块头对齐和最小块粒度,通常可接受;外部碎片来自“空闲总量够、但没有连续大块”,是长期运行系统的主要风险。控制策略可以归纳为四条:

  • 分区隔离:不同大小分布、不同生命周期的对象放不同分区,避免互相切割。
  • 固定块池:对大小稳定的对象(如网络包、消息节点)使用固定块池,彻底消除外部碎片。
  • 分配大小分档:把请求归一到 32/64/128/256 等档位,减少零散尺寸造成的空洞。
  • 及时释放与合并:避免长生命周期块夹在短生命周期块之间,释放后让相邻空闲块有机会合并。
判断分区是否健康,可以周期性调用 memPartInfoGet 查看 numFreeBytes、numFreeBlocks 和 maxFreeBlockSize。如果 numFreeBytes 很大但 maxFreeBlockSize 很小,说明外部碎片已经严重,需要调整分区划分或改用固定块池。

五、一个可复用的分区规划检查表

检查项建议值/做法判断依据
分区数量按模块 3~6 个过多增加管理开销,过少失去隔离意义
单分区预留余量峰值用量 × 1.3~1.5覆盖突发和碎片损耗
最大块尺寸不超过分区 1/4避免单块挤占导致后续分配失败
释放配对同分区分配、同分区释放防止链表损坏
碎片监控每 5~10 秒采样一次maxFreeBlockSize 持续下降即预警

最后强调一个工程习惯:分区 ID 应作为模块私有变量保存,不要跨模块传递裸指针和分区 ID 的组合。分配和释放封装成模块自己的 xxxAlloc/xxxFree,内部固定分区,外部只看到业务对象。这样既保证配对纪律,也让后续把分区换成固定块池时,改动范围可控。