前面几节从 GEM fake offset 的角度一路追到mmap()、vm_ops和 page fault。那条链路里VMA 更多是作为“承接 BO mmap 的容器”出现。这一节补充下 VMA 的基础知识。从更通用的视角看VMA 是 Linux MM 用来描述一段用户进程虚拟地址区间的核心对象。不管这段地址来自匿名内存、普通文件、共享内存、设备 MMIO还是 GPU BO内核最后都需要把它抽象成一个struct vm_area_struct。不同来源的差异不是靠“每种 mmap 做一套完全不同的机制”而是靠 VMA 上的字段、flags 和vm_ops表达。1 VMA 描述什么源码里struct vm_area_struct的注释非常直接/* * This struct describes a virtual memory area. There is one of these * per VM-area/task. A VM area is any part of the process virtual memory * space that has a special rule for the page-fault handlers (ie a shared * library, the executable area etc). */structvm_area_struct{...};也就是说VMA 不是物理内存也不是页表项本身。它描述的是某个进程 mm_struct 中的一段虚拟地址范围 这段范围允许什么权限 它对应什么 backing fault 时该怎么处理 fork/munmap/mremap 时该怎么维护生命周期最核心的字段可以先压成这样structvm_area_struct{unsignedlongvm_start;unsignedlongvm_end;structmm_struct*vm_mm;pgprot_tvm_page_prot;vm_flags_tvm_flags;conststructvm_operations_struct*vm_ops;unsignedlongvm_pgoff;structfile*vm_file;void*vm_private_data;...};这些字段分别回答不同问题字段问题vm_start/vm_end这段 VMA 覆盖哪段用户虚拟地址vm_mm属于哪个进程地址空间vm_page_prot页表最终应该用什么 page protectionvm_flags这段映射有什么语义和限制vm_ops后续 fault/open/close/access 谁处理vm_pgoff这段 VMA 对应 backing 的页偏移vm_file如果是 file-backed/device-backed它对应哪个 filevm_private_data子系统/驱动挂自己的上下文因此一个 VMA 可以被看成一条规则[vm_start, vm_end) 这段用户 VA 访问权限由 vm_flags / vm_page_prot 决定 backing 由 vm_file vm_pgoff vm_private_data 描述 动态行为由 vm_ops 决定这就是 VMA 的通用性所在它不关心 backing 到底是 page cache、匿名页、shmem page、PFN、MMIO 还是 GPU VRAM它只提供一套足够表达这些差异的元数据和回调接口。2 VMA 与页表的关系VMA 和页表经常被混在一起但它们不是一回事。可以这样分层用户进程虚拟地址空间 - VMA: 描述一段 VA 的规则 - page table: 描述某个 VA page 当前映射到哪个 PFN - physical memory / device memory / swap / file pageVMA 是区间级别的描述页表是页级别的映射。比如用户态调用void*pmmap(NULL,64*1024*1024,PROT_READ|PROT_WRITE,MAP_SHARED,fd,offset);内核可以先创建一段 64 MiB 的 VMAvm_start 0x7f00_0000_0000 vm_end 0x7f00_0400_0000 vm_pgoff offset PAGE_SHIFT vm_file fd 对应的 struct file但这并不代表 64 MiB 的 PTE 已经全部建立好了。很多映射是 lazy 的mmap 阶段: 建立 VMA 不一定建立每个 PTE 第一次访问某个 VA: CPU page fault handle_mm_fault() 根据 VMA 找到处理逻辑 建立这个 VA page 的 PTE这正是为什么 VMA 中需要vm_ops-fault。VMA 描述“这段地址应该怎么 fault”但真正插入 PTE 可以推迟到访问时。从 3.5 的 GPU BO 例子看VMA: CPU VA 区间已经存在 vm_private_data 指向 BO vm_ops-fault 指向 amdgpu_gem_fault PTE: 还未建立直到 CPU 第一次访问所以不要把mmap()成功理解成“物理页已经全部映射好了”。更准确的是mmap()成功意味着进程地址空间多了一段 VMA这段 VMA 后续如何变成 PTE由 fault/populate/remap 等机制决定。3 VMA 如何进入进程地址空间用户态mmap()最终会进入内核的 mmap 路径。通用入口之一是do_mmap()unsignedlongdo_mmap(structfile*file,unsignedlongaddr,unsignedlonglen,unsignedlongprot,unsignedlongflags,vm_flags_tvm_flags,unsignedlongpgoff,unsignedlong*populate,structlist_head*uf)注释里说得很清楚它把一段用户映射放进当前进程地址空间参数包括file - file-backed 映射对应的 struct file可为 NULL addr - 用户期望的地址或 MAP_FIXED 指定地址 len - 映射长度 prot - PROT_READ/WRITE/EXEC flags - MAP_SHARED/PRIVATE/FIXED 等 pgoff - file-backed 映射中的页偏移do_mmap()会先做通用检查长度是否为 0、offset 是否溢出、VMA 数量是否超过限制、权限和 flags 如何转换成vm_flags然后选择一个未占用的 VA 范围。核心抽象是用户传入 mmap 参数 - MM 计算 vm_flags / page_prot - 选择 [addr, addr len) - 尝试和相邻 VMA 合并 - 不能合并则创建新 vm_area_struct - 插入 mm_struct 的 VMA 索引结构新 VMA 最终会通过vma_link()挂入进程地址空间intvma_link(structmm_struct*mm,structvm_area_struct*vma){VMA_ITERATOR(vmi,mm,0);vma_iter_config(vmi,vma-vm_start,vma-vm_end);if(vma_iter_prealloc(vmi,vma))return-ENOMEM;vma_start_write(vma);vma_iter_store_new(vmi,vma);vma_link_file(vma);mm-map_count;validate_mm(mm);return0;}这里有两个重要动作。第一把 VMA 放进mm_struct的 VMA 索引结构中vma_iter_store_new(vmi,vma);这样后续 page fault 时内核可以根据 fault address 找到覆盖它的 VMA。第二如果这是 file-backed mapping还要和文件的 mapping 建立关系vma_link_file(vma);对应代码是voidvma_link_file(structvm_area_struct*vma){structfile*filevma-vm_file;structaddress_space*mapping;if(file){mappingfile-f_mapping;i_mmap_lock_write(mapping);__vma_link_file(vma,mapping);i_mmap_unlock_write(mapping);}}这就是普通文件 mmap、shmem mmap、设备文件 mmap 都能被反向管理的基础文件的address_space可以知道有哪些 VMA 映射了它。4 vm_pgoffVMA 与 backing 的偏移关系vm_pgoff是理解 VMA 的关键字段之一unsignedlongvm_pgoff;/* Offset (within vm_file) in PAGE_SIZE units */它是 page-based offset不是 byte-based offset。用户态传给mmap()的最后一个参数是 byte offset但进入 VMA 后会变成页偏移。对于普通文件mmap(fd, offset 2 MiB) - vm_pgoff 2 MiB / PAGE_SIZE - VMA 起点对应文件第 2 MiB 处对于 GEM fake offsetmmap(drm_fd, fake_offset) - vm_pgoff fake_offset PAGE_SHIFT - drm_gem_mmap() 用 vm_pgoff 查 vma_node对于 device MMIOmmap(device_fd, offset) - vm_pgoff 可能表示设备 BAR 或设备内存窗口中的页偏移vm_pgoff的意义由vm_file和对应的 mmap/fault 实现解释。它不是天然等于物理地址也不是天然等于文件页号它只是“这段 VMA 相对 backing 的页偏移”。在 TTM fault 中可以看到vm_pgoff如何参与 BO 内部 offset 计算page_offset((address-vma-vm_start)PAGE_SHIFT)vma-vm_pgoff-drm_vma_node_start(bo-base.vma_node);这行代码表达的是fault address 在 VMA 内的偏移 VMA 起点相对 BO fake offset 起点的偏移 BO 内部 page index所以读 mmap/fault 代码时一定要问这个驱动如何解释 vm_pgoff 它代表 file offset、fake offset、BAR offset还是对象内部 offset这通常决定了整个 mmap 机制的路由方式。5 vm_file 与 vm_private_dataVMA 既有通用字段也允许子系统挂自己的上下文。vm_file是通用 file-backed 关系structfile*vm_file;它说明这段 VMA 来自哪个文件对象。普通文件 mmap 时它指向具体文件设备 mmap 时它指向设备文件匿名映射时它通常为空。vm_private_data则是给子系统/驱动用的私有指针void*vm_private_data;它没有固定类型必须和vm_ops配套解释。几个典型例子普通 GEM helper: vm_private_data - struct drm_gem_object TTM/AMDGPU: vm_private_data - struct ttm_buffer_object i915: vm_private_data - struct i915_mmap_offset 某些设备驱动: vm_private_data - driver-specific context这就是为什么读代码时不能只看字段名还要看它在哪个vm_ops下使用。例如 TTM 的 close 回调是voidttm_bo_vm_close(structvm_area_struct*vma){structttm_buffer_object*bovma-vm_private_data;drm_gem_object_put(bo-base);vma-vm_private_dataNULL;}它明确假设vm_private_data是struct ttm_buffer_object *。如果一个 VMA 的vm_private_data实际指向drm_gem_object却配上 TTM 的vm_ops那就是类型错配。所以 VMA 的私有上下文要成组理解vm_ops 决定如何解释 vm_private_data vm_private_data 给 vm_ops 回调提供对象上下文6 vm_flags这段 VA 的语义标签vm_flags是 VMA 的语义标签。普通读写执行权限只是其中一部分#defineVM_READ0x00000001#defineVM_WRITE0x00000002#defineVM_EXEC0x00000004#defineVM_SHARED0x00000008#defineVM_MAYREAD0x00000010#defineVM_MAYWRITE0x00000020#defineVM_MAYEXEC0x00000040#defineVM_MAYSHARE0x00000080VM_READ/WRITE/EXEC表示当前权限VM_MAYREAD/MAYWRITE/MAYEXEC表示未来mprotect()等操作允许提升到什么权限。设备和 GPU mmap 中经常出现这些 flags#defineVM_PFNMAP0x00000400/* Page-ranges managed without struct page, just pure PFN */#defineVM_IO0x00004000/* Memory mapped I/O or similar */#defineVM_DONTEXPAND0x00040000/* Cannot expand with mremap() */#defineVM_DONTDUMP0x04000000/* Do not include in the core dump */它们大致表达flag含义VM_PFNMAP这段 VMA 可能直接映射 PFN不一定有struct pageVM_IO这段映射是 I/O 或类似设备内存VM_DONTEXPAND不允许通过mremap()扩展VM_DONTDUMPcore dump 时不要把它 dump 出去TTM mmap 中就会设置vm_flags_set(vma,VM_PFNMAP|VM_IO|VM_DONTEXPAND|VM_DONTDUMP);这告诉 MM这不是一段普通匿名内存也不是普通 page cache 映射。它可能由驱动在 fault 时插入 PFN可能对应 MMIO/BAR也不应该被随意扩展或 dump。所以vm_flags的作用不是装饰性的。它会影响fault 行为 mremap 行为 core dump 行为 GUP/页迁移/回收如何看待这段 VMA 是否按普通 struct page 处理读设备 mmap 时vm_flags往往和vm_ops一样重要。7 vm_opsVMA 的行为表VMA 本身描述一段地址区间但它不可能内建所有 backing 的处理逻辑。因此它提供了行为表conststructvm_operations_struct*vm_ops;vm_ops里的回调让不同子系统接管 VMA 生命周期事件fault - CPU 访问缺页时如何建立映射 open - VMA 被复制时如何增加引用 close - VMA 被销毁时如何释放引用 access - 内核特殊访问这段 VMA 时如何读写 backing从 MM fault 代码看真正调用驱动 fault 的位置是staticvm_fault_t__do_fault(structvm_fault*vmf){structvm_area_struct*vmavmf-vma;...retvma-vm_ops-fault(vmf);...}这就是所有 file-backed/special/device-backed fault 的共同入口模式CPU page fault - 根据 fault address 找 VMA - 如果需要文件/设备/特殊 fault - 调用 vma-vm_ops-fault()VMA 被复制时内核也会调用openif(new_vma-vm_opsnew_vma-vm_ops-open)new_vma-vm_ops-open(new_vma);这正是 GEM/TTM 用 open/close 维护 BO 引用计数的原因。所以 VMA 的通用设计可以收束成VMA 地址区间 属性 backing offset 私有上下文 行为表它是 Linux MM 与文件系统、设备驱动、GPU 内存管理之间的接口层。8 阅读 VMA 代码的通用方法以后看到任何 mmap/fault 代码可以按这个顺序看1. 这段 VMA 的来源是什么 anonymous / file / shmem / device / GPU BO 2. vm_start/vm_end 覆盖多大范围 用户 mmap length 是否和 backing 大小一致 3. vm_pgoff 怎么解释 file offset / fake offset / BAR offset / object offset 4. vm_flags 设置了什么 普通页、PFNMAP、IO、DONTEXPAND、DONTDUMP 5. vm_ops 是谁 fault/open/close/access 各自接管什么 6. vm_private_data 指向谁 和 vm_ops 是否类型匹配 7. fault 最终插入什么 struct page / PFN / huge page / dummy page / SIGBUS用这套方法再回头看 GEM mmap就不会只盯着 fake offset。fake offset 只是vm_pgoff在 DRM 文件 mmap 空间中的一种解释真正承接用户 CPU VA 生命周期的仍然是通用 VMA 机制。