4. VxWorks内存分区与区域管理

在 VxWorks 中,内存管理并不是“给任务一个 malloc 就结束”这么简单。实时系统最怕的不是内存不够,而是内存碎片、分配时间不确定、任务之间互相踩踏。VxWorks 用“系统内存池 + 内存分区(memPart)”这套机制来解决这些问题:系统启动时先建立一块全局内存池,之后所有动态内存都从池中划分;而每个分区则是一段被独立管理的连续内存区域,拥有自己的空闲链表和分配策略。理解分区,就理解了 VxWorks 内存隔离与实时性的基础。

系统内存池:一切动态内存的源头

VxWorks 启动时,BSP 会把物理内存中可用的一段交给内核,形成系统内存池(system memory pool)。它由内核对象 memSysPartId 标识,是默认分区。调用 malloc()、calloc()、free() 时,如果不指定分区,实际就是在系统内存池上操作。系统内存池同时还要承载任务控制块、消息队列、信号量、驱动缓冲区等内核对象的分配,因此它既是“公共仓库”,也是“内核自留地”。

系统内存池的关键属性包括:起始地址、总大小、当前空闲字节数、最大空闲块、分配算法。VxWorks 默认采用首次适配(first fit)加空闲块合并的策略,分配时从空闲链表头部开始找第一个足够大的块。这个策略分配速度快,但长期运行后容易在池中留下大小不一的空洞,也就是外部碎片。可以用 memPartShow(memSysPartId, 0) 查看系统池的实时状态,输出中会列出空闲块数量、最大空闲块和累计分配失败次数,这是判断系统池是否健康的第一手依据。

分区创建:把一块内存变成独立王国

分区创建的本质,是把一段连续内存的“管理权”交给一个独立的 PART_ID。常用接口是 memPartCreate():

char *poolAddr;
size_t poolSize = 0x100000;   /* 1 MB 示例 */
PART_ID myPart;

poolAddr = (char *)malloc(poolSize);   /* 从系统池借地 */
if (poolAddr == NULL) {
    /* 系统池不足,需检查 memPartShow 输出 */
}
myPart = memPartCreate(poolAddr, poolSize);
if (myPart == NULL) {
    /* 创建失败,通常是地址未对齐或大小不足 */
}

这里有一个容易被忽略的细节:memPartCreate() 要求传入的地址按 sizeof(long) 对齐,大小也要留出分区控制结构本身的开销。示例中 1 MB 是教学假设值,实际项目中应根据任务峰值需求加 20%~30% 余量。分区创建后,它和系统池就是两个独立的分配域:在 myPart 上调用 memPartAlloc(myPart, size) 不会影响系统池的空闲链表,反之亦然。这正是任务内存隔离的物理基础。

分区属性配置:让分配行为可预期

分区不是创建完就一成不变。VxWorks 允许对分区做几类关键配置:

  • 分配算法:可通过 memPartOptionsSet() 调整。首次适配适合通用场景;最佳适配(best fit)能减少大块浪费,但查找更慢;对于固定大小块频繁分配的场景,可考虑在分区之上再封装一层固定块池。
  • 块大小与对齐:分配结果按 sizeof(long) 对齐,若驱动或 DMA 需要 32 字节对齐,应在分区内自行向上取整,或使用 memPartAlignedAlloc()。
  • 统计与告警:通过 memPartShow() 定期采集空闲块数、最大空闲块、失败次数。当最大空闲块持续下降而总空闲量不变时,说明碎片在加剧,这是实时性劣化的前兆。

下面这张图展示了系统内存池与两个任务分区之间的结构关系,以及分配请求如何被路由到各自分区:

VxWorks 系统内存池与任务分区结构 系统内存池 memSysPartId 内核对象 · 驱动缓冲 · 默认 malloc/free 空闲链表 A 统计计数器 任务分区 P1(myPart1) 1 MB 连续内存 · 独立空闲链表 空闲块 0x20000 空闲块 0x08000 memPartAlloc(myPart1, n) 任务分区 P2(myPart2) 512 KB 连续内存 · 独立空闲链表 空闲块 0x10000 空闲块 0x04000 memPartAlloc(myPart2, n) memPartCreate 借地 memPartCreate 借地 P1 与 P2 的空闲链表互不可见:一个分区耗尽不会直接破坏另一个分区的分配结构

任务内存隔离:从“共享池”到“责任田”

任务内存隔离的目标不是让任务完全不能共享内存,而是让分配行为的影响范围可控。在系统池上,一个任务疯狂申请内存,会把系统池的空闲块切碎,导致其他任务甚至内核对象分配失败;而在独立分区上,同样的行为只会耗尽该分区,其他分区和系统池仍然可用。这就是“故障域”的缩小。

实际工程中常见的做法是:为每个关键任务或任务组创建一个分区,任务内部所有动态分配都走该分区。例如一个采集任务和一个通信任务各自持有 myPart1、myPart2,采集任务即使因为突发数据量把分区撑满,通信任务仍能正常分配发送缓冲。判断隔离是否生效,可以观察三个信号:一是 memPartShow(myPart1, 0) 中失败次数上升时,memPartShow(myPart2, 0) 是否仍然正常;二是系统池的最大空闲块是否保持稳定;三是任务是否出现因分配失败而进入错误处理分支。

分区隔离不是万能的。如果任务把分区内指针错误地传给另一个任务,或者跨分区释放内存,仍然会造成破坏。因此分区隔离必须配合编码规范:谁分配、谁释放;跨任务传递数据时使用消息队列拷贝或明确的所有权约定。

配置与观察的实操要点

下面给出一个可参考的分区配置与检查清单,字段和阈值均属教学示例,实际项目需按任务峰值和硬件内存调整:

检查项示例值/命令判断依据
分区大小峰值需求 × 1.3运行 24 h 后空闲块不低于 20%
地址对齐32 字节对齐满足 DMA 与缓存行要求
碎片监控memPartShow(myPart, 0)最大空闲块不持续下降
失败处理分配返回 NULL 时记录任务名与大小能定位到具体任务和请求量
跨分区释放禁止代码审查中无跨分区 free

最后要强调:分区是 VxWorks 内存管理从“能用”走向“可预期”的关键一步。系统内存池负责兜底,分区负责隔离,属性配置负责让分配行为可观察、可调优。把这三者配合好,实时任务才不会因为一次内存分配而失去确定性。