4. GEM 内存管理:GEM 对象创建、内存分配与 Buffer Object 管理

GEM(Graphics Execution Manager)是 DRM 框架中负责图形缓冲区管理的核心基础设施。它抽象了 GPU 与 CPU 之间的内存共享、分配与生命周期管理,是驱动开发者必须掌握的第一道关卡。本章将深入 GEM 对象的创建流程、三种主流内存后端(shmem、dma-buf、CMA)的实现差异,以及 Buffer Object(BO)在驱动层的管理策略。

核心认知: GEM 并非一种具体的内存分配器,而是一套 对象句柄(handle)与内存后端解耦 的框架。驱动通过 gem_create_object 回调创建 GEM 对象骨架,再通过 gem_create_memory 或 gem_create_dma_buf 等回调绑定实际物理内存。

4.1 GEM 对象生命周期与创建流程

一个 GEM 对象从用户态 ioctl 请求到内核态物理内存映射,经历以下关键阶段:

  1. 用户态发起:调用 drmIoctl(fd, DRM_IOCTL_MODE_CREATE_DUMB, &create) 或 DRM_IOCTL_GEM_CREATE。
  2. 内核框架层:DRM 核心调用 drm_gem_create_object 分配 struct drm_gem_object 并初始化引用计数、大小、句柄。
  3. 驱动回调:调用 ->gem_create_object 或 ->gem_create_memory 分配驱动私有数据(如 GPU 虚拟地址预留、页表绑定)。
  4. 内存后端绑定:根据配置选择 shmem、dma-buf 或 CMA 分配物理页面。
  5. 句柄注册:通过 drm_gem_handle_create 将对象与用户态 fd 或 handle 关联。
  6. 映射与使用:用户态通过 mmap 或 Prime 导出获取 CPU/GPU 访问权限。
GEM 对象创建流程 GEM 对象创建与内存绑定流程 用户态 ioctl drm_gem_create_object 驱动 gem_create 回调 内存后端分配 shmem (默认) dma-buf (跨设备) CMA (连续内存) 句柄注册 drm_gem_handle_create

4.2 三种内存后端对比

DRM 框架为 GEM 对象提供了三种标准内存后端,驱动可根据硬件特性(是否要求连续物理内存、是否需要跨设备共享)选择最合适的方案:

后端类型 物理连续性 跨设备共享 典型场景 分配函数
shmem 不连续(Scatter-Gather) 不支持(仅本设备) 普通渲染目标、纹理 shmem_read_mapping_page
dma-buf 不连续 / 连续(取决于源) 支持(跨 GPU、V4L2、DRM) 多 GPU 渲染、视频编解码 dma_buf_export
CMA 连续物理内存 有限支持(需额外映射) 显示控制器、硬件合成器 dma_alloc_from_contiguous
选型建议: 对于大多数 GPU 驱动,shmem 是默认且最灵活的选择。CMA 仅在硬件要求连续物理地址(如某些显示控制器直接读取帧缓冲)时使用。dma-buf 是跨设备共享的标准方案,现代驱动应优先支持。

4.3 shmem 后端:默认的页面分配器

shmem(Shared Memory)后端利用 Linux 内核的 tmpfs 机制分配匿名页面。其核心优势在于 按需分配 和 交换支持:

  • 分配时机:调用 drm_gem_create_object 时仅分配 struct drm_gem_object,物理页面在首次访问(page fault)时由 shmem_read_mapping_page 分配。
  • Scatter-Gather 支持:通过 sg_alloc_table_from_pages 将不连续页面组织成 SG 表,供 GPU 页表使用。
  • CPU 映射:通过 drm_gem_get_pages 获取页面数组,再调用 vm_map_ram 或 kmap 建立临时映射。
// 驱动自定义 GEM 对象创建示例(shmem 后端)
static struct drm_gem_object *my_gem_create_object(struct drm_device *dev, size_t size)
{
    struct my_gem_object *my_obj;

    my_obj = kzalloc(sizeof(*my_obj), GFP_KERNEL);
    if (!my_obj)
        return ERR_PTR(-ENOMEM);

    // 初始化 shmem 页面分配器
    my_obj->base.funcs = &my_gem_object_funcs;
    my_obj->base.size = roundup(size, PAGE_SIZE);

    // 驱动私有数据初始化
    my_obj->vma_node = NULL;
    my_obj->sg_table = NULL;

    return &my_obj->base;
}

// 在 fault 处理中分配物理页面
static vm_fault_t my_gem_fault(struct vm_fault *vmf)
{
    struct drm_gem_object *obj = vmf->vma->vm_private_data;
    struct page *page;

    page = shmem_read_mapping_page(file_inode(obj->filp)->i_mapping,
                                   vmf->pgoff);
    if (IS_ERR(page))
        return VM_FAULT_SIGBUS;

    vmf->page = page;
    return 0;
}

4.4 dma-buf 后端:跨设备共享的桥梁

dma-buf 是 Linux 内核中用于 跨设备 DMA 缓冲区共享 的标准机制。在 DRM 中,GEM 对象可以通过 Prime 接口导出为 dma-buf fd,或从 dma-buf fd 导入创建 GEM 对象:

  • 导出流程:drm_gem_prime_export → dma_buf_export → 驱动提供 gem_prime_export_sg_table 回调。
  • 导入流程:drm_gem_prime_import → dma_buf_attach → 驱动提供 gem_prime_import_sg_table 回调。
  • 同步保证:通过 dma_buf_map_attachment 和 dma_buf_unmap_attachment 管理 DMA 一致性。
dma-buf 跨设备共享 dma-buf 跨设备共享架构 GPU A (导出者) GEM Object → dma-buf sg_table 导出 dma-buf 缓冲区 struct dma_buf GPU B (导入者) dma-buf → GEM Object sg_table 导入 关键回调:gem_prime_export_sg_table gem_prime_import_sg_table

4.5 CMA 后端:连续物理内存分配

CMA(Contiguous Memory Allocator)后端用于分配 物理连续的大块内存,主要服务于显示控制器、硬件合成器或某些 DMA 引擎。其特点包括:

  • 预留区域:系统启动时通过 cma_declare_contiguous 或设备树预留连续内存区域。
  • 分配接口:dma_alloc_from_contiguous 从 CMA 区域分配连续页面。
  • 释放接口:dma_release_from_contiguous 归还页面。
  • 限制:分配可能阻塞(migrate 页面),不适合高频分配场景。
// CMA 后端分配示例(简化)
static int my_gem_cma_create(struct drm_device *dev, struct drm_gem_object *obj)
{
    struct my_gem_cma_object *cma_obj = to_my_gem_cma_obj(obj);
    size_t size = obj->size;

    // 从 CMA 区域分配连续物理内存
    cma_obj->vaddr = dma_alloc_wc(dev->dev, size, &cma_obj->paddr,
                                   GFP_KERNEL | __GFP_NOWARN);
    if (!cma_obj->vaddr)
        return -ENOMEM;

    // 建立 SG 表(连续内存只需一个 entry)
    cma_obj->sgt = kzalloc(sizeof(*cma_obj->sgt), GFP_KERNEL);
    sg_alloc_table(cma_obj->sgt, 1, GFP_KERNEL);
    sg_dma_address(cma_obj->sgt->sgl) = cma_obj->paddr;
    sg_set_page(cma_obj->sgt->sgl, phys_to_page(cma_obj->paddr),
                size, 0);

    return 0;
}

4.6 Buffer Object 管理策略

Buffer Object(BO)是驱动层对 GEM 对象的进一步封装,通常包含 GPU 虚拟地址映射、同步 fence、内存域迁移等状态。高效的管理策略包括:

管理维度 策略 关键数据结构
生命周期 引用计数 + 延迟销毁(RCU) kref, drm_gem_object
GPU 地址映射 按需建立页表,支持 sparse binding vm_area_struct, drm_mm
内存域迁移 系统内存 ↔ 显存 ↔ 连续内存 drm_gem_object->domain
同步 dma-fence 链 + 隐式/显式同步 dma_fence, dma_resv
缓存策略 write-combine / uncached / cached pgprot_t, dma_attr
常见陷阱:
  • 忘记在 gem_free_object 中释放 SG 表会导致内存泄漏。
  • dma-buf 导入时未调用 dma_buf_map_attachment 会导致 DMA 地址无效。
  • CMA 分配失败时未回退到 shmem 可能导致驱动加载失败。

4.7 本章小结

GEM 内存管理是 DRM 驱动开发的基石。开发者需要理解:

  • 对象创建 是框架层与驱动层的协作,驱动通过回调注入私有逻辑。
  • 三种后端 各有适用场景,shmem 通用、dma-buf 跨设备、CMA 连续内存。
  • BO 管理 涉及地址映射、同步、缓存策略等多个维度,需要与 GPU 硬件特性紧密结合。