3. 内存分配器原理:malloc/free在QNX中的实现、伙伴系统与slab分配器

说到内存分配,很多人觉得不就是malloc和free吗?

嗯,在QNX这种实时系统里,事情没那么简单。我早年做第一个QNX驱动项目时,就因为在中断处理里调了malloc,结果系统直接崩了。从那以后,我花了不少时间研究QNX的内存分配机制。

今天咱们就聊聊QNX里malloc/free到底怎么工作的,以及背后的伙伴系统和slab分配器。

3.1 QNX中malloc/free的实现架构

QNX的malloc/free不是自己从头写的。它用的是标准C库的实现,但针对实时系统做了不少优化。

说白了,QNX的malloc底层依赖两个东西:

  • mmap:从内核申请大块内存
  • sbrk:调整进程堆的大小

但QNX有个特殊的地方——它支持多进程,每个进程有自己的地址空间。所以malloc分配的内存,默认只在当前进程可见。

关键点:QNX的malloc默认不是多核安全的。如果你在多线程环境里频繁malloc/free,记得加锁,或者用mtmalloc替代。

我建议驱动开发中尽量少用malloc。为什么?因为malloc可能触发page fault,这在中断上下文里是致命的。我在项目中遇到过,一个同事在ISR里调了malloc,系统直接panic。后来我们改用预分配的内存池,问题就解决了。

3.2 伙伴系统(Buddy System)

伙伴系统是内核管理物理内存的核心算法。QNX的PROCESS MANAGER(procnto)就用它来管理物理页。

它的原理其实不复杂:

  • 把内存按2的幂次分成块
  • 最小的块叫一个page(通常是4KB)
  • 分配时找大小合适的块,如果找不到就拆更大的块
  • 释放时检查相邻块是否空闲,是的话就合并

举个例子:

// 假设系统有16个page,初始状态是1个16页的大块
// 你要分配4个page

// 步骤1:找大小为4的块,没有
// 步骤2:把16页的块拆成两个8页的块
// 步骤3:把第一个8页的块拆成两个4页的块
// 步骤4:分配第一个4页的块

// 释放时:
// 释放4页的块,检查它的"伙伴"(相邻的4页块)是否空闲
// 如果空闲,合并成8页的块
// 继续检查8页的伙伴,以此类推

你想想看,这个算法的好处是什么?碎片少,分配快。但坏处也很明显——内部碎片。比如你要3个page,系统给你4个,那1个page就浪费了。

实战经验:我在做视频采集驱动时,需要分配大量2MB的DMA缓冲区。如果用伙伴系统直接分配,会产生很多碎片。后来我改用连续内存分配器(CMA),专门预留了连续物理内存区域,性能提升很明显。

3.3 Slab分配器

伙伴系统分配的是page,但驱动里经常要分配几十字节的小对象。比如一个网络包描述符、一个USB请求块。这时候用伙伴系统就太浪费了。

Slab分配器就是解决这个问题的。

它的核心思想:

  • 每种对象类型维护一个缓存(cache)
  • 每个缓存由多个slab组成
  • 每个slab是一块连续内存,被划分成固定大小的对象
  • 分配时直接从slab里取一个对象,释放时放回去

QNX的slab实现和Linux类似,但更轻量。我记得QNX的slab分配器叫mmap_slab,在libc里实现。

看个实际例子:

// QNX中创建slab缓存
#include <sys/mman.h>

// 创建一个存放my_struct的slab缓存
slab_cache_t *my_cache;
my_cache = slab_cache_create("my_struct_cache",
                             sizeof(my_struct_t),
                             0,    // 对齐方式,0表示默认
                             NULL, // 构造函数
                             NULL); // 析构函数

// 分配一个对象
my_struct_t *obj = slab_cache_alloc(my_cache);

// 释放对象
slab_cache_free(my_cache, obj);

// 销毁缓存
slab_cache_destroy(my_cache);

注意:slab_cache_create创建的缓存是进程私有的。如果你要在共享内存里使用,需要用shm_open创建共享内存对象,然后在上面构建自己的slab分配器。

我曾经在做一个多核网络驱动时,每个核都需要频繁分配和释放sk_buff。如果用全局的malloc,锁竞争会非常严重。后来我为每个核创建了独立的slab缓存,性能提升了将近40%。

3.4 三种分配器的对比

分配器 适用场景 优点 缺点
malloc/free 用户态通用分配 使用方便,标准接口 可能触发page fault,不适合中断上下文
伙伴系统 内核物理页管理 分配快,外部碎片少 内部碎片,不适合小对象
Slab分配器 小对象频繁分配 速度快,无碎片 每种对象需要单独创建缓存

3.5 驱动开发中的内存分配建议

做了这么多年QNX驱动,我总结了几条经验:

  1. 中断上下文绝对不要用malloc。用预分配的内存池或者slab缓存。
  2. DMA缓冲区用连续内存。QNX提供了mmap_device_memoryposix_memalign来分配物理连续的内存。
  3. 多核场景用per-CPU缓存。避免锁竞争,性能会好很多。
  4. 监控内存碎片。QNX的pidin命令可以查看进程内存使用情况,showmem可以看内核内存。

核心原则:驱动里的内存分配,能静态分配就别动态分配。实在要动态,用slab或者内存池,别直接用malloc。

嗯,关于QNX的内存分配器,今天就聊这么多。下一章咱们会深入DMA缓冲区的分配和管理,到时候会用到今天讲的这些知识。