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.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; // 最后一个
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秒;也见过有人图省事每次都动态分配,结果系统跑了两天就崩了。嗯,平衡才是关键。