3. 物理内存管理:物理内存分配器、页面大小与对齐、内存池技术
好,咱们今天聊聊物理内存管理。这部分内容,说实在的,是QNX系统稳定性的基石。你想想看,所有进程、所有驱动,最终都要落到物理内存上。如果这层没管好,上层再花哨的优化都是白搭。
我个人习惯把物理内存管理拆成三个层面来看:分配器怎么工作、页面大小怎么选、以及内存池怎么用。咱们一个一个说。
3.1 物理内存分配器:伙伴系统与 slab
QNX内核里,物理内存分配器主要用的是伙伴系统(Buddy System)。这个算法很经典,核心思想就是把内存按2的幂次方分成块。比如4KB、8KB、16KB……一直到几MB。
分配的时候,找一块大小刚好够用的。如果没有,就拆大块。释放的时候,看看相邻的块是不是也空闲,是的话就合并回去。这样做的好处是外部碎片很少。
我在项目中遇到过一个问题:一个网络驱动频繁申请和释放64KB的缓冲区。用伙伴系统分配,每次都要拆128KB的块,用完了再合并。时间一长,系统响应就变慢了。后来我改用slab分配器,专门为这个64KB的缓冲区建了一个缓存池,性能一下就上来了。
核心要点:
- 伙伴系统:适合大块内存、频繁申请释放的场景。优点是碎片少,缺点是分配速度稍慢。
- slab分配器:适合固定大小的小对象。比如task_struct、inode等内核对象。速度快,缓存友好。
QNX里,你可以通过procctrl工具查看伙伴系统的状态。比如:
# 查看物理内存分配情况
procctrl -m
# 查看slab缓存信息
procctrl -s
嗯,这里要注意:slab分配器虽然快,但如果你申请的对象大小不是2的幂次方,可能会有内部碎片。比如你申请30字节,slab可能给你分配32字节的块,那2字节就浪费了。
3.2 页面大小与对齐:为什么4KB不够用?
页面大小,说白了就是内存管理的最小单位。QNX默认是4KB,但现代硬件都支持大页,比如2MB甚至1GB。
你可能会问:4KB不是挺好的吗?粒度细,浪费少。没错,但TLB(页表缓存)是有限的。如果你用4KB页面,一个需要2MB内存的进程,就要占用512个TLB条目。换成2MB大页,只需要1个条目。
我在调优一个视频处理系统时,发现内存带宽上不去。查了半天,发现是TLB miss太多。换成2MB大页后,性能提升了15%。
我的建议:
- 数据库、大数据处理:用2MB或1GB大页。TLB命中率提升明显。
- 实时控制、小内存设备:用4KB。避免浪费。
- 混合场景:QNX支持混合页面大小。你可以把关键数据放在大页里,普通数据用4KB。
对齐问题也很关键。QNX要求物理地址按页面大小对齐。比如你用2MB大页,起始地址必须是2MB的整数倍。我曾经犯过一个错:在驱动里直接申请物理内存,没注意对齐,结果DMA传输老是出错。后来加了mmap的MAP_ALIGNED标志才搞定。
代码示例:
#include <sys/mman.h>
// 申请2MB对齐的物理内存
void *addr = mmap(NULL, 2 * 1024 * 1024,
PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS | MAP_ALIGNED(21),
-1, 0);
// 注意:MAP_ALIGNED(21) 表示2^21 = 2MB对齐
3.3 内存池技术:自己管,更放心
内存池,说白了就是预分配一大块内存,然后自己管理。为什么需要它?因为内核的分配器再快,也快不过你自己定制的分配器。
我做过一个工业相机项目,每帧图像都要申请和释放几十个大小固定的缓冲区。用系统自带的malloc,每帧要花几百微秒。后来我改用内存池,预分配好所有缓冲区,每帧只需要从池里取一个,用完了还回去。时间降到了几微秒。
QNX里实现内存池,常用的方式有两种:
| 方式 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| posix_memalign + 链表管理 | 固定大小对象 | 实现简单,跨平台 | 需要自己处理并发 |
| shm_open + mmap | 多进程共享 | 支持进程间通信 | 管理稍复杂 |
一个简单的内存池实现思路:
// 伪代码,展示核心逻辑
struct mem_pool {
void *block; // 预分配的大块内存
size_t block_size; // 每个对象的大小
int free_list[MAX]; // 空闲索引列表
int free_count;
};
void *pool_alloc(struct mem_pool *pool) {
if (pool->free_count == 0) return NULL;
int idx = pool->free_list[--pool->free_count];
return pool->block + idx * pool->block_size;
}
void pool_free(struct mem_pool *pool, void *ptr) {
int idx = (ptr - pool->block) / pool->block_size;
pool->free_list[pool->free_count++] = idx;
}
避坑指南:
我曾经在内存池里犯过一个低级错误:没有处理并发。两个线程同时调用pool_alloc,结果拿到了同一个地址。后来加了原子操作或者互斥锁才解决。记住:内存池不是线程安全的,除非你明确设计成线程安全的。
3.4 实战建议:怎么选?
好了,三个技术都讲完了。你可能会问:我到底该用哪个?
我的经验是:
- 系统启动、驱动加载:用伙伴系统。这时候内存需求不确定,伙伴系统最灵活。
- 高频申请释放固定大小对象:用slab或内存池。比如网络包缓冲区、文件系统缓存。
- 大块连续内存:用大页。比如视频帧缓冲区、DMA缓冲区。
- 实时性要求极高:用内存池。分配时间确定,没有锁竞争。
嗯,最后说一句:没有银弹。每个项目都要根据实际负载来调。我一般会在原型阶段用默认配置,然后用procctrl和top观察内存分配情况。如果发现分配耗时波动大,再考虑换方案。
这一节就到这里。下一节咱们聊聊虚拟内存和MMU的配置,那又是另一番天地了。