4、物理内存管理:连续物理内存分配、非连续物理内存映射、内存碎片处理

说到QNX的物理内存管理,我得先坦白一件事。早年我刚接触嵌入式系统时,总觉得内存管理嘛,不就是malloc和free那点事?直到我在一个工业相机项目上栽了跟头——DMA传输老是莫名其妙失败,查了三天才发现是物理内存不连续导致的。嗯,从那以后,我对物理内存管理就再也不敢马虎了。

今天咱们就聊聊物理内存管理的三个核心问题:连续分配、非连续映射、以及那个让人头疼的内存碎片。

4.1 连续物理内存分配:为什么这么重要?

你想想看,DMA控制器要搬运数据,它可不像CPU那样有MMU帮忙做地址转换。DMA看到的就是物理地址,而且它要求这些地址必须是连续的。为什么?因为DMA的传输描述符通常只给一个起始地址和长度,它不会像CPU那样去查页表。

在QNX里,分配连续物理内存最常用的接口是mmap()配合SHM_PHYS标志。我个人习惯这样写:

#include <sys/mman.h>
#include <stdint.h>

// 分配4个物理页(16KB)的连续内存
size_t size = 4 * 4096;
uintptr_t phys_addr;
void *virt_addr;

// 关键:使用SHM_PHYS标志
int fd = shm_open(SHM_PHYS, O_RDWR, 0);
if (fd == -1) {
    perror("shm_open failed");
    return -1;
}

// 先分配物理页
if (posix_memalign(&virt_addr, 4096, size) != 0) {
    perror("posix_memalign failed");
    close(fd);
    return -1;
}

// 锁定物理内存,获取物理地址
if (mlock(virt_addr, size) == -1) {
    perror("mlock failed");
    free(virt_addr);
    close(fd);
    return -1;
}

// 获取物理地址(QNX特有)
phys_addr = mmap_device_io(size, 0);  // 这里需要根据实际情况调整
⚠️ 注意:连续物理内存是稀缺资源。系统启动后,物理内存会被内核和各个进程瓜分,剩下的空闲页可能东一块西一块。你申请4页连续内存,系统可能要把分散的页挪到一起——这个过程叫内存规整(memory compaction)。我在项目中遇到过,系统跑久了,连续内存分配就会失败,因为碎片太多了。

4.2 非连续物理内存映射:DMA的另一种思路

那如果实在分配不到连续物理内存怎么办?别急,QNX提供了另一种方案——非连续物理内存映射。说白了,就是用页表把不连续的物理页拼成连续的虚拟地址空间。

这个技术叫IOMMU(I/O Memory Management Unit),或者叫SMMU(System MMU)。它给DMA控制器也配了一个MMU,这样DMA看到的地址空间也可以是连续的虚拟地址,底层映射到不连续的物理页。

在QNX里,使用mmap()配合MAP_PHYS标志可以实现类似效果:

// 非连续物理页的集合
struct sge_list {
    uintptr_t phys_addr;
    size_t len;
} sg_list[4] = {
    {0x10000, 4096},
    {0x25000, 4096},
    {0x38000, 4096},
    {0x4A000, 4096}
};

// 创建分散-聚集列表
// 每个DMA描述符指向一个物理页
for (int i = 0; i < 4; i++) {
    dma_desc[i].src_addr = sg_list[i].phys_addr;
    dma_desc[i].length = sg_list[i].len;
    dma_desc[i].next_desc = &dma_desc[i+1];
}
dma_desc[3].next_desc = NULL;  // 最后一个
💡 经验之谈:我曾经在一个视频采集卡驱动里用过这种方案。采集卡需要连续的内存来存放一帧图像,但系统跑了一天后,连续内存已经分配不到了。后来改用SG-DMA(分散-聚集DMA),把一帧图像分散到4个不连续的物理页里,DMA控制器自动一个一个页传输。虽然传输延迟稍微大了点,但至少系统不会崩溃了。

4.3 内存碎片处理:一个不得不面对的现实

内存碎片,说白了就是系统跑久了,物理内存被切得七零八落。你申请一块大的连续内存,系统说「对不起,没有」。这个问题在嵌入式系统里尤其严重,因为物理内存本来就小。

碎片分两种:

  • 外部碎片:空闲页分散在各处,无法满足大块连续分配
  • 内部碎片:分配的内存块比实际需要的大,浪费了空间

QNX处理碎片的手段,我总结了几条:

方法 原理 适用场景
内存规整 移动已分配页,合并空闲页 系统空闲时主动触发
伙伴系统 按2的幂次分配,合并相邻空闲块 内核默认分配器
slab分配器 为常用对象预分配固定大小缓存 驱动中频繁分配/释放小对象
内存池 预先分配一大块,自己管理 实时性要求高的场景

我个人最推荐的是内存池方案。为什么?因为嵌入式系统的内存分配模式往往是固定的——比如网络驱动总是分配1514字节的包缓冲区,音频驱动总是分配4KB的DMA缓冲区。你提前把这些内存池建好,运行时直接从池里取,既快又不会产生碎片。

// 一个简单的内存池实现
typedef struct {
    void *pool_start;
    size_t block_size;
    int total_blocks;
    int free_blocks;
    void *free_list;  // 空闲块链表
} mem_pool_t;

// 初始化内存池
mem_pool_t* pool_create(size_t block_size, int count) {
    mem_pool_t *pool = malloc(sizeof(mem_pool_t));
    pool->block_size = block_size;
    pool->total_blocks = count;
    pool->free_blocks = count;
    
    // 分配一大块连续物理内存
    pool->pool_start = mmap(NULL, block_size * count,
                             PROT_READ|PROT_WRITE,
                             MAP_PHYS|MAP_ANON, NOFD, 0);
    
    // 初始化空闲链表
    pool->free_list = pool->pool_start;
    char *ptr = (char*)pool->pool_start;
    for (int i = 0; i < count - 1; i++) {
        *(void**)(ptr + i * block_size) = ptr + (i+1) * block_size;
    }
    *(void**)(ptr + (count-1) * block_size) = NULL;
    
    return pool;
}

// 从池中分配
void* pool_alloc(mem_pool_t *pool) {
    if (pool->free_blocks == 0) return NULL;
    
    void *block = pool->free_list;
    pool->free_list = *(void**)block;
    pool->free_blocks--;
    return block;
}

// 释放回池中
void pool_free(mem_pool_t *pool, void *block) {
    *(void**)block = pool->free_list;
    pool->free_list = block;
    pool->free_blocks++;
}
🔑 核心要点:
  • 连续物理内存是DMA传输的基础,但也是稀缺资源
  • 非连续映射(SG-DMA/IOMMU)是解决连续内存不足的有效手段
  • 内存碎片不可避免,但可以通过内存池、伙伴系统等策略缓解
  • 驱动开发中,建议提前规划好内存使用模式,避免运行时动态分配大块连续内存

最后说一句,内存管理没有银弹。每个系统都有自己的特点,你得根据实际需求选择方案。我见过有人为了追求极致性能,把所有DMA缓冲区都预分配好,结果系统启动慢了3秒;也见过有人图省事每次都动态分配,结果系统跑了两天就崩了。嗯,平衡才是关键。