4、QNX内存管理:内存映射、共享内存、内存池与泄漏检测

内存管理这个话题,说大不大,说小不小。在QNX这种硬实时系统里,搞不好就是系统崩溃的根源。我见过太多人,上来就malloc,用完了也不free,结果系统跑着跑着就挂了。今天咱们就把这块彻底讲透。

4.1 内存映射(mmap)—— 直接操作物理内存

内存映射,说白了就是把一个文件或者设备,直接映射到进程的地址空间里。你想想看,这样操作起来就跟操作普通内存一样,多方便。

我在项目中遇到过这样一个场景:需要采集高速传感器的数据,如果用read/write,每次都要经过内核,延迟受不了。后来用了mmap,直接把设备内存映射过来,读写速度提升了一个数量级。

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

int fd = shm_open("/myshm", O_RDWR | O_CREAT, 0666);
ftruncate(fd, 4096);

void *addr = mmap(NULL, 4096, 
                  PROT_READ | PROT_WRITE,
                  MAP_SHARED, fd, 0);

// 现在可以直接读写 addr 指向的内存了
// 比如:*(int *)addr = 42;

munmap(addr, 4096);
close(fd);
我的小技巧: 映射大块内存时,记得用MAP_POPULATE标志。这样内核会提前把物理页准备好,避免运行时缺页中断。实时系统最怕的就是这种不确定的延迟。

4.2 共享内存(shm)—— 进程间通信的利器

共享内存是QNX里效率最高的IPC方式。为什么?因为数据不需要拷贝,直接就在那了。你写我读,零拷贝。

我记得有一次做多进程数据采集系统,一开始用消息队列,CPU占用率居高不下。换成共享内存后,占用率直接降了60%。

// 进程A:创建共享内存
int shm_id = shm_open("/data_shm", 
                      O_CREAT | O_RDWR, 0666);
ftruncate(shm_id, 1024 * 1024); // 1MB

void *shm_ptr = mmap(NULL, 1024 * 1024,
                     PROT_READ | PROT_WRITE,
                     MAP_SHARED, shm_id, 0);

// 进程B:连接同一块共享内存
int shm_id = shm_open("/data_shm", O_RDWR, 0666);
void *shm_ptr = mmap(NULL, 1024 * 1024,
                     PROT_READ | PROT_WRITE,
                     MAP_SHARED, shm_id, 0);
注意: 共享内存本身不提供同步机制。多个进程同时读写时,必须配合互斥锁或信号量使用。我曾经就因为这个踩过坑,两个进程同时写,数据全乱了。

4.3 内存池(pool)—— 告别碎片化

频繁的malloc/free会导致内存碎片,这在嵌入式系统里是致命的。内存池就是提前申请一大块内存,然后自己管理分配和释放。

我习惯在系统启动时就创建好内存池,运行时只从池里取。这样既避免了碎片,分配速度也快得多。

#include <pool.h>

// 创建一个内存池,每个块64字节,共100个块
pool_t *my_pool = pool_create(64, 100, 
                              POOL_FLAG_NOWAIT);

// 从池中分配
void *block = pool_alloc(my_pool);

// 使用完后归还
pool_free(my_pool, block);

// 销毁内存池
pool_destroy(my_pool);
分配方式 速度 碎片风险 适用场景
malloc/free 通用场景
内存池 固定大小对象
mmap 中等 大块内存
经验之谈: 内存池的块大小要选好。太小了浪费管理开销,太大了内部碎片多。我一般取对象大小的1.2倍,留点余量。

4.4 内存泄漏检测 —— 别让内存悄悄溜走

内存泄漏是嵌入式开发的头号杀手。系统跑着跑着就OOM了,重启又好了,这种问题最难查。

QNX提供了几个好用的工具。我最常用的是memcheckmcheck

// 编译时加上 -lmalloc_g 选项
// 运行时设置环境变量
export MALLOC_DEBUG=1

// 程序退出时,会打印泄漏信息
// 或者用 mcheck 函数
#include <mcheck.h>

int main() {
    mtrace();  // 开始跟踪
    
    char *p = malloc(100);
    // 忘记 free(p)
    
    muntrace();  // 结束跟踪
    // 输出:Leaked memory: 100 bytes
}
我的调试习惯: 在开发阶段,我会在main函数开头调用mtrace(),结尾调用muntrace()。这样每次程序退出都能看到有没有泄漏。配合valgrind使用效果更好,不过QNX上valgrind支持有限。

还有一个土办法,我自己经常用。在关键位置打印malloc和free的计数:

static int malloc_count = 0;
static int free_count = 0;

void *my_malloc(size_t size) {
    malloc_count++;
    return malloc(size);
}

void my_free(void *ptr) {
    if (ptr) {
        free_count++;
        free(ptr);
    }
}

// 定时检查
if (malloc_count != free_count) {
    printf("Warning: %d bytes leaked!\n", 
           (malloc_count - free_count) * avg_size);
}
避坑指南: 我曾经在中断服务程序里用了malloc,结果系统死锁了。记住,中断上下文里绝对不能用动态内存分配。要么用内存池,要么提前分配好。

嗯,内存管理这块就这些。说白了就是:能用静态就别用动态,用动态就要管好释放。工具只是辅助,关键还是养成良好的编程习惯。下一章咱们聊聊中断处理和时钟管理,那又是另一番天地了。