1. QNX显示架构概览:理解QNX图形子系统、显示驱动在微内核中的位置、图形内存管理基础

好,我们直接进入正题。QNX的显示架构,说实话,跟Linux那套差别挺大的。我第一次从Linux转过来做QNX驱动时,就被它的微内核设计给"教育"了一顿。你想想看,在Linux里,显示驱动是内核的一部分,权限大得很。但在QNX里,事情完全不一样。

1.1 QNX图形子系统:谁管谁?

QNX的图形子系统,说白了就是一套分层架构。从上到下,大概是这么个关系:

  • 应用层:你的Qt、GTK或者直接调Screen API的程序
  • Screen图形框架:QNX自家的合成器,管理窗口、图层
  • 显示驱动:我们这节课的主角,跟硬件打交道
  • 硬件层:GPU、Display Controller、Framebuffer

我个人习惯把Screen框架理解成一个"中间人"。应用说"我要画个按钮",Screen框架说"好,我记下了",然后它再告诉驱动"兄弟,该刷新屏幕了"。驱动不直接跟应用对话,这是QNX的设计哲学——各司其职。

关键点:在QNX里,显示驱动是一个独立的进程,不是内核的一部分。它通过QNX的消息传递机制跟Screen框架通信。这意味着驱动崩溃了,内核不会挂,系统还能抢救一下。

1.2 显示驱动在微内核中的位置

嗯,这里要注意。QNX的微内核只做三件事:线程调度、进程间通信(IPC)、中断处理。别的?统统踢出内核。显示驱动自然也不例外。

我曾经在一个项目中,团队里有人问:"驱动跑在用户态,性能会不会有问题?" 说实话,早期确实有这个问题。但QNX通过共享内存直接中断绑定解决了大部分性能瓶颈。驱动进程可以直接映射硬件寄存器,中断来了也能直接响应,绕过了内核的层层转发。

驱动在系统中的位置,我用一张表来说明:

层级 组件 运行模式 权限
内核空间 微内核 + 资源管理器 特权模式 完全硬件访问
用户空间 Screen框架 用户模式 通过IPC访问驱动
用户空间 显示驱动 (io-display) 用户模式 映射的硬件寄存器
用户空间 应用程序 用户模式 仅Screen API

你看,驱动虽然跑在用户态,但它有"特权通道"——通过mmap直接映射了硬件地址。这就像给了一个保安队长配了把钥匙,他能进机房,但不能随便改安保系统。

1.3 图形内存管理基础

图形内存管理,说白了就是"谁用哪块显存,怎么用,用完怎么还"。QNX的做法跟Linux的DRM/GEM不太一样,它更简单粗暴一些。

在QNX里,图形内存管理主要靠物理内存分配器(PMA)。驱动启动时,会从系统申请一大块连续的物理内存,专门给图形用。这块内存不能被别的进程随便碰。

避坑指南:我曾经在一个项目里,因为没配好PMA的内存大小,导致系统跑一会儿就花屏。后来发现是显存被占满了,新的buffer分配不出来。记住,PMA的大小要根据你的分辨率和图层数来算。比如1080p@60fps,双缓冲,再加一个UI图层,至少得预留:

1920 * 1080 * 4 (RGBA) * 2 (双缓冲) * 2 (主层+UI层) ≈ 32 MB

这还只是保守估计。要是再加个视频播放层,翻倍都不够。

图形内存的分配流程,我简化一下:

  1. 驱动初始化时,调用pma_alloc()申请一大块连续内存
  2. Screen框架需要buffer时,通过驱动接口从PMA里切一块出来
  3. 驱动返回一个phys_addr(物理地址)给Screen框架
  4. Screen框架把这个地址映射到应用进程的虚拟空间
  5. 应用画完了,通知Screen框架,框架再告诉驱动"这块可以回收了"

这里有个细节:物理地址是关键。因为DMA(直接内存访问)只认物理地址,不认虚拟地址。你给GPU传虚拟地址,它直接罢工给你看。

注意:QNX的图形内存是非缓存(non-cacheable)的。为什么?因为CPU和GPU可能同时访问同一块内存,如果开了缓存,CPU写完了数据还在cache里,GPU读到的就是旧数据。画面就会出现撕裂或者闪烁。我见过有人为了性能开了cache,结果调试了三天才发现是缓存一致性问题。

1.4 驱动与Screen框架的交互模型

驱动跟Screen框架怎么说话?靠的是QNX的资源管理器框架。驱动注册为一个路径,比如/dev/io-display。Screen框架通过标准的open()ioctl()mmap()来跟驱动交互。

举个例子,Screen框架要创建一个显示buffer:

// 伪代码,实际流程更复杂
int fd = open("/dev/io-display", O_RDWR);
// 告诉驱动:我要一个1920x1080的buffer
struct display_buffer_req req = {
    .width = 1920,
    .height = 1080,
    .format = DISPLAY_FORMAT_RGBA8888
};
ioctl(fd, DISPLAY_IOCTL_ALLOC_BUFFER, &req);
// 驱动返回了buffer的物理地址和大小
// Screen框架把它mmap到自己的地址空间
void *buffer = mmap(NULL, req.size, PROT_READ|PROT_WRITE, 
                    MAP_SHARED, fd, req.phys_addr);
// 然后Screen框架再把这个地址传给应用

你看,整个过程没有一次陷入内核(除了open和ioctl那一下)。数据传递全靠共享内存,效率很高。

1.5 我的一点经验总结

做QNX显示驱动,最核心的就是理解三件事:

  • 谁拥有内存:PMA分配的内存,驱动是管家,Screen框架是用户
  • 谁控制显示:驱动控制硬件时序和寄存器,Screen框架控制图层合成
  • 谁处理中断:驱动处理VSync和DMA完成中断,然后通知Screen框架

我曾经犯过一个低级错误:在中断处理函数里直接调了Screen框架的回调函数。结果死锁了,整个显示系统卡死。后来才意识到,QNX里中断上下文不能做IPC调用,得用pulse消息通知另一个线程去处理。

嗯,这些坑后面章节会细讲。这一章先让你对整个架构有个宏观认识。记住一句话:QNX的显示驱动,本质是一个管理显存和控制硬件的用户态进程。理解了这一点,后面的内容就好办了。