内存管理-5-物理内存数据结构-6-struct vm_area_struct

基于msm-5.4

一、struct vm_area_struct

1. 简介

此结构用来描述一个 VMM 内存区域。每个 VM 区域/任务都有一个这样的区域。VM 区域是进程虚拟内存空间的一部分,它对page-fault处理程序有特殊规则(即共享库、可执行区域等)。

无论是加载一个动态链接库,还是通过mmap创建映射,都需要在进程地址空间中增加一个新的vma结构。具体过程是首先通过 get_unmapped_area()找到虚拟地址空间中一块空闲且大小满足要求的区域,分配给新vma并设置其flag属性,返回该vma起始处的虚拟地址。注意分配的vma只是这段虚拟地址的使用权,而不是物理地址的使用权。

 

2. 成员介绍

//include/linux/mm_types.h
struct vm_area_struct {
    unsigned long vm_start; //VMA起始地址(包含)
    unsigned long vm_end;   //VMA结束地址(不包含)
    struct vm_area_struct *vm_next, *vm_prev; //自己组成一个双向链表, 分别为 mm->mmap 单链表后继(按地址升序) 和 mm->mmap 前驱
    struct rb_node vm_rb; //挂入 mm->mm_rb 的红黑树节点
    unsigned long rb_subtree_gap; //该子树内“最大空洞”大小,加速 get_unmapped_area()

    /* Second cache line starts here. after 64B */
    struct mm_struct *vm_mm; //所属地址空间(进程的 mm_struct)
    pgprot_t vm_page_prot;   //页表保护位(最终落到 PTE/PMD prot)
    unsigned long vm_flags;  //VM_READ/VM_WRITE/VM_EXEC/VM_SHARED ...

    union {
        struct {
            struct rb_node rb; //挂到 address_space->i_mmap 的区间树节点
            unsigned long rb_subtree_last; //该子树覆盖到的最大 pgoff 末端
        } shared;
        const char __user *anon_name;
    };

    struct list_head anon_vma_chain; //连接到一个或多个 anon_vma(COW 后可多条),Serialized by mmap_sem & page_table_lock
    struct anon_vma *anon_vma; //该VMA主 anon_vma,匿名页反查入口, Serialized by page_table_lock
    const struct vm_operations_struct *vm_ops; //缺页、close、mremap、split 等回调
    unsigned long vm_pgoff; //vm_file内的文件页偏移(单位:PAGE_SIZE)
    struct file * vm_file;  //后端文件,匿名映射为 NULL
    void * vm_private_data;    //驱动/文件系统私有数据,was vm_pte (shared mem)
    atomic_long_t swap_readahead_info; //CONFIG_SWAP,swap fault 预读状态
    struct vm_userfaultfd_ctx vm_userfaultfd_ctx; //userfaultfd 注册上下文
} __randomize_layout;

2.1 成员介绍:

vm_start:

指向此区域起始虚拟地址(包含)。若用户mmap()此地址通常作为mmap()返回给用户空间的虚拟地址。必须是页大小对齐的。

vm_end:

指向此区域结束虚拟地址(不包含), 此vma定义的虚拟地址范围是 [vm_start, vm_end)。也必须得是页大小对齐的。所以长度 vm_end - vm_start 也是页大小整数倍

必须页对齐的原因:

页表映射的最小管理单位就是页。缺页、中断处理、mprotect、munmap、rmap 等都按页处理。VMA 合并/拆分逻辑也依赖页粒度边界。
不是所有场景都只要 4KB 对齐,普通匿名映射、普通文件映射,通常 PAGE_SIZE 对齐;HugeTLB 映射,需要按 huge page 大小对齐(例如 2MB、1GB),比普通页更严格。某些架构或策略(如共享库地址随机化、mmap 对齐策略)可能要求更大的地址对齐,但最终至少满足页对齐。

一个常见误区: 用户态看到的指针可以不是页对齐(比如 malloc 返回 16 字节对齐)。但底层承载这些分配的 VMA 边界仍是页对齐。只是 malloc 用户层实现页内再切分。

 

vm_next/vm_prev:

mm->mmap 单链表后继(按地址升序) 和 mm->mmap 前驱。任务的所有 VM 区域构成的链表,按地址升序挂在此链表中。

 

vm_rb:
通过它挂入 mm->mm_rb 的红黑树节点。通过 vma->vm_start 从小到大的次序挂入的,通常满足 prev_vma->vm_end <= vma->vm_start。若中序遍历(左子树-->父节点-->右子树),可以按虚拟地址从小到大的次序遍历进程所有的vma.

mm->mm_rb 这棵红黑树维护的是该进程地址空间里的所有 VMA,核心不变量就是:
(1) 各个 VMA 按虚拟地址有序;
(2) 任意两个 VMA 不能重叠;
(3) 允许“首尾相接”,不允许“交叉覆盖”;

保证不重叠: insert_vm_struct() 在真正插入前,会先调用 find_vma_links() 检查新区间 [vma->vm_start, vma->vm_end) 是否和已有 VMA 冲突。只要发现重叠,就直接返回 -ENOMEM,不会插入。

对同一个进程来说:mm->mmap 链表里 VMA 不重叠; mm->mm_rb 红黑树里 VMA 也不重叠.

同一进程地址空间里,一个虚拟地址只能有一种明确的映射语义。如果两个 VMA 在同一 mm 内重叠,会立刻带来歧义:这个地址到底属于哪个 VMA、权限该按谁的 vm_flags、page fault 时该走哪个 vm_ops、mprotect、munmap、rmap、find_vma 都会失去确定性。所以“VMA 不重叠”是整个内存管理的基础约束。

 

rb_subtree_gap:

以此 vma 为根的子树中最大的 gap(两个vma之间的空闲区间)。有助于 get_unmapped_area() 找到合适大小的可用区域。相关函数如下:

vma_compute_gap() //计算单个节点的 gap
vma_compute_subtree_gap() //递归计算子树最大 gap
vma_gap_update() //节点修改后的 gap 更新
vma_gap_callbacks //红黑树旋转时的回调
unmapped_area() //实际使用 rb_subtree_gap 的查询函数

 

vm_mm:
所属地址空间(进程的 mm_struct)

 

vm_page_prot:

页表保护位(最终落到 PTE/PMD prot)。描述此 vma 区域所有页面的保护标志(也即访问权限,如读写权限等)。vm_page_prot.pgprot 成员用于定义虚拟内存区域(VMA)的页表项权限标志。其取值由基础权限和架构扩展权限两部分组成:

(1) 基础权限:由 vm_flags 转换而来(如 VM_READ、VM_WRITE 等);
(2) 架构扩展权限:ARM64 特有的内存属性(如缓存策略、执行权限等)。包含:
a. 内核空间预设宏(用于内核代码/数据段):
PAGE_KERNEL: 可读/写/执行(默认);
PAGE_KERNEL_RO: 只读;
PAGE_KERNEL_ROX: 只读 + 可执行(用于代码段);
PAGE_KERNEL_EXEC: 可读 + 可执行(替代旧版 VM_EXEC)。
示例: 内核初始化时设置文本段为 PAGE_KERNEL_ROX 或 PAGE_KERNEL_EXEC。
b. 用户空间与设备映射:
Write-Combine (WC): 通过 pgprot_writecombine() 设置,用于 MMIO 设备映射(避免缓存,直接写入设备);
Non-Cacheable (NC): 通过 pgprot_noncached() 设置,用于无缓存访问;
Device Memory (DEVICE_nGnRE):用于外设寄存器映射。

关键机制说明:
(1) 动态生成逻辑: pgprot 最终通过 vm_get_page_prot() 生成,合并基础权限和架构扩展权限。
(2) 权限的实际应用: 最终 pgprot 值会写入页表项(PTE),由 MMU 硬件执行权限检查; 非法访问(如写入只读页)会触发缺页异常.
修改方式:

用户态通过 mprotect() 修改 vm_flags,触发 vm_page_prot 更新;

内核驱动可通过 io_remap_pfn_range() 直接设置自定义 pgprot(如 MMIO 映射)。

注: 完整权限标志定义可参考 Linux 内核头文件 arch/arm64/include/asm/pgtable-prot.h。

 

vm_flags:

描述此区域是私有的还是共享的等,见mm.h, VM_IO: 此区域映射的是设备IO地址;VM_LOCKED: 此页被锁住了不能交换出去;

更多参考《内存管理-5-vm_area_struct->vm_flags

 

shared.rb:

通过它挂到区间树上,文件页映射的VMA挂到 page->mapping->i_mmap, 匿名页映射的VMA挂到 page->mapping=(anon_vma|PAGE_MAPPING_ANON) anon_vma->rb_root 树上

 

shared.rb_subtree_last
该子树覆盖到的最大 pgoff 末端。以当前节点为根的子树的最大右区间。

 

anon_vma_chain:

反向映射使用。连接到一个或多个 anon_vma(COW 后可多条)。

在文件页面之一的 COW 之后,文件的 MAP_PRIVATE vma 可以同时位于 i_mmap 树和 anon_vma list 中。MAP_SHARED vma 只能位于 i_mmap 树中。匿名 MAP_PRIVATE、堆栈或 brk vma(带有 NULL 文件)只能位于 anon_vma 列表中。

 

anon_vma:
该VMA主 anon_vma,匿名页反查入口。

主 anon_vma,也就是该 VMA 当前“默认归属”的 anon_vma。还有一些关联 anon_vma (历史血缘、merge/split/fork 产生)挂在 vma->anon_vma_chain 链表上。

 

vm_ops:

用于处理此结构的行为回调函数指针。缺页、close、mremap、split 等回调。

它为NULL就表示匿名VMA,不为NULL就是非匿名VMA. 通过 vma_set_anonymous()/vma_is_anonymous() 判断。

 

vm_pgoff:

定义该区间映射到文件中的页偏移基线。vm_file内的文件页偏移(单位:PAGE_SIZE)。

 

vm_file:
关联具体文件对象。后端文件,匿名映射为 NULL

 

vm_private_data
给驱动或 FS 挂私有上下文。

 

swap_readahead_info
swap 预读策略状态

 

vm_userfaultfd_ctx
userfaultfd 注册上下文。支持 userfaultfd 的按区间拦截。

 

2.2. 补充介绍

1. 文件页映射的VMA,其 anon_ 成员是否为空

需要分情况来看:

(1) MAP_SHARED 共享文件页映射的VMA: anon_vma 通常为 NULL,anon_vma_chain成员通常为空链表。因为共享文件页映射走文件反向映射(使用 i_mmap区间树),不走匿名 COW 链路。

(2) MAP_PRIVATE 私有文件映射VMA,且还没发生写时复制(COW): anon_vma 成员通常为 NULL,anon_vma_chain 成员通常为空。因为此时页还是文件页语义,尚未需要匿名反向映射结构。

(3) MAP_PRIVATE 文件映射VMA,只要发生过一次 COW(哪怕只是一部分页): anon_vma 会会被建立并指向(非空),anon_vma_chain 成员会挂上对应节点(非空)。因为 COW 后产生匿名页,需要 anon_vma 路径支持匿名页反查。虽然这个 VMA 仍然是文件映射 VMA,同时也可能带 anon_vma 关系

总结:纯文件共享映射 anon_vma 相关为空。私有文件映射,COW 前为空,COW 后可非空。所以“文件页映射的 VMA”不一定一直没有 anon_vma,MAP_PRIVATE + COW 是例外且很常见。

注: 上面说的"这个 VMA"是之前的那个 VMA,COW不会新建 VMA, 只是在原 VMA 上懒惰地补全 anon_vma 结构,让新产生的匿名页能被反向映射找到.

 

二、struct vm_operations_struct

1. 简介

VMA的操作函数集。这些是虚拟 MM 函数 - 打开、关闭和取消映射一个区域(需要使磁盘上的文件保持最新等)、指向发生 no-page 或 wp-page 异常时调用的函数。

//include/linux/mm.h
struct vm_operations_struct {
    void (*open)(struct vm_area_struct * area);
    void (*close)(struct vm_area_struct * area); //进程退出时会调用这个回调
    int (*split)(struct vm_area_struct * area, unsigned long addr);
    int (*mremap)(struct vm_area_struct * area);
    vm_fault_t (*fault)(struct vm_fault *vmf);
    vm_fault_t (*huge_fault)(struct vm_fault *vmf, enum page_entry_size pe_size);
    void (*map_pages)(struct vm_fault *vmf, pgoff_t start_pgoff, pgoff_t end_pgoff);
    unsigned long (*pagesize)(struct vm_area_struct * area);
    vm_fault_t (*page_mkwrite)(struct vm_fault *vmf);
    vm_fault_t (*pfn_mkwrite)(struct vm_fault *vmf);
    int (*access)(struct vm_area_struct *vma, unsigned long addr, void *buf, int len, int write);
    const char *(*name)(struct vm_area_struct *vma);
    struct page *(*find_special_page)(struct vm_area_struct *vma, unsigned long addr);
};

此函数集的典型设置点通常在文件/设备的 file_operations->mmap() 中,如:

static int binder_mmap(struct file *filp, struct vm_area_struct *vma)
{
    vma->vm_ops = &binder_vm_ops;
    vma->vm_private_data = proc;
    return 0;
}

 

2. 成员介绍

open:

当一个“新的VMA实例”继承或复制了现有 VMA 的语义时调用。最典型的两个场景是:fork()复制父进程VMA,以及 split_vma()/mremap() 产生新的VMA对象。这里的 open 回调更多的是创建VMA时调研,可理解为这个VMA又多了一个实例/副本时调用。
很多驱动把 open 回调写成“增加引用计数”,对应 close() 回调做“减少引用计数”。


close:

当VMA从进程地址空间中移除时调用。典型路径包括: munmap()、进程退出时的 exit_mmap()、execve()销毁旧mm、以及某些 mremap() 场景下旧VMA被替换。

常见用途有:释放 `vm_private_data` 持有的资源; 对设备对象、buffer、映射句柄做引用计数递减; 触发写回、解绑、撤销映射等清理操作。

注意: close() 回调的语义是“VMA生命周期结束”,不是“文件关闭”,一个文件可以还开着,但它的某个VMA已经被 munmap() 了,此时 close() 也会被回调执行。


split:

在内核准备把一个 VMA 从某个地址处分裂成两个 VMA 之前调用。常见触发路径:munmap() 只解除中间一段、mprotect() 只改部分权限、mremap() 只移动/调整部分区间。

用途:驱动或特殊映射可以拒绝部分拆分。例如某些设备映射、特殊映射要求“整段必须保持完整”,则可返回 `-EINVAL`。

可直观理解为:这块 VMA 允许被从 addr 处分成两半吗。


mremap:

在 mremap() 系统调用已经决定要把这个 VMA 移动到新地址,且新的 VMA 结构已经准备好之后调用。该回调面对的是“新 VMA”。

用途:更新驱动内部记录的地址范围; 校验这种映射是否允许被移动; 对特殊映射执行自定义 remap 修正。

典型例子:[vdso]/[vvar]一类 special mapping 会通过 mremap 回调决定 remap 是否被允许。


fault:

缺页异常进入通用缺页处理后,发现该 VMA 需要由 vm_ops 自己解析页面时调用,用与向内核说明“缺页时怎么建映射”(通常分配物理页,记录到vmf上)。 最常见是:地址第一次被访问、PTE 不存在,内核需要为该地址建立映射。

典型场景:文件映射页缓存缺页; 设备驱动按页懒加载; PFNMAP/MIXEDMAP 类型映射在访问时把物理页插进去。

常见职责:根据 vmf->pgoff / vmf->address 找到后端页; 把页放到 vmf->page,或调用 vmf_insert_pfn()/ vm_insert_page() 类接口建立映射; 返回 VM_FAULT_* 结果给内核继续处理。

一句话:这个地址现在缺页了,请你告诉内核该映射哪一页。


huge_fault:

发生大页粒度的 fault 时调用。常见于 hugetlbfs、DAX、设备大页映射等场景。

用途:用大页大小来解析 fault,而不是标准 4KB 页。order 指示想要建立的页阶数。

何时会出现:普通匿名页/普通文件页大多数不需要自己实现它。只有底层确实支持 huge mapping 的 VMA 才会实现。


map_pages:

在普通读 fault 的“fault-around”优化路径中调用。内核发现某一页 fault 后,不只想映射当前页,还想顺手把周围一批页一起映射进去,减少后续 fault 次数。####

典型实现:页缓存文件映射常用 filemap_map_pages()。

和 fault 回调的区别:.fault 处理“这一个 fault 地址”,而 .map_pages 处理“围绕这次 fault,能不能顺便多装几页”。


pagesize:

内核或 procfs 需要知道该 VMA 的“有效页大小”时调用。常见于 hugetlb/shmem 特殊映射,需要对外展示不是普通 PAGE_SIZE 的页粒度。

用途:给 /proc/pid/smaps、NUMA 统计或某些内核判断提供该 VMA 的页面粒度信息。


page_mkwrite:

共享可写映射发生写保护 fault,某个原本只读映射页即将被改成可写之前调用。常见于 MAP_SHARED 文件映射第一次写入某页。

用途:让文件系统先做写入前准备,例如申请块、更新时间戳、等待 writeback 稳定、标脏页、阻止与 truncate 冲突。

关键点:这不是“写系统调用”的回调,而是“用户通过 mmap 写共享页”的回调。返回错误通常会让用户态收到 SIGBUS。


pfn_mkwrite:

与 page_mkwrite 类似,但用于 VM_PFNMAP/VM_MIXEDMAP这类不是标准 page cache page 的映射。常见于 DAX、设备直映射、特殊 PFN 插入映射。

一句话:某个 PFN 级别的共享映射页马上要变可写了,请先做写前检查/准备。


access:

access_process_vm() / ptrace/ /proc/<pid>/mem 一类“远程访问进程地址空间”的路径中,当通用 GUP(Get User Pages) 方式无法直接处理这个 VMA 时调用。

常见对象: IO内存映射; 特殊设备映射; VM_PFNMAP 这类没有普通 struct page 支撑的 VMA。

用途:从特殊映射中读数据到内核 buffer。或把数据写回这种特殊映射。


name:

procfs 输出 /proc/<pid>/maps 时调用。如果返回非空字符串,procfs 就把这个名字显示成该 VMA 的名字。

典型用途:给特殊映射显示 [vdso]、[vvar] 或驱动自定义名称。

注意:这是展示用途,不参与实际映射逻辑。


find_special_page:

把“被标记为 special 的 PTE”重新解析成对应的 struct page,供内核当作普通页继续处理。

触发位置: 在 vm_normal_page() 里调用, 当 PTE 是 pte_special,默认逻辑通常拿不到正确的 struct page,这时会尝试调用 vm_ops->find_special_page(vma, addr)。

有些驱动/特殊映射会把页表项做成 special 形式(例如 PFNMAP/MIXEDMAP 相关场景),导致不能直接靠 pte_page 得到可用 page。find_special_page 提供一个“驱动自定义反查”入口, 它返回 struct page* 告诉内核“这个地址其实对应一个可当普通页处理的 page”,返回 NULL 表示仍然是特殊映射,不作为普通页处理。

和 fault 回调的区别是,fault 是“缺页时怎么建映射”,而 find_special_page 是“映射已经在页表里了,但它是 special,内核需要一个 struct page 时该怎么找”。

新版本内核这个接口名字演进成了 find_normal_page 了。


小结:

可以把这些回调按用途分成 5 组:
(1) 生命周期管理:open、close
(2) VMA形态变化:may_split、mremap、mprotect
(3) 缺页与建图:fault、huge_fault、map_pages
(4) 写入前通知:page_mkwrite、pfn_mkwrite
(5) 辅助信息/扩展能力:pagesize、access、name、set_policy、get_policy


最容易混淆的 4 组区别:

(1) vm_ops->open() vs f_op->mmap()
f_op->mmap() 是“第一次建立映射时”的文件操作,在 mmap() 系统调用路径中执行。vm_ops->open() 是“后续新 VMA 副本诞生时”的 VMA 相关操作,最典型是 fork() 和 split_vma()。

(2) vm_ops->fault vs vm_ops->map_pages
vm_ops->fault 解决当前缺页。vm_ops->map_pages 是顺手多映射几页的性能优化。

(3) vm_ops->page_mkwrite vs vm_ops->pfn_mkwrite
vm_ops->page_mkwrite 面向普通 struct page / page cache 页。vm_ops->pfn_mkwrite 面向 PFNMAP / MIXEDMAP 等特殊映射。

(4) vm_ops->close vs 文件 f_op->release
vm_ops->close 针对 VMA 生命周期。f_op->release 针对文件描述符生命周期。二者可能先后发生,也可能只发生其中一个。

 

 

 

 

posted on 2024-06-19 16:15  Hello-World3  阅读(302)  评论(0)    收藏  举报

导航