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 请求到内核态物理内存映射,经历以下关键阶段:
- 用户态发起:调用
drmIoctl(fd, DRM_IOCTL_MODE_CREATE_DUMB, &create)或DRM_IOCTL_GEM_CREATE。 - 内核框架层:DRM 核心调用
drm_gem_create_object分配struct drm_gem_object并初始化引用计数、大小、句柄。 - 驱动回调:调用
->gem_create_object或->gem_create_memory分配驱动私有数据(如 GPU 虚拟地址预留、页表绑定)。 - 内存后端绑定:根据配置选择 shmem、dma-buf 或 CMA 分配物理页面。
- 句柄注册:通过
drm_gem_handle_create将对象与用户态 fd 或 handle 关联。 - 映射与使用:用户态通过 mmap 或 Prime 导出获取 CPU/GPU 访问权限。
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 一致性。
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 硬件特性紧密结合。