4.3 论证了采用 shmem 文件作为后备的理由:内容存放于文件中,helper 仅按需获取页,使用完毕后归还页。本节先给出 helper 的核心数据结构struct drm_gem_shmem_object的全貌,再讨论drm_gem_shmem_create()创建对象时完成的工作。结论只有一句:创建结束时,对象拥有一个 shmem 文件,但没有任何物理页。
1、结构全貌
该结构定义于include/drm/drm_gem_shmem_helper.h:
structdrm_gem_shmem_object{structdrm_gem_objectbase;/* 内嵌的 GEM 对象 */structpage**pages;/* 从 shmem 文件获取的页 */refcount_tpages_use_count;/* pages[] 的持有者计数 */refcount_tpages_pin_count;/* pin 计数 */intmadv;/* 0 在用;>0 可丢弃;<0 已 purge */structlist_headmadv_list;/* 加入驱动的可回收链表 */structsg_table*sgt;/* 已 DMA 映射的 sg 表 */void*vaddr;/* 内核连续映射地址 */refcount_tvmap_use_count;/* vaddr 的持有者计数 */bool pages_mark_dirty_on_put:1;bool pages_mark_accessed_on_put:1;bool map_wc:1;/* 以 write-combined 方式映射 */};除base外,其余字段均描述同一事项:所持有的页当前处于何种状态,包括是否已获取、是否被 pin、被映射为哪些形态以及能否被回收。
2、base:内嵌的 GEM 对象
base是内嵌的结构体而非指针,因此drm_gem_shmem_object的头部即为一个完整的drm_gem_object。GEM core 仅可见base,helper 获得drm_gem_object *后通过container_of取回外层结构:
#defineto_drm_gem_shmem_obj(obj)\container_of(obj,structdrm_gem_shmem_object,base)驱动通常再将drm_gem_shmem_object内嵌到自身的结构中,形成两层内嵌,见 4.3.7。
3、创建入口:分配与初始化分离
对象刚创建时,上述字段几乎均为空:pages为 NULL,各计数为 0。公开入口drm_gem_shmem_create()直接转调内部函数:
structdrm_gem_shmem_object*drm_gem_shmem_create(structdrm_device*dev,size_tsize){return__drm_gem_shmem_create(dev,size,false);}__drm_gem_shmem_create()分两步完成:先分配外层对象,再进行初始化。
staticstructdrm_gem_shmem_object*__drm_gem_shmem_create(structdrm_device*dev,size_tsize,bool private){...size=PAGE_ALIGN(size);if(dev->driver->gem_create_object){obj=dev->driver->gem_create_object(dev,size);...shmem=to_drm_gem_shmem_obj(obj);}else{shmem=kzalloc_obj(*shmem);...obj=&shmem->base;}ret=__drm_gem_shmem_init(dev,shmem,size,private);...returnshmem;}- 分配可由驱动接管。驱动实现
gem_create_object时,helper 使用其返回的对象。驱动由此可分配内嵌了drm_gem_shmem_object的更大结构,例如 virtio-gpu 的virtio_gpu_object(4.3.7)。 - 初始化可单独调用。
drm_gem_shmem_init()直接调用__drm_gem_shmem_init(),供自行分配对象的调用者使用,例如 Rust 的 GEM 绑定。 private参数。普通创建传false;dma-buf 导入时传true,见第 6 节。
4、__drm_gem_shmem_init()的四步
staticint__drm_gem_shmem_init(structdrm_device*dev,structdrm_gem_shmem_object*shmem,size_tsize,bool private){structdrm_gem_object*obj=&shmem->base;intret=0;if(!obj->funcs)obj->funcs=&drm_gem_shmem_funcs;/* ① 回调表 */if(private){drm_gem_private_object_init(dev,obj,size);shmem->map_wc=false;}else{ret=drm_gem_object_init(dev,obj,size);/* ② shmem 文件 */}...ret=drm_gem_create_mmap_offset(obj);/* ③ 伪偏移 */...INIT_LIST_HEAD(&shmem->madv_list);if(!private)mapping_set_gfp_mask(obj->filp->f_mapping,/* ④ GFP 掩码 */GFP_HIGHUSER|__GFP_RETRY_MAYFAIL|__GFP_NOWARN);return0;...}① 回调表
drm_gem_shmem_funcs是 helper 预置的一套drm_gem_object_funcs,free、pin、unpin、get_sg_table、vmap、vunmap、mmap、vm_ops都指向 helper 的实现。该赋值的条件为if (!obj->funcs):若驱动已在gem_create_object中设置了自身的回调表,helper 不会将其覆盖(4.3.7)。
② 建立 shmem 文件
drm_gem_object_init()位于 GEM core(drm_gem.c),不属于 helper:
intdrm_gem_object_init(structdrm_device*dev,structdrm_gem_object*obj,size_tsize){structvfsmount*huge_mnt;structfile*filp;constvma_flags_tflags=mk_vma_flags(VMA_NORESERVE_BIT);drm_gem_private_object_init(dev,obj,size);huge_mnt=drm_gem_get_huge_mnt(dev);if(huge_mnt)filp=shmem_file_setup_with_mnt(huge_mnt,"drm mm object",size,flags);elsefilp=shmem_file_setup("drm mm object",size,flags);...obj->filp=filp;return0;}shmem_file_setup()在内核内部的 tmpfs 上创建一个没有路径的文件,并设置到obj->filp。文件大小即对象大小,此后不再改变;文件在drm_gem_object_release()中随对象一起释放,与创建它的进程无关。此即 4.3 第 3.1 节所述的「必选层满足」。
此时尚未分配任何物理页。shmem_file_setup()仅建立了一个可按偏移取页的容器,address_space已就位,page cache 仍为空。页要等到 4.3.2 的get_pages才获取。
若驱动事先调用drm_gem_huge_mnt_create()为设备挂载了启用 THP 的 tmpfs,文件即创建于该挂载点上,此后获取的页可能为 PMD 大小的 folio,mmap 缺页时可按大页插入(4.3.4)。
③ 伪偏移
drm_gem_create_mmap_offset()在设备的drm_vma_offset_manager中为对象分配一段伪偏移,用户态以该偏移调用mmap。这是第三章 3.3 节所述的 GEM 通用机制,shmem helper 直接复用。伪偏移在 shmem helper 中有两处用途:mmap 时据此找到对象(4.3.4),purge 时被回收(4.3.6)。
④ GFP 掩码:设备层的第一处修补
mapping_set_gfp_mask(obj->filp->f_mapping,GFP_HIGHUSER|__GFP_RETRY_MAYFAIL|__GFP_NOWARN);该步骤修改的是address_space的gfp_mask,即「此后从该文件分配页时使用的标志」。源码注释说明了其理由:
Our buffers are kept pinned, so allocating them from the MOVABLE zone is a really bad idea, and conflicts with CMA.
GFP_HIGHUSER不含__GFP_MOVABLE。shmem 文件的页默认从可迁移区域分配,而这些页将被 helper 长期持有并供设备访问,若位于可迁移区域,会妨碍内存规整,并与 CMA 冲突;__GFP_RETRY_MAYFAIL:尽力重试,但允许最终失败,避免大缓冲分配触发 OOM killer;__GFP_NOWARN:分配失败时不打印警告,由上层处理错误。
5、private:导入对象不创建文件
private为 true 时调用drm_gem_private_object_init():不创建 shmem 文件,也不设置 GFP 掩码。该路径用于 dma-buf 导入:页由导出方提供,本地无需后备(4.3.5)。此时map_wc被置为 false,源码注释给出的理由是 dma-buf 映射总是使用 write-combine,由导出方决定。
6、销毁与创建对称
销毁路径drm_gem_shmem_release()对本地对象的处理,恰好按相反顺序撤销创建与使用过程中建立的各项资源:
voiddrm_gem_shmem_release(structdrm_gem_shmem_object*shmem){structdrm_gem_object*obj=&shmem->base;if(drm_gem_is_imported(obj)){drm_prime_gem_destroy(obj,shmem->sgt);}else{dma_resv_lock(shmem->base.resv,NULL);...if(shmem->sgt){/* 设备映射 */dma_unmap_sgtable(obj->dev->dev,shmem->sgt,DMA_BIDIRECTIONAL,0);sg_free_table(shmem->sgt);kfree(shmem->sgt);}if(shmem->pages)drm_gem_shmem_put_pages_locked(shmem);/* 所持有的页 */...dma_resv_unlock(shmem->base.resv);}drm_gem_object_release(obj);/* shmem 文件与伪偏移 */}helper 默认回调表中的free为drm_gem_shmem_object_free(),该函数调用drm_gem_shmem_free(),后者即drm_gem_shmem_release()加上对象本身内存的释放。
7、本节小结
drm_gem_shmem_object头部内嵌drm_gem_object,对象身份、resv、伪偏移、后备文件均由base提供;其余字段均围绕「所持有的页处于何种状态」组织。- 创建分为分配与初始化两步,分配可由驱动通过
gem_create_object接管。 - 初始化四步:设置回调表、建立 shmem 文件、分配伪偏移、设定 GFP 掩码。导入对象走
private路径,不建文件。 - 创建结束时只有文件,没有页。
下一节(4.3.2):页何时、如何从 shmem 文件中取出,又如何归还;以及这些操作共同依赖的那把锁。