4. QNX内存管理:进程地址空间、共享内存、内存映射、内存池与物理内存访问

内存管理这个话题,在QNX里其实比Linux要「赤裸」得多。我刚开始从Linux转过来做QNX开发时,第一反应是:怎么连个像样的malloc调试工具都没有?后来才发现,QNX的内存哲学是——给你足够的控制权,但你要为自己的行为负责。

这一章,我们就把QNX内存管理的几个核心模块掰开揉碎。你想想看,工业控制器里跑的是实时任务,内存分配如果不可预测,那后果可是灾难性的。

4.1 进程地址空间:每个进程的「私有领地」

QNX是微内核架构,每个进程都有自己的地址空间。这一点和Linux类似,但实现细节上差异很大。

在QNX里,进程地址空间被划分为几个区域:

  • 代码段(text):只读,存放指令
  • 数据段(data):全局变量、静态变量
  • 堆(heap):动态分配的内存
  • 栈(stack):函数调用、局部变量
  • 共享库区域:加载的.so文件
  • 内存映射区域:mmap映射的文件或设备

你可以用pidin命令查看进程的内存布局。我个人习惯在调试时先跑一下:

pidin -p my_controller -m

这会显示进程的完整内存映射。我曾经遇到一个诡异的内存泄漏问题,就是靠这个命令定位到某个共享库占用了大量地址空间。

小技巧:QNX的pidin比Linux的/proc/pid/maps更直观。它会把每个区域的权限(rwx)和物理地址都列出来,调试时非常有用。

4.2 共享内存(shm_open):进程间通信的「高速公路」

共享内存是QNX里最高效的IPC方式。没有消息传递的开销,没有管道的数据拷贝,就是一块内存,多个进程直接读写。

基本流程是这样的:

  1. shm_open创建或打开一个共享内存对象
  2. ftruncate设置大小
  3. mmap映射到进程地址空间
  4. 读写这块内存
  5. munmapshm_unlink清理

来看一个实际例子。这是我在一个运动控制项目中用过的代码片段:

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

#define SHM_NAME "/axis_data"
#define SHM_SIZE 4096

int create_shared_memory() {
    int fd = shm_open(SHM_NAME, O_CREAT | O_RDWR, 0666);
    if (fd == -1) {
        perror("shm_open failed");
        return -1;
    }

    if (ftruncate(fd, SHM_SIZE) == -1) {
        perror("ftruncate failed");
        close(fd);
        return -1;
    }

    void *addr = mmap(NULL, SHM_SIZE, 
                      PROT_READ | PROT_WRITE, 
                      MAP_SHARED, fd, 0);
    if (addr == MAP_FAILED) {
        perror("mmap failed");
        close(fd);
        return -1;
    }

    close(fd);  // 映射后可以关闭文件描述符
    return 0;
}
注意shm_open创建的对象默认在/dev/shmem/下。如果你不调用shm_unlink,进程退出后这个对象会一直存在。我曾经因为这个原因,在系统重启后发现共享内存还在,导致新进程无法创建同名对象——嗯,血的教训。

4.3 内存映射(mmap):不只是共享内存

mmap的用途远不止共享内存。在QNX里,它还可以:

  • 映射文件到内存(文件I/O加速)
  • 映射物理地址(直接访问硬件寄存器)
  • 映射设备内存(如DMA缓冲区)
  • 创建匿名映射(类似malloc但更底层)

我特别喜欢用mmap来映射配置文件。你想想看,一个工业控制器启动时要加载几百个参数,如果用fread逐行读取,那效率太低了。用mmap直接把文件映射到内存,然后像访问数组一样读取,速度能快一个数量级。

int fd = open("/etc/controller.cfg", O_RDONLY);
if (fd == -1) error();

struct stat st;
fstat(fd, &st);

char *config = mmap(NULL, st.st_size, 
                    PROT_READ, MAP_PRIVATE, fd, 0);
if (config == MAP_FAILED) error();

close(fd);

// 现在可以直接用 config[offset] 访问文件内容
// 处理完后
munmap(config, st.st_size);

关键点MAP_PRIVATE会创建写时拷贝的副本,适合只读场景。MAP_SHARED的修改会写回文件,适合需要持久化的数据。

4.4 内存池管理:告别碎片化

实时系统最怕什么?内存碎片。你想想看,一个控制器跑了几个月,堆里全是小碎片,突然要分配一个大缓冲区——malloc返回NULL,系统直接崩溃。

我的做法是:预分配,固定大小,绝不动态释放

QNX没有自带的内存池库,但实现一个并不复杂。这里给出一个我常用的简化版:

typedef struct {
    void *pool;          // 预分配的内存块
    size_t block_size;   // 每个块的大小
    int num_blocks;      // 块数量
    int *free_list;      // 空闲块索引
    int free_count;      // 空闲块数量
} mem_pool_t;

mem_pool_t* pool_create(size_t block_size, int num_blocks) {
    mem_pool_t *pool = malloc(sizeof(mem_pool_t));
    pool->block_size = block_size;
    pool->num_blocks = num_blocks;
    pool->pool = malloc(block_size * num_blocks);
    pool->free_list = malloc(num_blocks * sizeof(int));
    
    // 初始化空闲列表
    for (int i = 0; i < num_blocks; i++) {
        pool->free_list[i] = i;
    }
    pool->free_count = num_blocks;
    return pool;
}

void* pool_alloc(mem_pool_t *pool) {
    if (pool->free_count == 0) return NULL;
    int idx = pool->free_list[--pool->free_count];
    return (void*)((char*)pool->pool + idx * pool->block_size);
}

void pool_free(mem_pool_t *pool, void *ptr) {
    int idx = ((char*)ptr - (char*)pool->pool) / pool->block_size;
    pool->free_list[pool->free_count++] = idx;
}
我的经验:在运动控制项目中,我为不同大小的数据结构创建了多个内存池。比如轴状态用64字节池,轨迹点用256字节池。这样分配时间固定为O(1),而且永远不会产生碎片。

4.5 物理内存访问:直接操作硬件

工业控制器免不了要操作硬件寄存器。在QNX里,用户态进程不能直接访问物理地址,必须通过mmap映射/dev/mem设备。

这是访问GPIO寄存器的典型做法:

#include <fcntl.h>
#include <sys/mman.h>
#include <hw/inout.h>  // QNX硬件访问头文件

#define GPIO_BASE 0x48000000  // 假设的GPIO基地址
#define MAP_SIZE  0x1000

int access_gpio() {
    int fd = open("/dev/mem", O_RDWR);
    if (fd == -1) {
        perror("open /dev/mem failed");
        return -1;
    }

    volatile uint32_t *gpio = mmap(NULL, MAP_SIZE,
                                   PROT_READ | PROT_WRITE,
                                   MAP_SHARED | MAP_PHYS,
                                   fd, GPIO_BASE);
    if (gpio == MAP_FAILED) {
        perror("mmap failed");
        close(fd);
        return -1;
    }

    // 现在可以直接读写寄存器
    gpio[0] = 0x01;  // 设置GPIO输出高电平
    uint32_t val = gpio[1];  // 读取输入状态

    munmap((void*)gpio, MAP_SIZE);
    close(fd);
    return 0;
}
重要警告MAP_PHYS标志告诉QNX这是物理地址映射。没有这个标志,mmap会认为你给的是文件偏移。另外,访问/dev/mem需要root权限。我在调试一个电机驱动时,就因为忘了加MAP_PHYS,写寄存器写到了错误的地方,电机直接飞车——还好有急停按钮。

4.6 避坑指南:我踩过的那些坑

做QNX内存管理这些年,我总结了几条铁律:

  • 不要依赖malloc的实时性malloc可能触发缺页中断,导致延迟不可预测。实时任务里用内存池。
  • 共享内存要加同步:多个进程同时写同一块共享内存,数据会乱掉。用pthread_mutex或QNX的Sync原语保护。
  • mmap后检查返回值MAP_FAILED不是NULL,是(void*)-1。我见过新手用if(!addr)检查,结果漏掉了错误。
  • 物理内存访问要加volatile:编译器会优化掉看似无用的寄存器读写,volatile能防止这种优化。
  • 记得unlink共享内存:进程崩溃后,shm_unlink可能没执行。可以在初始化时先shm_unlinkshm_open,确保干净启动。

嗯,这一章的内容就到这里。内存管理是实时系统的基石,搞懂了这些,你写出来的代码才能稳定跑上几年不重启。下一章我们聊聊中断处理——那又是另一个精彩的话题。