4.1 概述里我们把轻量后备分成了两支:需要物理连续的走drm_gem_dma_object,可换出的走drm_gem_shmem_object。本章从更简单的那一支讲起——drm_gem_dma_object。它是整个第四章里结构最小、流程最短的后备方案,很适合用来先把「一个 GEM 对象的后备存储到底要做哪几件事」这条最小闭环走通,再去理解 shmem 为了「可换出」多付出的那些复杂度。
前置知识:本节会用到内核 DMA API(
dma_alloc_wc/dma_alloc_coherent)与CMA(Contiguous Memory Allocator,连续内存分配器)。只需记住一句话:CMA 是内核预留的一块可动态分配的物理连续内存池,供那些要求连续 DMA 缓冲的设备使用;dma_alloc_*在没有 IOMMU 时底层正是从它取内存。
如果想深入 DMA APIs 的设计请移步:从硅到用户态:DMA 全链路解析。
1、它解决什么:不支持 scatter-gather 的设备要「连续」
shmem 后备交给设备的是一批散落的页(靠sg_table描述)。但有一类设备吃不下散页——它们的 DMA 引擎不支持 scatter-gather,只能对着「一段连续地址」搬数据。内核源码的DOC: dma helpers把适用场景讲得很清楚(drm_gem_dma_helper.c):
- 无 IOMMU 的设备:靠CMA分配物理连续的 buffer;
- 有(紧耦合)IOMMU 的设备:页可以物理分散,但映射进 IOVA 后对设备呈现为连续——所以「连续」的真正含义是「设备视角(
dma_addr)连续」,不一定物理连续。
典型用户是显示控制器(display controller)、简单 SoC 的 framebuffer、v4l2 视频缓冲等——它们扫描输出一帧画面时,需要一段地址连续、属性可控(通常 write-combine)的内存。
2、结构drm_gem_dma_object:四个字段,仅此而已
先看它的全貌,定义在drm_gem_dma_helper.h:
structdrm_gem_dma_object{structdrm_gem_objectbase;// 内嵌的 GEM 基类dma_addr_tdma_addr;// 后备内存的 DMA 地址(设备视角)structsg_table*sgt;// 仅 PRIME 导入时使用(保证连续)void*vaddr;// 后备内存的内核虚拟地址bool map_noncoherent;// 是否使用非一致性内存};#defineto_drm_gem_dma_obj(gem_obj)\container_of(gem_obj,structdrm_gem_dma_object,base)和 shmem 一样,它在头部内嵌drm_gem_object,用to_drm_gem_dma_obj反推。但真正值得注意的是它没有什么:
- 没有
pages[]—— 后备是一整块连续内存,不需要页数组; - 没有
pages_use_count/pages_pin_count/vmap_use_count—— 内存分配即常驻、直到释放,不换入换出、不需要分层引用计数; - 没有
madv/madv_list—— 不参与 shrinker,不可回收; vaddr在分配时就已拿到,不是惰性 vmap 的结果。
一句话:drm_gem_dma_object用「一次分配、钉死到底」换来了结构上的极简。它只需记住三样东西——「内核怎么访问(vaddr)、设备怎么访问(dma_addr)、以及(导入时)它的 sg 描述(sgt)」。
3、创建:drm_gem_dma_create—— 一次性分配,分配即到位
创建入口是drm_gem_dma_create,核心就两步:先建壳,再一把分配连续内存。
structdrm_gem_dma_object*drm_gem_dma_create(structdrm_device*drm,size_tsize){structdrm_gem_dma_object*dma_obj;size=round_up(size,PAGE_SIZE);// ① 对齐到页dma_obj=__drm_gem_dma_create(drm,size,false);// ② 建壳 + 伪 offsetif(IS_ERR(dma_obj))returndma_obj;if(dma_obj->map_noncoherent){dma_obj->vaddr=dma_alloc_noncoherent(...,size,&dma_obj->dma_addr,DMA_TO_DEVICE,GFP_KERNEL|__GFP_NOWARN);}else{dma_obj->vaddr=dma_alloc_wc(...,size,// ③ 默认 write-combine&dma_obj->dma_addr,GFP_KERNEL|__GFP_NOWARN);}...}三个要点,恰好对照 shmem 的「惰性」:
- ② 建壳:
__drm_gem_dma_create负责drm_gem_object_init与drm_gem_create_mmap_offset(伪 offset 的分配,与第三章 3.3 的drm_vma_offset_manager是同一套机制)。它也支持 driver 用gem_create_object插入自己的外层对象——和 shmem 一样的「内嵌可扩展」套路; - ③ 分配即到位:
dma_alloc_wc/dma_alloc_noncoherent一次性拿到连续内存,同时得到vaddr(内核地址)与dma_addr(设备地址)。没有get_pages、没有按需换入——这是与 shmem 最根本的流程差异:shmem 的页要到「第一次用到」才分配,而 DMA-CMA 在 create 时就全部就位、并钉死到释放; - 默认 write-combine:普通路径用
dma_alloc_wc,适配「CPU 顺序写入、设备读取」的 framebuffer 访问模式;需要非一致性语义时才走map_noncoherent分支。
释放是对称的一把回收——drm_gem_dma_free调dma_free_wc/dma_free_noncoherent直接归还,没有「计数归零才释放」的层层退栈。
4、访问:mmap 全量映射、vmap 直接返回(无缺页、无 pin)
这是 DMA-CMA「简」得最明显的地方。
内核态vmap:直接返回缓存地址
drm_gem_dma_vmap整个函数只有一行实质代码:
intdrm_gem_dma_vmap(structdrm_gem_dma_object*dma_obj,structiosys_map*map){iosys_map_set_vaddr(map,dma_obj->vaddr);// 直接给出已有的 vaddrreturn0;}因为dma_alloc_*返回的vaddr本就在内核虚拟地址空间里,内核要访问直接用即可——不像 shmem 还要pin+vmap把散页拼成连续地址。
用户态mmap:一次性映射整个对象
drm_gem_dma_mmap的关键是清掉VM_PFNMAP、一次性把整块 buffer 映射进用户态:
intdrm_gem_dma_mmap(structdrm_gem_dma_object*dma_obj,structvm_area_struct*vma){...vma->vm_pgoff-=drm_vma_node_start(&obj->vma_node);// 伪 offset 归零vm_flags_mod(vma,VM_DONTDUMP|VM_DONTEXPAND,VM_PFNMAP);if(dma_obj->map_noncoherent)ret=dma_mmap_pages(...);elseret=dma_mmap_wc(...,dma_obj->vaddr,dma_obj->dma_addr,vma->vm_end-vma->vm_start);// 整段一次映射...}内核注释一句话点破差异:“immediately faults in the entire object instead of using on-demand faulting”——整个对象在 mmap 时就立即全部映射,而不是按需缺页。这也是为什么它的vm_ops里根本没有.fault(drm_gem_dma_vm_ops只有.open/.close):内存连续且常驻,无需逐页填充。对照 shmem 的drm_gem_shmem_fault(4.3.5 会讲)按需逐页补页,这里的「简」一目了然。
5、回调表与驱动集成:几乎零代码接入
helper 预置了一张默认回调表drm_gem_dma_default_funcs:
staticconststructdrm_gem_object_funcsdrm_gem_dma_default_funcs={.free=drm_gem_dma_object_free,.print_info=drm_gem_dma_object_print_info,.get_sg_table=drm_gem_dma_object_get_sg_table,.vmap=drm_gem_dma_object_vmap,.mmap=drm_gem_dma_object_mmap,.vm_ops=&drm_gem_dma_vm_ops,};__drm_gem_dma_create在 driver 没有自带funcs时会自动挂上这张表(if (!gem_obj->funcs) gem_obj->funcs = &drm_gem_dma_default_funcs;)。再配合drm_driver层的快捷宏DRM_GEM_DMA_DRIVER_OPS,driver 只需一行就能把dumb_create(走drm_gem_dma_dumb_create,给 KMS framebuffer 分配 dumb buffer)和 PRIME 导入接好:
staticconststructdrm_driverxxx_driver={...DRM_GEM_DMA_DRIVER_OPS,// dumb_create + gem_prime_import_sg_table 一把到位};这正呼应 4.1 说的「helper 而非 framework」——复用成本极低。
6、共享:get_sg_table 与 PRIME 导入(必须连续)
要导出成 dma-buf,需要一张sg_table。drm_gem_dma_get_sg_table用dma_get_sgtable从vaddr/dma_addr直接生成——因为内存连续,这张表通常就是一段。
导入方向有个硬约束:drm_gem_dma_prime_import_sg_table会用drm_prime_get_contiguous_size校验导入的 sg_table 是否物理连续(单一连续段),否则直接-EINVAL。这与它的定位一致:DMA-CMA 对象天生只接受连续内存,不像 shmem 那样能容纳任意散页。
7、与 shmem 对比:为什么它是「最简样例」
把两支轻量后备并排看,差异全部源于一句话——DMA-CMA 一次分配、钉死到底,shmem 惰性分配、可换出可 pin:
| 维度 | drm_gem_dma_object(4.2) | drm_gem_shmem_object(4.3~) |
|---|---|---|
| 后备内存 | CMA 物理连续内存 | shmem 可换出匿名页(散) |
| 分配时机 | create 时一次性分配 | 惰性,get_pages首次用时分配 |
| 能否换出/回收 | 否(钉死,不进 shrinker) | 是(madv+ shrinker) |
| 引用计数 | 无(无需分层) | pages_use_count/pin_count/vmap_use_count |
内核vmap | 直接返回已有vaddr | 先 pin +vmap()拼连续地址 |
用户态mmap | 全量一次映射,无.fault | 空映射 + 按需缺页.fault |
sg_table | 通常单段连续 | 多段散页 |
| PRIME 导入 | 要求物理连续 | 接受任意散页 |
| 典型用户 | display controller、无 IOMMU 的 SoC、v4l2 | virtio-gpu、iGPU、SoC GPU |
读到这里应建立的认知是:DMA-CMA 把「对象 + 后备页 + 用户态/内核态访问 + 共享」这条链路做到了最短。理解了它,再看 shmem,你会发现 shmem 的绝大多数额外字段与流程,都是在为一个它不具备的能力买单——可换出。
8、本节小结
drm_gem_dma_object面向不支持 scatter-gather(或经紧耦合 IOMMU)的设备,提供设备视角连续的后备内存,底层无 IOMMU 时由CMA供给;- 结构只有
dma_addr/vaddr/sgt/map_noncoherent四个字段,一次分配、钉死到底,不换出、不 pin、不计数; - 访问路径极短:
vmap直接返回vaddr,mmap全量映射且无缺页回调; - 它是第四章「最简后备样例」,为理解 shmem 的复杂度提供了对照基线。
下一节(4.3):我们转向更通用的 shmem 后备。进入代码之前,先回答一个问题:BO 对后备存储有哪些要求,为什么恰好是 shmem 文件能全部满足。