1. 本章小结
本章讨论没有专用显存的设备上,BO 的页从哪里来。两种轻量后备的差异可以归结为一个取舍:内存常驻,还是允许回收。
1.1 定位
GEM 把对象身份与后备存储分离,后备存储可以替换。TTM 的放置、迁移与驱逐都围绕稀缺的 VRAM 设计,对没有 VRAM 的设备而言是多余的负担,因此 DRM core 另外提供两种轻量后备:
- 设备只能访问连续地址时,使用
drm_gem_dma_object; - 其余情况使用
drm_gem_shmem_object。
二者都以 helper 的形式提供,驱动内嵌对象、在回调表中引用 helper 函数即可接入。
1.2 常驻的 DMA-CMA 后备
DMA-CMA 后备在对象创建时一次分配全部内存,同时得到 CPU 使用的虚拟地址与设备使用的 DMA 地址,内存常驻至对象释放,不会被换出或回收。由于页始终存在,后备不需要跟踪页的状态,访问也很直接:vmap返回已有地址,mmap一次建立全部页表项,本设备直接使用 DMA 地址。代价是内存始终被占用,且导入其他设备的缓冲区时要求 DMA 地址连续。
1.3 可回收的 shmem 后备
无显存设备上的 BO 数量多、体积大、大部分时间闲置,且与普通应用共用系统内存。后备存储因此既要提供 CPU 与设备可访问的页,又要尽量少占内存。shmem 文件满足后一项要求:页在首次使用时才分配,内容保存在文件的页缓存中,对象持有的页数组只是引用,归还后页可以由内核换出,再次使用时透明换入。前一项要求所需的 pin、sg 表与 DMA 映射,由 helper 补齐。
可回收性的代价是,页不再始终存在,helper 必须知道每个时刻哪些页可以被回收。
1.4 可回收性的实现
shmem helper 以三层计数记录页的使用者,计数只在 0 与 1 之间跨越时执行实际操作:
pages_use_count:持有页数组的使用者;pages_pin_count:要求页不可移动的使用者;vmap_use_count:内核虚拟映射的使用者。
对象据此处于无页、有页、已 pin、已 purge 四种状态之一。访问与回收都以这些状态为依据:
- 访问路径按需求的强弱取用不同的计数。CPU 侧,
vmap需要 pin,mmap只增加使用计数,缺页时从页数组插入 PFN,写缺页时标记脏页;设备侧,本设备使用的 sg 表只持有使用计数,导出给其他设备时在 attach 时 pin。 - 回收路径只作用于计数允许的状态。使用计数归零后,页归还给 shmem 文件,由内核换出至 swap,再次使用时透明换入;用户态把对象标记为可丢弃后,驱动注册的 shrinker 在内存紧张时将其 purge,对象进入终态,再次访问收到
SIGBUS。
1.5 驱动接入
virtio-gpu 的 GUEST blob 表明,驱动只需内嵌对象、转发少量回调并设置一个钩子,即可把 guest 页转换为交给 host 的地址表。
下一章进入有专用显存的独立显卡,讨论 TTM 如何在 VRAM 与系统内存之间放置、迁移与驱逐 BO。
2. 依赖的内核机制与延伸问题
两种轻量后备都直接建立在内核内存管理与 DMA 子系统之上。以下按子系统说明本章用到的机制,并就每个子系统提出若干沿本章内容向外延伸的问题,每题附研究入口,供读者自行追踪。
2.1 shmem 与页回收
本章的 shmem 对象以 shmem 文件为后备(4.4、4.5):shmem_file_setup()创建文件,shmem_read_folio_gfp()按需取页;页保存在文件的页缓存中,归还后由内核经pageout()与shmem_writeout()换出至 swap,purge 时则以unmap_mapping_range()撤销 CPU 映射(4.8)。shmem/tmpfs 与页回收见附录 B.7,address_space与反向映射见附录 B.8。用户态 mmap 以VM_PFNMAP映射这些页,缺页时由vmf_insert_pfn()或vmf_insert_pfn_pmd()插入页表项,写缺页经pfn_mkwrite标脏(4.2、4.6),实现位于mm/memory.c。
不可回收标记:
drm_gem_get_pages()以mapping_set_unevictable()标记整个 shmem 文件,drm_gem_put_pages()再清除标记。这一标记作用于address_space而不是单个 folio,内核如何把已在 LRU 上的页移入或移出不可回收链表?它与 mlock 造成的不可回收有何异同?(入口:drivers/gpu/drm/drm_gem.c、mm/vmscan.c的check_move_unevictable_folios())脏页与换出:还页时 helper 按标志为 folio 补标 dirty 与 accessed。shmem 页没有对应的磁盘文件,回收路径如何决定把它写入 swap,为什么
pageout()直接调用shmem_writeout()?页仍被其他路径引用时,回收会如何处理?(入口:mm/vmscan.c的pageout()、mm/shmem.c的shmem_writeout();本书附录 B.7)大页映射:
drm_gem_huge_mnt_create()为设备挂载一个带大页选项的私有 tmpfs,helper 缺页时可以用vmf_insert_pfn_pmd()一次映射一个 PMD。要使 PMD 映射成立,shmem 文件中的 folio 与用户态虚拟地址各需满足什么条件?(入口:drivers/gpu/drm/drm_gem.c、drivers/gpu/drm/drm_gem_shmem_helper.c的缺页处理、Documentation/admin-guide/mm/transhuge.rst)
2.2 shrinker
驱动通过shrinker_alloc()注册 shrinker,以count_objects与scan_objects在内存紧张时 purge 可丢弃对象(4.8、4.9),实现位于mm/shrinker.c。
shrinker 与锁:panfrost 的 shrinker 在扫描时对设备锁、映射锁和对象的
resv都只使用 trylock。内存回收可能发生在哪些持锁路径中,为什么阻塞加锁会导致死锁?lockdep 如何借助fs_reclaim_acquire()提前发现这类问题?(入口:mm/shrinker.c、drivers/gpu/drm/panfrost/panfrost_gem_shrinker.c、mm/page_alloc.c)可丢弃语义:用户态把对象标记为可丢弃后,对象可能在任意时刻被 purge。panfrost 的 madvise ioctl 以
retained返回对象当前是否仍保有内容,用户态应如何利用这一结果?这一语义与匿名内存的MADV_FREE、MADV_DONTNEED有何异同?(入口:drm_gem_shmem_madvise_locked()、drivers/gpu/drm/panfrost/panfrost_drv.c、mm/madvise.c)
2.3 DMA 映射、CMA 与 dma-buf
DMA-CMA 后备以dma_alloc_wc()分配连续内存,可能取自 CMA 区;shmem 后备以dma_map_sgtable()将 sg 表映射至设备;两者都经 dma-buf 导出与导入(4.2、4.7),实现位于kernel/dma/与mm/cma.c,dma-buf 本身见第 6 章。vmap()建立的内核连续映射(4.6)与用户态映射一样受map_wc控制,其缓存属性问题见本小节第 3 题。
CMA 的来源:
dma_alloc_wc()在没有 IOMMU 时经 dma-direct 分配物理连续的内存,可能取自 CMA 区;有 IOMMU 时则由 dma-iommu 把离散页映射为连续的 DMA 地址。CMA 区如何预留,平时如何借给可移动页使用,分配连续内存时又如何把这些页迁走?(入口:kernel/dma/direct.c、kernel/dma/contiguous.c、mm/cma.c、drivers/iommu/dma-iommu.c)连续性判断:DMA helper 导入缓冲区时以
drm_prime_get_contiguous_size()判断 DMA 地址是否连续。IOMMU 合并 sg 段后,nents与orig_nents有何区别?为什么同一个 dma-buf 对一个设备连续,对另一个设备可能不连续?(入口:drivers/gpu/drm/drm_prime.c、drivers/gpu/drm/drm_gem_dma_helper.c、include/linux/scatterlist.h)写合并属性:设置
map_wc时,helper 在 x86 上取页后调用set_pages_array_wc(),还页前调用set_pages_array_wb()。为什么同一物理页不能在不同映射中使用相互冲突的缓存属性?x86 的 PAT 如何记录和检查这些属性?源码注释为何把其他架构的支持留作待办?(入口:drivers/gpu/drm/drm_gem_shmem_helper.c、arch/x86/mm/pat/set_memory.c、arch/x86/mm/pat/memtype.c)
2.4 虚拟化
virtio-gpu 的 GUEST blob(4.9)把 shmem 对象的页交给 host 直接访问,实现位于drivers/gpu/drm/virtio/。
- guest 页如何到达 host:virtio-gpu 把 sg 表转换为
virtio_gpu_mem_entry数组,使用 DMA API 时表中填入sg_dma_address(),否则填入sg_phys()。这两种地址在何种配置下不同,guest 启用 virtio-iommu 后会怎样?host 侧又如何把这些地址映射为自己可访问的内存?(入口:drivers/gpu/drm/virtio/virtgpu_object.c的virtio_gpu_object_shmem_init();virtio 规范的 GPU 设备一节)
3. 参考资料
[1] The kernel development community. DRM Memory Management[EB/OL]. https://docs.kernel.org/gpu/drm-mm.html.
[2] The kernel development community. Tmpfs[EB/OL]. https://docs.kernel.org/filesystems/tmpfs.html.
[3] The kernel development community. Transparent Hugepage Support[EB/OL]. https://docs.kernel.org/admin-guide/mm/transhuge.html.
[4] The kernel development community. Dynamic DMA mapping using the generic device[EB/OL]. https://docs.kernel.org/core-api/dma-api.html.
[5] The kernel development community. Buffer Sharing and Synchronization (dma-buf)[EB/OL]. https://docs.kernel.org/driver-api/dma-buf.html.
[6] OASIS. Virtual I/O Device (VIRTIO) Version 1.2[EB/OL]. https://docs.oasis-open.org/virtio/virtio/v1.2/cs01/virtio-v1.2-cs01.html.
[7] CORBET J. GEM v. TTM[EB/OL]. (2008-05-28). https://lwn.net/Articles/283793/.
[8] CORBET J. Smarter shrinkers[EB/OL]. (2013-05-14). https://lwn.net/Articles/550463/.
[9] GORMAN M. Understanding the Linux Virtual Memory Manager[M]. Upper Saddle River: Prentice Hall, 2004.