4.2 的 DMA-CMA 将分配链路缩至最短,代价是内存一次分配后即固定不变。对于 UMA 设备上数以百计乃至千计的 BO,这种方式代价过高。DRM 提供的通用方案是shmem:每个 BO 以一个 shmem 文件作为后备。
本节按以下三个步骤说明为何选择 shmem 文件:
- 从 BO 的使用方式归纳出对后备存储的需求;
- 分析 GEM框架层将这些需求体现为哪些接口,以及由此对后备提出的具体要求;
- 逐层检验shmem满足了哪些要求,哪些须由 helper 补充。
1、需求:BO 在 UMA 设备上的使用方式
一个 BO 从创建到释放,通常经历以下环节:用户态创建 BO,经mmap映射后填入数据;BO 提交给 GPU,由 GPU 通过 DMA 读写;内核偶尔通过vmap访问它(例如 fbdev 控制台);有时 BO 被导出为 dma-buf,供显示控制器或视频编码器使用;各方均不再使用后,BO 被释放。
这一过程体现出三个特点:
- 持有者多:handle、dma-buf 与正在执行的 GPU 任务都可能引用同一个 BO。创建它的进程退出后,BO 仍可能被其他持有者使用;
- 访问方多:用户态、内核、本设备与其他设备都可能访问同一批页,且可能同时访问;
- 量大且常闲置:BO 体积大(帧缓冲、纹理通常达数 MB),数量多(一个进程可拥有数百乃至上千个),且大部分时间处于闲置状态。在 UMA 设备上,这些内存还与普通应用共享同一份系统内存。
由此得出后备存储须满足的六项需求:
| 需求 | 来源 |
|---|---|
| N1 内容归 BO 所有,仅在最后一个引用消失时释放 | 持有者多 |
| N2 访问有序:CPU、本设备与其他设备之间的访问不得相互冲突 | 访问方多 |
N3 CPU 可访问:用户态mmap、内核vmap | 访问方多 |
| N4 设备可访问,包括导出给其他设备 | 访问方多 |
| N5 设备可正确访问:地址可达、访问期间不被迁移、缓存一致 | 设备直接访问物理内存 |
| N6 闲置时减少内存占用:按需分配、闲置时可换出、不再需要时可丢弃 | 量大且常闲置,且与 CPU 共享内存 |
2、框架层:GEM 中与需求对应的接口
GEM 核心并不规定后备的具体形式,但通过struct drm_gem_object的字段与struct drm_gem_object_funcs的回调对后备提出了要求。按强制程度,这些要求可分为四层:
| 层次 | 对应需求 | GEM 接口 | 对后备的要求 |
|---|---|---|---|
| 必选 | N1、N2 | free(唯一必选的回调,引用计数归零时调用);size(注释写明对象生命期内不可变);resv(vmap、evict等回调要求持锁调用) | 随对象释放;大小固定;状态变更在dma_resv下进行 |
| 按功能 | N3、N4 | mmap/vm_ops;vmap/vunmap;pin/unpin(dma-buf 导入方 attach 时调用);get_sg_table(导出时调用,随后以导入方设备进行 DMA 映射) | 可按页插入用户页表;可组合为连续的内核地址;可被固定;可表示为由struct page组成的 sg 表 |
| 可选回收 | N6 | evict;drm_gem_lru;status中的 RESIDENT / PURGEABLE;rss | 允许部分驻留、可被驱逐、可被 purge |
| 框架不涉及 | N5 | 无对应接口,仅在dma_map_sgtable时交由 DMA API 处理 | 由驱动与 DMA API 负责 |
需要说明以下几点:
- 功能层隐含了「后备由普通内存页组成」这一前提。
get_sg_table的注释指出,这套导出 helper 不能用于指向驱动私有内存区域的 sg 表。因此,若要使用通用的共享路径,后备必须是具有struct page的普通内存; - 框架仅强制要求必选层。回收层仅提供钩子:
rss可以小于size,表明框架允许后备仅部分驻留在内存中,但并不强制。然而对 UMA 设备而言,N6 恰恰最为重要,因为 BO 与普通应用竞争同一份内存; - 框架具有默认后备。
drm_gem_object中filp字段的注释为 “SHMEM file node used as backing storage for swappable buffer objects”;内核文档drm-mm.rst亦写道 “GEM uses shmem to allocate anonymous pageable memory”。这与 GEM 的起源有关:GEM 最初为没有独立显存的 Intel 集成显卡设计,BO 即以 shmem 文件作为后备,i915 至今仍以这种方式处理集成显卡上的 BO(i915_gem_shmem.c),创建 shmem 文件这一步也因此位于 GEM 核心而非 helper 中(3.2.2)。不使用 shmem 的对象称为私有对象,由drm_gem_private_object_init()初始化,其存储由驱动自行管理。
3、shmem 是否满足:逐层检验
3.1 必选层:满足
shmem 文件在drm_gem_object_init()中按对象大小创建,在drm_gem_object_release()中随对象一起经fput释放。其大小在创建时确定,生命期与对象一致,与创建该对象的进程无关。持锁约定由 helper 遵守(见 4.3.2)。
3.2 功能层:所需条件均已具备
shmem 文件的页即普通的 page cache 页(文件内容在内存中的页,由文件的address_space按偏移索引),每页均有对应的struct page。将这些页取出并组织为pages[]后:
mmap发生缺页时,将对应的页插入用户页表;vmap通过内核的vmap()将分散的页映射为一段连续地址;get_sg_table通过drm_prime_pages_to_sg()生成 sg 表。
仅pin的语义须由 helper 自行实现:维护一个 pin 计数,保证计数大于 0 时页不会被驱动 purge(见 4.3.3)。
3.3 回收层:天然满足
这是 shmem 最核心的优势,其关键在于:内容保存在 shmem 文件中,页数组仅是对文件页的引用。
shmem BO 的内容归属于其后备的shmem 文件,而非pages[]数组:
- 获取页:helper 需要页时,按偏移从文件中获取。若页尚不存在则即时分配,若已被换出则即时从 swap 换回,此即按需分配;
- 归还页:helper 使用完毕后仅释放对页的引用,页仍保留在文件的 page cache 中。此后内存紧张时,内核可像处理普通 tmpfs 文件一样将其换出至 swap,下次获取页时再换回。由于内容始终归属于文件,因此不会丢失;
- 丢弃:将文件截断为空即可,页与 swap 槽位随之一并释放,此即 purge(见 4.3.6)。
由此可见,shmem 文件将「内容保存」与「内存回收」交由内核已有的机制完成,helper 仅需负责获取页与归还页。
3.4 框架不涉及的设备层:需要修补
shmem 为普通文件而设计,不考虑设备访问,因此不满足 N5 的各项要求。helper 中最关键的几段代码均用于修补这一层:
| 设备层要求 | shmem 的情况 | helper 的修补 | 详见 |
|---|---|---|---|
| 访问期间不被迁移 | 文件的页默认从 MOVABLE 区分配,可能被迁移;在该区域长期 pin 还会妨碍内存规整,并与 CMA 冲突 | 创建时将文件的 GFP 掩码改为GFP_HIGHUSER,不含__GFP_MOVABLE | 4.3.1 |
| 访问期间不被回收 | 内核可能随时回收文件的页 | 持有页期间将文件标记为 unevictable,内核不再回收;本设备访问期间由驱动保证不进行 purge;内核 vmap 及导出给其他设备时另行 pin | 4.3.2 / 4.3.3 |
| 地址可达、不要求连续 | 页在物理上分散,位置亦不受控制 | 将页数组转换为sg_table,经dma_map_sgtable映射,由 IOMMU 或 swiotlb 处理寻址 | 4.3.5 |
| 缓存一致 | page cache 页默认按 cached 方式映射 | 驱动设置map_wc后,helper 在 x86 上将页属性改为 WC,vmap 与 mmap 亦以 write-combined 方式映射 | 4.3.2 / 4.3.4 |
4、对照:其他候选方案的不足所在
将其他几种内存来源置于同一框架下比较:
| 候选 | 必选层 | 功能层 | 回收层 | 设备层 |
|---|---|---|---|---|
dma_alloc/ CMA(4.2) | ✓ | ✓ | ✗ 一次分配后固定不变 | ✓ 连续且常驻,一次性满足 |
驱动自行alloc_pages | ✓ | ✓ | ✗ 内核无法换出,只能自行实现 | 需自行处理 |
| 用户态匿名内存(userptr) | ✗ 内存归进程所有 | ✓ | BO 无法控制 | 需先 pin |
| shmem 文件 | ✓ | ✓ | ✓ 天然满足 | 需 helper 修补 |
关键差异在于回收层:
- 设备层的不足可以通过一层通用代码弥补,第 3.4 节所述即为 helper 的工作;
- 回收层的能力则难以弥补。驱动自行分配的页不属于任何文件或进程映射,内核的换出机制无法处理这些页,若要换出只能自行将内容拷贝至他处。TTM 正是采用这种方式:
ttm_tt_swapout()先创建一个 shmem 文件,将页内容拷贝进去,再由内核换出该文件(见 5.10)。因此,即便是 TTM,换出时最终也须借助 shmem 文件。
CMA 恰好相反:设备层一次性满足,回收层则完全放弃,因此适用于必须使用物理连续内存的设备(见 4.1 第 4 节)。
5、从 shmem 到 helper
在 helper 出现之前,每个 UMA 驱动都须自行编写几乎相同的代码:
- 创建 shmem 文件,按需获取页;
- 供设备使用时 pin 页并构建
sg_table; - 实现用户态
mmap与缺页处理; - 实现
vmap供内核访问; - 释放时归还页。
这些逻辑高度雷同且容易出错,各驱动又各自实现一套。drm_gem_shmem_helper(2018 年引入)将其抽象为一层公共代码:第 3 节中功能层的操作与设备层的修补均由它统一完成。其定位有两点:
- 是 helper,而非 framework:它是一组可选的公共函数。驱动只需内嵌
drm_gem_shmem_object,并将回调指向 helper 的实现,即可直接复用(见 4.3.7); - 仅实现 UMA 所需的功能:分配页、pin、
sg_table、vmap、mmap、缺页、回收、释放。放置、迁移、VRAM 驱逐均不在其范围内,这正是它与 TTM 的分工界线。
6、shmem 系列路线
第 3.3 节指出,BO 的内容保存在 shmem 文件中,helper 持有的仅是对文件页的引用。后续七篇即围绕「一份内容,多份引用」这一主线展开,依次回答四个问题:页从哪里来、由谁使用、何时能够收回、驱动如何接入。
| 问题 | 小节 | 内容 | 对应需求 |
|---|---|---|---|
| 页从哪里来 | 4.3.1 对象与创建 | struct drm_gem_shmem_object的字段,以及drm_gem_shmem_create如何建立 shmem 文件、分配伪偏移、设定 GFP 掩码;对象创建完成时只有文件,尚未持有任何页 | N1、N5 |
| 4.3.2 取页与还页 | get_pages/put_pages从文件获取页与归还页,以及所有状态变更共用的dma_resv锁;后续所有访问路径均建立在这一对操作之上 | N2、N5、N6 | |
| 由谁使用 | 4.3.3 引用的强弱 | use 与 pin 两种强度的引用、三层计数的持有者,以及由此决定的四种对象状态 | N2、N5、N6 |
| 4.3.4 CPU 访问 | vmap与mmap两条 CPU 访问路径,以及缺页处理 | N3 | |
| 4.3.5 设备访问 | 本设备经get_pages_sgt进行 DMA,其他设备经 PRIME / dma-buf 导出与导入共享 | N4、N5 | |
| 何时能够收回 | 4.3.6 回收 | 换出至 swap 与驱动 purge 两条回收路径,以及 shrinker 的接入方式 | N6 |
| 驱动如何接入 | 4.3.7 驱动集成 | 驱动如何内嵌对象、填写回调表并使用gem_create_object(以 virtio-gpu 为例) | — |
7、本节小结
- 需求:BO 持有者多、访问方多、量大且常闲置,由此得出 N1~N6 六项需求;
- 框架层:GEM 将需求体现为必选、按功能、可选回收三层接口,设备层则不在框架范围内;
- shmem:满足必选层与功能层,并天然满足回收层,因为内容保存在文件中,
pages[]仅是引用;设备层由 helper 修补; - 对照:设备层的不足可由通用代码弥补,回收层则难以弥补,即便 TTM 换出也须借助 shmem 文件。
下一节(4.3.1):阐明选择依据之后,下一节介绍 helper 的核心数据结构struct drm_gem_shmem_object及其创建过程。