ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

4.3 为什么选择 shmem:从 BO 需求到 GEM 框架,再到 shmem

4.3 为什么选择 shmem:从 BO 需求到 GEM 框架,再到 shmem

4.2 的 DMA-CMA 将分配链路缩至最短,代价是内存一次分配后即固定不变。对于 UMA 设备上数以百计乃至千计的 BO,这种方式代价过高。DRM 提供的通用方案是shmem:每个 BO 以一个 shmem 文件作为后备。

本节按以下三个步骤说明为何选择 shmem 文件:

  1. 从 BO 的使用方式归纳出对后备存储的需求;
  2. 分析 GEM框架层将这些需求体现为哪些接口,以及由此对后备提出的具体要求;
  3. 逐层检验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、N2free(唯一必选的回调,引用计数归零时调用);size(注释写明对象生命期内不可变);resv(vmap、evict等回调要求持锁调用)随对象释放;大小固定;状态变更在dma_resv下进行
按功能N3、N4mmap/vm_ops;vmap/vunmap;pin/unpin(dma-buf 导入方 attach 时调用);get_sg_table(导出时调用,随后以导入方设备进行 DMA 映射)可按页插入用户页表;可组合为连续的内核地址;可被固定;可表示为由struct page组成的 sg 表
可选回收N6evict;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_MOVABLE4.3.1
访问期间不被回收内核可能随时回收文件的页持有页期间将文件标记为 unevictable,内核不再回收;本设备访问期间由驱动保证不进行 purge;内核 vmap 及导出给其他设备时另行 pin4.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及其创建过程。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进