4. GEM (Graphics Execution Manager) 内存管理

GEM(Graphics Execution Manager)是 DRM 框架中负责图形缓冲区管理的核心子系统。它提供了一套统一的 API,用于在用户空间(如 Mesa、Wayland)和内核空间之间高效地分配、共享和同步显存。本章将深入剖析 GEM 对象的生命周期、Buffer 对象的管理机制以及 DMA-BUF 在跨设备显存共享中的关键作用。

4.1 GEM 对象创建与销毁

GEM 对象是 DRM 驱动中表示一块连续物理或虚拟显存的基本单位。每个 GEM 对象在内核中由一个 struct drm_gem_object 结构体描述,并通过一个唯一的句柄(Handle)暴露给用户空间。

4.1.1 核心数据结构

成员 类型 描述
size size_t 对象大小(字节)
handle_count u32 用户空间句柄引用计数
dev struct drm_device * 所属 DRM 设备
filp struct drm_file * 创建该对象的文件句柄上下文
dma_buf struct dma_buf * 关联的 DMA-BUF 导出缓冲区

4.1.2 创建流程

用户空间通过 DRM_IOCTL_GEM_CREATE 请求创建 GEM 对象。内核驱动会执行以下步骤:

  1. 参数校验:检查请求的大小是否合法(非零且不超过设备限制)。
  2. 分配物理内存:调用驱动实现的 gem_create_object 回调,通常使用 dma_alloc_attrs() 或连续内存分配器(CMA)。
  3. 初始化对象:填充 drm_gem_object 结构体,设置 size、dev 等字段。
  4. 生成句柄:通过 drm_gem_handle_create() 为用户空间分配一个唯一的整数句柄。
  5. 返回句柄:将句柄通过 ioctl 返回值传递给用户空间。
关键点:GEM 句柄是进程私有的。同一个物理 GEM 对象在不同进程中可能拥有不同的句柄值,内核通过 idr 机制维护句柄到对象的映射。

4.1.3 销毁流程

当用户空间调用 DRM_IOCTL_GEM_CLOSE 或进程退出时,内核会减少句柄引用计数。当 handle_count 降为零时,触发 GEM 对象的释放:

  • 调用 drm_gem_object_release() 释放内核资源。
  • 调用驱动实现的 gem_free_object 回调,归还物理内存。
  • 如果对象已导出为 DMA-BUF,则需等待所有外部引用释放。
注意:GEM 对象的生命周期由引用计数管理。用户空间必须确保在不再使用缓冲区后及时关闭句柄,否则会导致显存泄漏。

4.2 Buffer 对象管理

Buffer 对象(BO)是 GEM 对象在驱动层面的具体实现,通常包含物理内存地址、大小、缓存模式、同步对象等附加信息。驱动需要管理 BO 的分配、映射、缓存一致性以及同步。

4.2.1 BO 管理核心操作

操作 描述 关键函数/回调
分配 从系统内存或 VRAM 中分配物理页 drm_gem_cma_create()
映射 将物理内存映射到用户空间或内核虚拟地址 drm_gem_cma_vmap()
缓存同步 确保 CPU 与 GPU 之间的缓存一致性 dma_sync_single_for_device()
绑定 将 BO 绑定到 GPU 的页表或显示控制器地址空间 驱动自定义 bind() 回调

4.2.2 缓存模式与一致性

嵌入式系统中,CPU 和 GPU 通常共享同一片物理内存(如 CMA 区域)。为避免缓存不一致导致的数据错误,驱动必须根据 BO 的使用场景选择合适的缓存模式:

  • 非缓存(Non-cached):适用于帧缓冲区,CPU 写入后 GPU 立即可见,但性能较低。
  • 写合并(Write-combine):适用于 GPU 读取、CPU 写入的场景(如纹理上传),性能优于非缓存。
  • 回写(Write-back):适用于 CPU 频繁读写的临时缓冲区,需要显式缓存刷新。
实践建议:对于显示帧缓冲区,推荐使用写合并模式。在 ARM 平台上,可通过 pgprot_writecombine() 设置页表属性。

4.3 DMA-BUF 与显存共享

DMA-BUF 是 Linux 内核提供的一种跨设备、跨驱动共享 DMA 缓冲区的机制。在 DRM 框架中,GEM 对象可以通过 DMA-BUF 导出,使得其他设备(如 VPU、ISP、显示控制器)能够直接访问同一块物理内存,避免不必要的数据拷贝。

4.3.1 DMA-BUF 生命周期

  1. 导出(Export):拥有 GEM 对象的驱动调用 dma_buf_export(),创建一个 struct dma_buf,并关联到 GEM 对象。
  2. 获取文件描述符:通过 DRM_IOCTL_GEM_DMA_BUF 或 dma_buf_fd() 获取一个文件描述符,传递给其他进程或驱动。
  3. 导入(Import):接收方驱动通过文件描述符调用 dma_buf_get() 获取 struct dma_buf,并映射到自己的地址空间。
  4. 同步与访问:使用 dma_buf_begin_cpu_access() 和 dma_buf_end_cpu_access() 管理访问同步。
  5. 释放:所有引用计数归零后,DMA-BUF 自动销毁,底层 GEM 对象随之释放。

4.3.2 DMA-BUF 共享架构

DMA-BUF 跨设备显存共享架构 用户空间进程 A (GPU 驱动) 用户空间进程 B (显示驱动) GEM 对象 (物理显存) DMA-BUF (共享描述符) 物理内存 (CMA / VRAM) 连续物理页帧 创建/导出 导入 关联 映射 分配 说明: DMA-BUF 通过文件描述符传递,实现零拷贝共享。进程 A 导出 GEM 对象,进程 B 导入后可直接访问同一块物理内存。

4.3.3 驱动实现要点

在嵌入式 LCD 控制器驱动中实现 DMA-BUF 共享时,需要关注以下回调:

// 导出回调:将 GEM 对象包装为 DMA-BUF
struct dma_buf *(*gem_export_object)(struct drm_gem_object *obj, int flags);

// 导入回调:从 DMA-BUF 创建 GEM 对象
struct drm_gem_object *(*gem_import_object)(struct drm_device *dev,
                                            struct dma_buf *dma_buf);

// 驱动示例:使用 dma_buf_export 创建共享缓冲区
static struct dma_buf *my_gem_export(struct drm_gem_object *obj, int flags)
{
    struct dma_buf_export_info exp_info = {
        .exp_name = "my_drm_driver",
        .owner = THIS_MODULE,
        .ops = &my_dma_buf_ops,
        .size = obj->size,
        .flags = flags,
        .priv = obj,
    };
    return dma_buf_export(&exp_info);
}
性能优势:在典型的嵌入式多媒体场景中,使用 DMA-BUF 共享显存可以减少 2~3 次内存拷贝,帧率提升可达 30% 以上。

4.4 本章小结

GEM 内存管理是 DRM 框架的基石,它通过句柄机制隔离用户空间与内核空间的地址空间,通过 DMA-BUF 实现跨设备零拷贝共享。理解 GEM 对象的创建与销毁、Buffer 对象的缓存管理以及 DMA-BUF 的导入导出流程,是开发高效、稳定的嵌入式显示驱动的关键。