内存管理-63-VMA-2-VMA相关操作
基于msm-5.4
本文介绍VMA的初始化、插入、删除、查找、split、merge、copy等操作。
一、VMA初始化
1. 相关函数
static inline void INIT_VMA(struct vm_area_struct *vma) //linux/mm.h static inline void vma_init(struct vm_area_struct *vma, struct mm_struct *mm) //linux/mm.h
2. 函数实现
static inline void INIT_VMA(struct vm_area_struct *vma) { INIT_LIST_HEAD(&vma->anon_vma_chain); /* 在低竞争条件下,尝试无锁处理页面错误,避免长时间持有 mmap_lock,默认不使能 */ #ifdef CONFIG_SPECULATIVE_PAGE_FAULT seqcount_init(&vma->vm_sequence); atomic_set(&vma->vm_ref_count, 1); #endif } /* 初始化,清0 VMA,将.vm_ops指向dummy的空函数集 */ static inline void vma_init(struct vm_area_struct *vma, struct mm_struct *mm) { /* 静态变量,放在数据段,默认所有回调函数集都不实现#### */ static const struct vm_operations_struct dummy_vm_ops = {}; memset(vma, 0, sizeof(*vma)); vma->vm_mm = mm; vma->vm_ops = &dummy_vm_ops; /* 调用上面函数 */ INIT_VMA(vma); }
3. 简单调用路径
mmap_region //mmap.c do_brk_flags //mmap.c __install_special_mapping //mmap.c vm_area_alloc(mm) //fork.c shmem_swapin //fork.c shmem_alloc_hugepage //fork.c shmem_alloc_page //fork.cs shmem_pseudo_vma_init //shmem.c hugetlbfs_evict_inode(inode) //inode.c hugetlb_vmtruncate //inode.c hugetlbfs_punch_hole //inode.c remove_inode_hugepages //inode.c hugetlbfs_file_operations.fallocate //inode.c hugetlbfs_fallocate //inode.c vma_init(vma, mm)
vma_init 不涉及红黑树和链表插入操作,不需要持锁保护。
二、VMA插入
1. 相关函数
int insert_vm_struct(struct mm_struct *mm, struct vm_area_struct *vma);
2. 函数实现
int insert_vm_struct(struct mm_struct *mm, struct vm_area_struct *vma) //mmap.c定义,mm.h导出 { struct vm_area_struct *prev; struct rb_node **rb_link, *rb_parent; /* 查找插入位置,若有区间重叠,则失败返回#### */ if (find_vma_links(mm, vma->vm_start, vma->vm_end, &prev, &rb_link, &rb_parent)) return -ENOMEM; /* 只有 vm_flags 带有 VM_ACCOUNT 标志,才检查虚拟地址空间是否够用 */ if ((vma->vm_flags & VM_ACCOUNT) && security_vm_enough_memory_mm(mm, vma_pages(vma))) return -ENOMEM; /* 匿名页映射的VMA的判断条件是 vma->vm_ops==NULL */ if (vma_is_anonymous(vma)) { //return !vma->vm_ops; /* 此时 VMA 还没有对应的物理页,所以 anon_vma 必须为空 */ BUG_ON(vma->anon_vma); /* 匿名VMA将虚拟起始地址转换为"页号",并以此为key挂入区间树上。 * 这样做的目的是让匿名VMA也能参与后续的VMA合并逻辑 */ vma->vm_pgoff = vma->vm_start >> PAGE_SHIFT; } /* 按VMA起始地址递增的次序插入 mm->mmap 链表和 mm->mm_rb 红黑树,对于文件 * 映射VMA以 vma->vm_pgoff 为 key 插入 file->f_mapping->i_mmap 区间树 */ vma_link(mm, vma, prev, rb_link, rb_parent); return 0; }
此函数执行完后,匿名VMA还没有插入到其区间树中。硬件映射VMA已经插入其区间树中了。
1.1 find_vma_links()
找到插入位置。
/* * insert_vm_struct: (mm, vma->vm_start, vma->vm_end, &prev, &rb_link, &rb_parent) //后三个是传出参数 * * 假如有这颗树: * * 18(36) * / \ *2(7) 96(156) * \ / \ * 9(16) 60(65) 181(256) * / \ / \ * 40(49) 70(95) 157(180) 280(312) * / * NULL_1(66-68要插入这里) * * 比如要插入 66-68,执行完后各输出参数指向为: * pprev: 指向 60(65), 后续链表插入时它会被待插入的 vma->vm_prev 指向 * rb_parent 指向待插入位置的父节点,即 70(95) * __rb_link 指向 NULL_1 位置, */ static int find_vma_links(struct mm_struct *mm, unsigned long addr, unsigned long end, struct vm_area_struct **pprev, struct rb_node ***rb_link, struct rb_node **rb_parent) { struct rb_node **__rb_link, *__rb_parent, *rb_prev; __rb_link = &mm->mm_rb.rb_node; //VMA红黑树根节点 rb_prev = __rb_parent = NULL; while (*__rb_link) { struct vm_area_struct *vma_tmp; __rb_parent = *__rb_link; vma_tmp = rb_entry(__rb_parent, struct vm_area_struct, vm_rb); if (vma_tmp->vm_end > addr) { /* 若存在相交的VMA区间,直接失败返回,这也决定了VMA树/链表中虚拟地址不会相交#### */ if (vma_tmp->vm_start < end) return -ENOMEM; __rb_link = &__rb_parent->rb_left; } else { rb_prev = __rb_parent; __rb_link = &__rb_parent->rb_right; } } *pprev = NULL; if (rb_prev) *pprev = rb_entry(rb_prev, struct vm_area_struct, vm_rb); *rb_link = __rb_link; *rb_parent = __rb_parent; return 0; }
1.2 vma_link()
执行实际的插入。
static void vma_link(struct mm_struct *mm, struct vm_area_struct *vma, struct vm_area_struct *prev, struct rb_node **rb_link, struct rb_node *rb_parent) { struct address_space *mapping = NULL; /* 若是文件映射(匿名映射为NULL) */ if (vma->vm_file) { mapping = vma->vm_file->f_mapping; i_mmap_lock_write(mapping); //down_write(&mapping->i_mmap_rwsem); } /* 按起始地址递增的次序插入 mm->mmap 链表和 mm->mm_rb 红黑树 */ __vma_link(mm, vma, prev, rb_link, rb_parent); /* 文件映射的VMA,根据vm_flags做统计,然后以 vma->vm_pgoff 为key, 插入 file->f_mapping->i_mmap 区间树 */ __vma_link_file(vma); if (mapping) i_mmap_unlock_write(mapping); //up_write(&mapping->i_mmap_rwsem); mm->map_count++; /* 若没实现 CONFIG_DEBUG_VM_RB 是空函数 */ validate_mm(mm); }
文件映射的VMA在红黑树插入和链表插入都有 file->f_mapping->i_mmap_rwsem 进行保护,而匿名映射的VMA没有任何保护####。
1.2.1 __vma_link()
static void __vma_link(struct mm_struct *mm, struct vm_area_struct *vma, struct vm_area_struct *prev, struct rb_node **rb_link, struct rb_node *rb_parent) //mmap.c { /* 按起始地址递增的次序插入 mm->mmap 链表 */ __vma_link_list(mm, vma, prev, rb_parent); /* 按起始地址递增的次序插入 mm->mm_rb 红黑树 */ __vma_link_rb(mm, vma, rb_link, rb_parent); } /* 若是传了prev,直接插入到 prev->vm_next 位置; 若prev传NULL表示插入的是首个vma */ void __vma_link_list(struct mm_struct *mm, struct vm_area_struct *vma, struct vm_area_struct *prev, struct rb_node *rb_parent) { struct vm_area_struct *next; /* 链表的插入,此vma节点的前向节点指向prev, prev的next改为指向本vma, 本vma->next指向之前prev->next */ vma->vm_prev = prev; if (prev) { next = prev->vm_next; prev->vm_next = vma; } else { /* 若是VMA链表中没有前向节点(待插入VMA起始地址最小), 直接由 mm->mmap 这个链表头指向 */ mm->mmap = vma; /* 若存在起始地址比此VMA大的VMA, 后向节点指向它 */ if (rb_parent) next = rb_entry(rb_parent, struct vm_area_struct, vm_rb); else next = NULL; } vma->vm_next = next; /* next的prev指向新插入的VMA */ if (next) next->vm_prev = vma; } void __vma_link_rb(struct mm_struct *mm, struct vm_area_struct *vma, struct rb_node **rb_link, struct rb_node *rb_parent) { /* 向上传导更新后向节点的增强值 rb_subtree_gap */ if (vma->vm_next) vma_gap_update(vma->vm_next); else mm->highest_vm_end = vm_end_gap(vma); mm_rb_write_lock(mm); //未使能 CONFIG_SPECULATIVE_PAGE_FAULT 是空实现 rb_link_node(&vma->vm_rb, rb_parent, rb_link); vma->rb_subtree_gap = 0; vma_gap_update(vma); //传导更新增强值 rb_subtree_gap vma_rb_insert(vma, mm); //插入 mm->mm_rb 这颗增强红黑树中 mm_rb_write_unlock(mm); //未使能 CONFIG_SPECULATIVE_PAGE_FAULT 是空实现 }
1.2.2 __vma_link_file()
static void __vma_link_file(struct vm_area_struct *vma) { struct file *file; /* 若是文件映射的VMA才执行 */ file = vma->vm_file; if (file) { struct address_space *mapping = file->f_mapping; /* 若此VMA映射的文件不允许写入, inode->i_writecount-- */ if (vma->vm_flags & VM_DENYWRITE) atomic_dec(&file_inode(file)->i_writecount); /* 若是文件共享映射产生的VMA, 加加 */ if (vma->vm_flags & VM_SHARED) atomic_inc(&mapping->i_mmap_writable); flush_dcache_mmap_lock(mapping); //空实现(没有宏控制) /* 文件映射的VMA,以 vma->vm_pgoff 为key, 插入 file->f_mapping->i_mmap 区间树 */ vma_interval_tree_insert(vma, &mapping->i_mmap); flush_dcache_mmap_unlock(mapping); //空实现(没有宏控制) } }
文件映射的VMA,以 vma->vm_pgoff 为 key, 插入 file->f_mapping->i_mmap 区间树。
3. 调用路径:
arch_setup_additional_pages //vdso.c 持 mm->mmap_sem 写锁后调用 __setup_additional_pages //vdso.c _install_special_mapping //mmap.c install_special_mapping //mmap.c //没有调用位置 __install_special_mapping //mmap.c __bprm_mm_init //exec.c 持 mm->mmap_sem 写锁后调用 insert_vm_struct(mm, vma) //mmap.c
看起来是通过 mm->mmap_sem 这把内存大锁来保护匿名VMA的链表和红黑树的插入操作。
三、VMA移除
1. 相关函数
/* 还有一个 vma_rb_erase_ignore, 只是多出在校验时忽略一个VMA节点 */ static __always_inline void vma_rb_erase(struct vm_area_struct *vma, struct mm_struct *mm) //mmap.c { /* 默认空实现,若使能 CONFIG_DEBUG_VM_RB, 会验证每个vma节点的 vma->rb_subtree_gap 都是正确的 */ validate_mm_rb(&mm->mm_rb, vma); /* 从红黑树 mm->mm_rb 中移除 */ __vma_rb_erase(vma, mm); } static void __vma_rb_erase(struct vm_area_struct *vma, struct mm_struct *mm) { struct rb_root *root = &mm->mm_rb; mm_rb_write_lock(mm); //空实现 /* 将节点从红黑树中移除 */ rb_erase_augmented(&vma->vm_rb, root, &vma_gap_callbacks); mm_rb_write_unlock(mm); //空实现 /* wmb */ RB_CLEAR_NODE(&vma->vm_rb); //vma->vm_rb.__rb_parent_color = &vma->vm_rb; }
看上面只是从 mm->vm_rb 红黑树中移除,并没有从 mm->mmap 链表和 address_space->i_mmap/anon_vma->rb_root 区间树中移除。
这个earse操作也是没有任何锁保护的。
1.1 detach_vmas_to_be_unmapped()
/* * 从 mm 的 VMA 链表/红黑树中,摘下一段 [vma, end) 范围内的连续 VMA, * 形成一条“待 unmap 的独立子链表”。 * * 入参语义: * - mm : 目标进程地址空间 * - vma : 待摘除区间的起始 VMA(通常是包含 unmap 起点的那个) * - prev : vma 在原链表中的前驱(若 vma 是头结点则为 NULL) * - end : 终止地址,循环会摘掉所有 vm_start < end 的 VMA * * 返回值语义: * - true : 后续可安全尝试降级 mmap_lock(写锁 -> 读锁) * - false : 不应降级(邻接可增长 VMA,存在竞态风险) */ static bool detach_vmas_to_be_unmapped(struct mm_struct *mm, struct vm_area_struct *vma, struct vm_area_struct *prev, unsigned long end) { struct vm_area_struct **insertion_point; struct vm_area_struct *tail_vma = NULL; /* * insertion_point 指向“原链表中,这段被摘除区间之前的 next 指针位置”: * - 若有前驱 prev,则是 &prev->vm_next * - 若无前驱,说明从头摘,位置就是 &mm->mmap * 之后会把它改接到“摘除区间之后的第一个保留 VMA”。 */ insertion_point = (prev ? &prev->vm_next : &mm->mmap); //返回的是二级指针 /* 被摘下来的子链表会成为“独立链”,其头结点前驱应置空 */ vma->vm_prev = NULL; /* * 循环摘除所有 vm_start < end 的 VMA: 从 mm->mm_rb 红黑树删除; map_count 递减; 向后推进, * 直到遇到 >= end 的第一个 VMA 或链表结束. * 这里先移除一个,然后再判断next,条件成立才继续移除。 */ do { /* 只是从 mm->vm_rb 红黑树中移除 */ vma_rb_erase(vma, mm); /* VMA个数计数值递减 */ mm->map_count--; tail_vma = vma; /* 上面只是将vma从树上删除,链表还没动,还可以继续用来遍历下一个VMA */ vma = vma->vm_next; } while (vma && vma->vm_start < end); /* 把原链表前半段直接接到剩余后半段 */ *insertion_point = vma; /* 截取下去一段后,尾端链表还有VMA不是空, 说明截下去的是中间那一段,则需更新尾段的前向指针 */ if (vma) { vma->vm_prev = prev; /* * 更新 vma->rb_subtree_gap这个增强值, 红黑树上移除会自动传导更新,这里还要还显式调用 * 的原因是 * vma_gap_callbacks_compute_max --> vma_compute_gap 中计算vam的 gap 值时,用到了链表 * 中的vma->vm_prev 前向节点,这里更改了前向节点。 * 这里更改了前向节点的指向,这种变化并不一定天然落在“被删节点的树路径更新范围”内。所 * 以必须显式刷新。CONFIG_DEBUG_VM_RB 使能后能dbeug这块。 * 补充一点:红黑树只负责“存放和传播这个增强值”,但这个增强值值本身怎么计算,不是由红 * 黑树决定,而是由vma链表邻接关系决定的。 */ vma_gap_update(vma); } else { /* 若截下去的是后半段,则需要更新最大虚拟结束地址 */ mm->highest_vm_end = prev ? vm_end_gap(prev) : 0; } /* 截下去的子段链表next要设为NULL,成为 参数vma--tail_vma 的独立链表 */ tail_vma->vm_next = NULL; /* Kill the cache */ /* 执行 mm->vmacache_seqnum++ 让VMA小cache失效, 防止后续命中已过期的 VMA 指针 */ vmacache_invalidate(mm); /* * Do not downgrade mmap_lock if we are next to VM_GROWSDOWN or * VM_GROWSUP VMA. Such VMAs can change their size under * down_read(mmap_lock) and collide with the VMA we are about to unmap. */ /* * mmap_lock 是否允许降级的安全判断: * 如果摘除区间紧邻可自动扩展的 VMA(栈向下/向上增长),在仅持有读锁时这些 VMA 可能改变边界 * 并与待 unmap 区间冲突,因此必须保持更强同步,不允许降级。 */ if (vma && (vma->vm_flags & VM_GROWSDOWN)) return false; if (prev && (prev->vm_flags & VM_GROWSUP)) return false; return true; }
上面需要手动调用 vma_gap_update(vma) 的实验说明:
假设最初 mm->vm_rb 树状态如下:
C(300-350, rsg=50) / \ A(30-200, rsg=30) D(400-450, rsg=50) \ B(220-260, rsg=20)
对应的 mm->mmap 链表顺序是 A[30, 200) -> B[220, 260) -> C[300, 350) -> D[400, 450)
若从红黑树上删除B节点,但是由于树型没有变化,剩余树节点的 rb_subtree_gap(rsg) 值保持不变。当然在删除B节点时增强型红黑树会向上传导更新 rsg 值,但是此时B节点只是从红黑树中移除了,还没有从链表中移除,此时即使自动传导更新了,各
节点的 rsg 值也不会变化。
此时从链表中删除B节点,然后显示调用 vma_gap_update(vma-C) 手动传导更新一次 rsg 值,此时 vma_gap_update() -->vma_gap_callbacks_propagate() --> vma_gap_callbacks_compute_max() --> vma_compute_gap() 中会根据变化后的链表,
修正C节点自身的gap值,修正后为:
C(300-350, rsg=100) / \ A(30-200, rsg=30) D(400-450, rsg=50)
这块比较绕,建议改动后使能 CONFIG_DEBUG_VM_RB 进行调试校验。
2. 调用路径
mremap_to //mremap.c SYSCALL_DEFINE5(mremap //持当前任务的 mm->mmap_sem 写锁后调用 move_vma //mremap.c SYSCALL_DEFINE5(mremap //持当前任务的 mm->mmap_sem 写锁后调用 mremap_to //mremap.c aio_setup_ring //aio.c 持当前任务的 mm->mmap_sem 写锁后调用 do_shmat //shm.c 持当前任务的 mm->mmap_sem 写锁后调用 vm_mmap_pgoff //util.c 持当前任务的 mm->mmap_sem 写锁后调用 get_unmapped_area //mmap.c SYSCALL_DEFINE5(remap_file_pages //mmap.c 持当前任务的 mm->mmap_sem 写锁后调用 do_mmap_pgoff //mm.h do_mmap //mmap.c mmap_region //mmap.c SYSCALL_DEFINE1(brk //mmap.c 持当前任务的 mm->mmap_sem 写锁后调用 vm_brk_flags //mmap.c 持当前任务的 mm->mmap_sem 写锁后调用 do_brk_flags //mmap.c ksys_shmdt //shm.c 持当前任务的 mm->mmap_sem 写锁后调用 remap_oldmem_pfn_checked //vmcore.c mmap_vmcore //vmcore.c do_munmap //mmap.c __vm_munmap //持当前任务的 mm->mmap_sem 写锁后调用 SYSCALL_DEFINE1(brk //持当前任务的 mm->mmap_sem 写锁后调用 SYSCALL_DEFINE5(mremap //mremap.c 持当前任务的 mm->mmap_sem 写锁后调用 __do_munmap //mmap.c detach_vmas_to_be_unmapped(mm, vma, prev, end) //mmap.c TODO: 看其实现 vma_rb_erase(vma, mm)
四、VMA移除补充
上面只是从 mm->vm_rb 红黑树中移除,并没有从 mm->mmap 链表和 address_space->i_mmap/anon_vma->rb_root 区间树中移除,明显是不足的。这里从 __vma_unlink_common() 开始看。
1. 相关函数
/* * 低层 VMA unlink 原语,只做"摘节点"这一件事:1. 从红黑树摘除; 2. 修复链表指针. * 但是不负责 vma_gap_update —— 那是调用者的职责。 * * 参数:ignore 仅用于 DEBUG 阶段的 validate_mm_rb(),跳过对它的 rsg 验证,因为 erase * 进行中它的值是临时不一致的。若不使能调试此参数无效。 */ static __always_inline void __vma_unlink_common(struct mm_struct *mm, struct vm_area_struct *vma, struct vm_area_struct *prev, bool has_prev, struct vm_area_struct *ignore) { struct vm_area_struct *next; /* 第1步:从红黑树摘除。[4-1-1] */ vma_rb_erase_ignore(vma, mm, ignore); next = vma->vm_next; /* * 第2步:修复链表 vm_next 指针。 * has_prev=true 表示调用者已经知道 prev 是谁,直接用, false 时从 vma->vm_prev 里取。 */ if (has_prev) prev->vm_next = next; else { /* 判断待删除VMA是否有前向节点,若没有直接让 mm->mmap 这个链表头指向 */ prev = vma->vm_prev; if (prev) prev->vm_next = next; else mm->mmap = next; } if (next) next->vm_prev = prev; /* 使 per-task VMA 小缓存(vmacache)失效,防止命中过期指针 */ vmacache_invalidate(mm); //mm->vmacache_seqnum++; }
如下面VMA红黑树,删除的是B节点:
C(300-350, rsg=50) / \ A(30-200, rsg=30) D(400-450, rsg=50) \ B(220-260, rsg=20)
注[4-1-1]: vma_rb_erase_ignore() 内部调用 rb_erase_augmented(),会沿删除路径向上 propagate,重算路径上每个祖先的 rb_subtree_gap。 但关键是,此时链表还没改,C.vm_prev 仍然指向 B,所以 propagate 时 vma_compute_gap(C) 算出的是旧 gap(300-260=40),C.rsg 此时 = max(40, A.rsg, D.rsg) = 50(暂时错了)。但这没问题 —— 调用者稍后会显式修正,此列中修正位置在外层函数 __vma_adjust() --> vma_gap_update(next) 中。
这里是从红黑树和链表的删除,没有从区间树中删除的逻辑,没有区分文件VMA和匿名VMA。也没有任何锁保护。
可以看到,内核中无论是删除一段VMA,还是删除单个VMA,都是先从红黑树上删除(增强字段暂时不正确),然后再从链表上删除(然后再显式更新增强值)。
2. 调用路径
__vma_unlink_common 调用路径:
vma_merge //mm.h copy_vma //mmap.c __vma_merge //mmap.c __split_vma //mmap.c SYSCALL_DEFINE5(mremap //mremap.c shift_arg_pages //exec.c vma_adjust //mm.h __vma_adjust //mmap.c __vma_unlink_prev __vma_unlink_common(mm, vma, prev, true, vma) __vma_adjust //mmap.c 调用路径如上 __vma_unlink_common(mm, next, NULL, false, vma)
五、VMA查找
1. 相关函数
/* 查找第一个满足 vma->vm_end > addr 的VMA,addr可能落在VMA区间了,也可能落在其前面的空洞里 */ struct vm_area_struct *find_vma(struct mm_struct *mm, unsigned long addr) //mmap.c /* * 同 find_vma, 除了查找addr对应的VMA外(首个vm_end>addr的VMA),还返回这个VMA在链表中的前向节点, * 若addr对应的VMA不存在,则返回vma_start最大(最后)的那个VMA. */ struct vm_area_struct * find_vma_prev(struct mm_struct *mm, unsigned long addr, struct vm_area_struct **pprev) //mmap.c /* 查找首个与 [start_addr, end_addr) 相交的VMA, 若不存在相交的VMA则返回NULL */ static inline struct vm_area_struct * find_vma_intersection(struct mm_struct * mm, unsigned long start_addr, unsigned long end_addr) //mm.h /* 要求查到与区间[vm_start,vm_end)完全匹配的VMA, 即start=start, end=end, 否则返回NULL */ static inline struct vm_area_struct *find_exact_vma(struct mm_struct *mm, unsigned long vm_start, unsigned long vm_end) //mm.h /* * 查找包含 addr 的 VMA, 若 addr 落在某个可向下扩展的栈 VMA 前面的空洞里, * 则尝试把该 VMA 的 vm_start 向下扩展,使 addr 被覆盖。 */ struct vm_area_struct * find_extend_vma(struct mm_struct *mm, unsigned long addr)
2. 函数实现
下面依 find_vma() 为例,看其实现:
/* 注意: 返回的vma不一定包含 addr,只是返回首个 vma->vma_end > addr 的vma,也可能落在空洞里 */ struct vm_area_struct *find_vma(struct mm_struct *mm, unsigned long addr) //已chat { struct vm_area_struct *vma; /* 先查 vmacache。它记录了当前线程最近访问过的几个 VMA,命中缓存是更常见 */ vma = vmacache_find(mm, addr); if (likely(vma)) return vma; /* cache 未命中,则走红黑树/主结构查找。 */ vma = __find_vma(mm, addr); if (vma) /* 找到后直接覆盖更新vmacache,方便下次快速命中 */ vmacache_update(addr, vma); return vma; } /* 在进程 mm 的 VMA 集合中,找到“第一个满足 vm_end > addr 的 VMA” */ static struct vm_area_struct *__find_vma(struct mm_struct *mm, unsigned long addr) { struct rb_node *rb_node; struct vm_area_struct *vma = NULL; rb_node = mm->mm_rb.rb_node; while (rb_node) { struct vm_area_struct *tmp; tmp = rb_entry(rb_node, struct vm_area_struct, vm_rb); /* * 树中的vma的地址区间是互不重叠的. 若 tmp->start <= addr < tmp->end 直接就break退出不再找了, * 就是对应这个vma, 若只满足 addr < tmp->end 则记录下tmp 需要继续找。 */ if (tmp->vm_end > addr) { /* 记录首个 vm_end > addr 的VMA节点 */ vma = tmp; if (tmp->vm_start <= addr) break; rb_node = rb_node->rb_left; } else rb_node = rb_node->rb_right; } return vma; }
比如下面这个红黑树中查找 addr=210 返回的就是B节点:
C(300-350, rsg=50) / \ A(30-200, rsg=30) D(400-450, rsg=50) \ B(220-260, rsg=20)
可以看到这个查找过程也是没有任何锁保护的。
2. 调用路径
系统有海量调用,这里就不列了。
六、VMA调整
vma_adjust
此函数是多种VMA操作类型调用的公共函数,例如 split、merge 等。
1. 相关函数
/* 将 vma 边界调整为 [start, end), 并把 insert 这个 new vma 插入 mm 的链表/红黑树并维护增强信息。*/ static inline int vma_adjust(struct vm_area_struct *vma, unsigned long start, unsigned long end, pgoff_t pgoff, struct vm_area_struct *insert) { return __vma_adjust(vma, start, end, pgoff, insert, NULL, false); //insert 传的是新分配出来的vma }
1.1.1 __vma_adjust
(1) 函数注释
/* * 函数作用: * 将 vma 边界调整为 [start, end), 并把 new(==insert) 插入 mm 的链表/红黑树并维护增强信息。#### * 在不直接搬页表内容的前提下,安全地调整一个或多个相邻 VMA 的边界、页偏移和连接关系,并同步维护所有相关索引与反向映射结构。 * 它主要解决的是“VMA 形状变了,内核里所有引用它的结构都要一起跟着变”的问题。 * * (1) split插入场景 * split_vma: (vma, start, end, pgoff, insert=new, NULL, false) //inserit有值,就是new * * (2) adjust * vma_adjust: (vma, start, end, pgoff, insert, NULL, false); * * (3) merge: * __adjust_vma 的 merge的需要对照下面这个图和参数看:#### * * AAAA AAAA AAAA AAAA * PPPPPPNNNNNN PPPPPPNNNNNN PPPPPPNNNNNN PPPPNNNNXXXX * 无法合并 可能变为 可能变为 可能变为 * PPNNNNNNNNNN PPPPPPPPPPNN PPPPPPPPPPPP 6 或 * mmap、brk 或 对应下方 case4 对应下方 case5 PPPPPPPPXXXX 7 或 * mremap move: PPPPXXXXXXXX 8 * AAAA * PPPP NNNN PPPPPPPPPPPP PPPPPPPPNNNN PPPPNNNNNNNN * 可能变为 对应下方 case1 对应下方 case2 对应下方 case3 * * 只有只要一个边界对齐才能merge,一共就这8种可能。是将A依次向右移得来的。 * * //case 1, 6, 左右都可以合并。把 prev 的右边界扩到 next->vm_end,并把 next 删除####。 * __vma_merge: (prev, prev->vm_start, next->vm_end, prev->vm_pgoff, NULL, prev, keep_locked); * //case 2, 5, 7, 只能左侧合并,右侧不可合并。把 prev 的右边界扩展到 end。 * __vma_merge: (prev, prev->vm_start, end, prev->vm_pgoff, NULL, prev, keep_locked); * //case4: 先把 prev 截断到 addr,剩下的再由 next 向左接管####。 *__vma_merge: (prev, prev->vm_start, addr, prev->vm_pgoff, NULL, next, keep_locked); * //对应 case 3, 8: 由 area(上图中的N) 触发调整,把 next(case3是N,case8是X) 向左扩展到 addr: * __vma_merge: (area, addr, next->vm_end, next->vm_pgoff - pglen, NULL, next, keep_locked); * * __vma_adjust 中的 next 重新取值了, 取的是第一个参数的 next。 */
(2) 函数内容
int __vma_adjust(struct vm_area_struct *vma, unsigned long start, unsigned long end, pgoff_t pgoff, struct vm_area_struct *insert, struct vm_area_struct *expand, bool keep_locked) { struct mm_struct *mm = vma->vm_mm; /* 双向循环链表,即使只有一个元素,next也不为NULL吧 */ struct vm_area_struct *next = vma->vm_next, *orig_vma = vma; struct address_space *mapping = NULL; struct rb_root_cached *root = NULL; struct anon_vma *anon_vma = NULL; struct file *file = vma->vm_file; bool start_changed = false, end_changed = false; long adjust_next = 0; int remove_next = 0; /* * vm_raw_write_begin/end 是给“推测缺页路径”看的版本序列保护,避免 lockdep 在 vm_sequence * 与 i_mmap_rwsem/fs_reclaim 链路上报理论死锁。这里我们只做版本变化标记,不在 vm_sequence 上阻塞等待。 * * 默认不使能 CONFIG_SPECULATIVE_PAGE_FAULT 是空实现。 */ vm_raw_write_begin(vma); if (next) vm_raw_write_begin(next); /* * 预判本次调整与 next 的关系(仅在“不是 split 插入”时): * 1) 完全覆盖 next (可能还覆盖 next->next) * 2) 部分覆盖 next (边界上移) * 3) vma 缩小导致与 next 的分界下移 * 同时处理 anon_vma 的“导出者/导入者”关系,确保边界移动后 rmap 一致。 * * split 场景 insert != NULL, merge 场景 insert == NULL */ if (next && !insert) { struct vm_area_struct *exporter = NULL, *importer = NULL; /* merge: 可合并且满足这个条件, 对应 case 1 6 7 8 */ if (end >= next->vm_end) { /* * 翻译: vma 扩展,覆盖 next 的所有部分,也可能覆盖 next 后面的(mprotect case 6), * 其它能走到这里的情况是case 1 7 8. 它这个注释写到下面了。 * merge: 单看这个,case 3 4 8 都满足,叠加上面条件,只有 case 8 满足。 */ if (next == expand) { /* 翻译: 唯一不展开“vma”而是展开“next”的情况是 case 8。 * case 8 是成立的,不会触发鉴权。*/ VM_WARN_ON(end != next->vm_end); /* 翻译: 将 remove_next 赋值为 3 表示我们要删除“vma”,为此我们交换了“vma” 和 “next”####????。*/ remove_next = 3; VM_WARN_ON(file != next->vm_file); /* 直接是vma的内容互换 */ swap(vma, next); } else { VM_WARN_ON(expand != vma); /* * 翻译: case 1, 6, 7 中 remove_next=2 的是 case 6, remove_next=1的是 case 1, 7 * * merge: case 1 6 7 走这里,case 6 remove_next 赋值为 2, case 1 7 remove_next 赋值为 1 */ remove_next = 1 + (end > next->vm_end); VM_WARN_ON(remove_next == 2 && end != next->vm_next->vm_end); //case 6 可鉴权通过 VM_WARN_ON(remove_next == 1 && end != next->vm_end); //case1 7 可鉴权通过 /* * 翻译: 对于 case 6 的第一遍, 修剪 end 到 next */ * merge: 对于 case 1 7 相当于空操作,对于 case 6 相当于把 end 缩小了, */ end = next->vm_end; } /* merge: case 1 6 7 8 都进行这个赋值,case 8 比较特殊,两个都后移了一个VMA */ exporter = next; importer = vma; /* * 翻译: 如果 next 中没有 anon_vma,如果 vma 与其重叠,则从 next 之后的 vma 导入。 * * merge: case 6 里 next 可能没有 anon_vma,需要从 next->next 导入。 */ if (remove_next == 2 && !next->anon_vma) exporter = next->vm_next; /* merge: case 5 是成立的 */ } else if (end > next->vm_start) { /* * 翻译: vma 扩展,与 next 部分重叠:mprotect case 5 将边界向上移动。 * * merge: 只覆盖 next 的前半段(case 5):next 的左边界需要向右移 adjust_next 页。 */ adjust_next = (end - next->vm_start) >> PAGE_SHIFT; exporter = next; importer = vma; VM_WARN_ON(expand != importer); /* merge: case 4 是成立的 */ } else if (end < vma->vm_end) { /* * 翻译: vma 缩小了,!insert 表示它不是 split_vma 插入另一个:所以一定是 * mprotect case 4 将边界向下移动。 * * merge: 取负数,表示要缩小 prev, 表示 next 的左边界左移,覆盖prev的一部分。 */ adjust_next = -((vma->vm_end - end) >> PAGE_SHIFT); exporter = vma; //被缩小者是 prev importer = next; //被放大者是 next VM_WARN_ON(expand != importer); //case 4 满足鉴权 } /* * 翻译: 容易被忽略:当 mprotect 移动边界时,如果缩小的 vma 有 anon_vma 设置,则 * 确保扩展的 vma 也有 anon_vma 设置,以覆盖导入的任何匿名页面。 * * 边界移动时,若“导入者”还没有 anon_vma,而“导出者”有,需要克隆链路,避免匿名页 rmap 断裂。 */ if (exporter && exporter->anon_vma && !importer->anon_vma) { int error; importer->anon_vma = exporter->anon_vma; /* * 把 src 的 anon_vma_chain 复制到 dst。复制的是一批 anon_vma_chain, * 让 dst VMA 继承 src VMA 的 anon_vma 关系网络 */ error = anon_vma_clone(importer, exporter); if (error) { if (next && next != vma) /* 默认不使能 CONFIG_SPECULATIVE_PAGE_FAULT, 是空实现 */ vm_raw_write_end(next); vm_raw_write_end(vma); return error; } } } again: /* 先处理 THP 相关边界调整(避免后续对齐/拆分问题), 默认不使能 CONFIG_TRANSPARENT_HUGEPAGE 空实现 */ vma_adjust_trans_huge(orig_vma, start, end, adjust_next); /* * 文件映射场景:准备 i_mmap 区间树的更新和 uprobe 通知。注意顺序:先 uprobe_munmap,再改树, * 再 uprobe_mmap。这是公共路径,split走,若参数vma是文件映射VMA。 */ if (file) { mapping = file->f_mapping; root = &mapping->i_mmap; //文件映射的VMA区间树根节点 uprobe_munmap(vma, vma->vm_start, vma->vm_end); //以后看 if (adjust_next) //split路径传0, merge路径表示next要扩展或收缩的页 uprobe_munmap(next, next->vm_start, next->vm_end); i_mmap_lock_write(mapping); //持 mapping->i_mmap_rwsem 写锁 if (insert) { /* * 翻译: 现在放入区间树中,这样实例化的页面对 arm/parisc __flush_dcache_page 就可见; * 但是,在 vma 开始或结束更新之前,我们不能将其插入地址空间。 * * split 场景:先把 insert 挂进文件映射区间树,便于某些架构 dcache 刷新看到完整页实例。 */ __vma_link_file(insert); } } /* anon_vma 区间树更新前置:先从 anon_vma 树里摘下要改边界的 vma/next,改完再挂回去。*/ anon_vma = vma->anon_vma; if (!anon_vma && adjust_next) anon_vma = next->anon_vma; if (anon_vma) { VM_WARN_ON(adjust_next && next->anon_vma && anon_vma != next->anon_vma); anon_vma_lock_write(anon_vma); //持 anon_vma->root->rwsem 写锁 /* 遍历 vma->anon_vma_chain 将上面的所有AVC从区间树上删除 */ anon_vma_interval_tree_pre_update_vma(vma); if (adjust_next) /* 若 next 的边界需要调整的话,遍历 next->anon_vma_chain 将上面的所有AVC从区间树上删除 */ anon_vma_interval_tree_pre_update_vma(next); } /* 文件映射区间树也先摘(root 来自上面 file->f_mapping->i_mmap),再改字段,再插回。 */ if (root) { flush_dcache_mmap_lock(mapping); //直接空实现 /* 文件映射就只移除这一个vma就行 */ vma_interval_tree_remove(vma, root); /* 若 next 的边界需要调整的话,next 也需要从VMA区间树上移除 */ if (adjust_next) vma_interval_tree_remove(next, root); } /* 真正修改主 VMA 边界与偏移, pgoff是参数传进来的要调整到的值 */ if (start != vma->vm_start) { WRITE_ONCE(vma->vm_start, start); start_changed = true; } if (end != vma->vm_end) { WRITE_ONCE(vma->vm_end, end); end_changed = true; } WRITE_ONCE(vma->vm_pgoff, pgoff); /* 若next也需要调整(如merge路径),改 next 的起点和文件偏移, adjust_next 可正可负 */ if (adjust_next) { WRITE_ONCE(next->vm_start, next->vm_start + (adjust_next << PAGE_SHIFT)); WRITE_ONCE(next->vm_pgoff, next->vm_pgoff + adjust_next); } /* 若是文件映射VMA,重新将其插回区间树 */ if (root) { if (adjust_next) vma_interval_tree_insert(next, root); vma_interval_tree_insert(vma, root); flush_dcache_mmap_unlock(mapping); //空实现 } /* * 三选一结构操作:A) remove_next: merge 后删除 next; B) insert: split 后插入新 VMA * C) 仅边界变化:更新 gap 增强信息 * * merge: remove_next 不为0, 对应case 1 6 7 8, 其中 case 8 remove_next==3, * case 6 remove_next==2, case 1 7 remove_next==1 */ if (remove_next) { /* * 翻译: vma_merge() 已将 next 合并到 vma 中,需要在释放锁之前删除 next。 * * merge: case 1 6 7 走这里, 这三种情况下 next 都被 prev 吸收了,把 next 从将VMA红黑树和链表中删除。 */ if (remove_next != 3) __vma_unlink_prev(mm, next, vma); else /* * 翻译: 如果它们已被交换,则 vma 不位于 next 之前。pre-swap() next->vm_start 的值已减小, * 因此请告知 validate_mm_rb 忽略 pre-swap() "next"(该值存储在 post-swap() "vma" 中)。 * * merge: case 8 走这里,remove_next==3 时使用 swap 语义对应的特殊 unlink。 */ __vma_unlink_common(mm, next, NULL, false, vma); /* 从VMA区间树中删除 next 这个VMA */ if (file) __remove_shared_vm_struct(next, file, mapping); } else if (insert) { /* * 翻译: split_vma 包含来自 vma 的拆分部分的插入,需要我们在释放锁之前插入它(它可以 * 在 vma 之后插入,也可以在 vma 之前插入)。 * * split 场景:把 insert 这个分离出来的vma 插入VMA链表和红黑树,并更新增强值. */ __insert_vm_struct(mm, insert); } else { /* * 仅边界调整场景:维护增强 gap。 * start 变了:当前 vma gap 可能变化; end 变了:可能影响 next 的前向 gap 或 highest_vm_end * * merge: remove_next==0 对应 case 2 3 4 5, 其中 start_changed 对应 case 3, 它会传导更新gap值 */ if (start_changed) vma_gap_update(vma); /* merge: case 2 4 5 成立, 刚好都属于2345的子集 */ if (end_changed) { /* case: 2 4 5 的next应该都不为NULL,都不执行 */ if (!next) mm->highest_vm_end = vm_end_gap(vma); /* merge: 除了case 4 5都成立,结合上面条件取交集只有 case 2 成立 */ else if (!adjust_next) vma_gap_update(next); } } /* anon_vma区间树后置恢复 */ if (anon_vma) { /* 在将 vma->anon_vma_chain 上的所有AVC全部加回 AVC 区间树中 */ anon_vma_interval_tree_post_update_vma(vma); if (adjust_next) anon_vma_interval_tree_post_update_vma(next); anon_vma_unlock_write(anon_vma); //释放 anon_vma->root->rwsem 的写锁。 } /* 若是文件映射VMA */ if (mapping) i_mmap_unlock_write(mapping); //释放 mapping->i_mmap_rwsem 写锁 /* 若是文件映射,还要更新uprobe的 */ if (root) { uprobe_mmap(vma); //TODO: 日后再看 if (adjust_next) uprobe_mmap(next); } /* * 真正释放 remove_next 对象(若有):uprobe 清理; anon_vma 合并; map_count--; put_vma(next) * * merge: remove_next 不为0, 对应case 1 6 7 8, 其中 case 8 remove_next==3, * case 6 remove_next==2, case 1 7 remove_next==1 */ if (remove_next) { if (file) uprobe_munmap(next, next->vm_start, next->vm_end); if (next->anon_vma) /* 从VMA中卸载所有关联的 anon_vma 节点 */ anon_vma_merge(vma, next); mm->map_count--; vm_raw_write_end(next); put_vma(next); /* * 翻译: 在 mprotect 的 case 6 中(参见 vma_merge 的注释),我们也必须移除另一个 next。 * 如果一次性完成两项操作,代码会变得过于臃肿。 * * merge: case 6 需要删除两个 next,分两轮完成####。 */ if (remove_next != 3) { /* * 翻译: 如果“next”被移除,并且 vma->vm_end 向上扩展覆盖了它, * 那么“next->vm_prev->vm_end”也会随之改变,“vma->vm_next”之间的差距必须更新。 */ next = vma->vm_next; if (next) vm_raw_write_begin(next); //空实现 } else { /* * 翻译: 对于注释“next”和“vma”在swap()之前的范围:如果“vma”被移除,next->vm_start * 会向下扩展覆盖它,“next”的间隙必须更新。由于 swap(),swap() 之后的“vma”实际上指向 swap() * 之前的“next”(而swap()之后的“next”现在是一个悬空指针)。 * * swap 特例下,逻辑“下一项”就是当前 vma。 */ next = vma; } /* merge: 对应 case 6, 设置为1后重新走了一遍 */ if (remove_next == 2) { /* 第二轮删除 */ remove_next = 1; end = next->vm_end; goto again; } else if (next) { /* 删除后补一次 gap 增强更新 */ vma_gap_update(next); } else { /* * 翻译: * 如果 remove_next == 2,显然我们无法到达此路径。 * 如果 remove_next == 3,我们也无法到达此路径,因为 pre-swap() 的 next 始终不为 NULL。 * pre-swap() 的“next”不会被移除,其 next->vm_end 也不会被修改(此外,在 remove_next == 3 的情况下, * “end”已经与 next->vm_end 匹配)。 * 只有在 remove_next == 1 的情况下,如果被移除的“next”vma 是 mm 中最高的 vma,我们才能到达此路径。 * 然而,在这种情况下,next->vm_end == “end”,并且扩展后的“vma”的 vma->vm_end == next->vm_end,因此 * 在 remove_next == 1 的情况下,mm->highest_vm_end 不需要任何更新。 * * 没有 next 时,highest_vm_end 理论上应已正确。 */ VM_WARN_ON(mm->highest_vm_end != vm_end_gap(vma)); } } /* split + file 即split文件页映射的VMA场景,还要拆分的这个 insert vma执行 uprobe_mmap */ if (insert && file) uprobe_mmap(insert); /* 结束 vm_raw_write 保护 */ if (next && next != vma) vm_raw_write_end(next); if (!keep_locked) /* 没有使能 CONFIG_SPECULATIVE_PAGE_FAULT 默认空实现 */ vm_raw_write_end(vma); /* 调试校验: 链表、rb树、gap增强值一致性 */ validate_mm(mm); return 0; }
2. 调用路径
__split_vma //mmap.c 见split调用路径 SYSCALL_DEFINE5(mremap //mremap.c elf_format.load_binary //binfmt_elf.c load_elf_binary //binfmt_elf.c setup_arg_pages //binfmt_elf.c shift_arg_pages //exec.c vma_adjust //mm.g __vma_merge //mmap.c 见merge的调用路径 __vma_adjust //mmap.c
七、VMA拆分
1. 相关函数
/* * 将 vma 在地址 addr 处分成两部分形成两个VMA,new_below 参数决定将低地址段(真)还是高地址 * 段(假)分给这个新的VMA */ int split_vma(struct mm_struct *mm, struct vm_area_struct *vma, unsigned long addr, int new_below) { /* 每个进程的VMA还有最大次数限制,默认 cat /proc/sys/vm/max_map_count 是 65530 */ if (mm->map_count >= sysctl_max_map_count) return -ENOMEM; return __split_vma(mm, vma, addr, new_below); }
1.1 __split_vma
/* * 将一个现有 VMA 在 addr 处分裂成两个 VMA。其中参数 addr 是分裂点(要求也必须要页对齐)#### * 参数 new_below 控制新分配的 VMA 放哪一半: * -为真, new 表示 [old_start, addr) 前(低)半, vma 调整为 [addr, * old_end) 后(高)半。 * -为假,new 表示 [addr, old_end) 后(高)半, vma 调整为 [old_start, addr) 前(低)半。 * * 成功返回0失败返回 -ENOMEM 或错误码。 */ int __split_vma(struct mm_struct *mm, struct vm_area_struct *vma, unsigned long addr, int new_below) { struct vm_area_struct *new; int err; /* 先给底层映射类型一个感知“即将分裂”的机会。某些驱动/文件映射有私有元数据,需要在 split 前做一致性处理。*/ if (vma->vm_ops && vma->vm_ops->split) { err = vma->vm_ops->split(vma, addr); if (err) return err; } /* 分配一个新VMA,直接值拷贝一份,然后只重新初始化了 anon_vma_chain 这个链表头 */ new = vm_area_dup(vma); if (!new) return -ENOMEM; /* 此参数擦混非0表示要 new 表示 [old_start, addr) 这部分区间,为0new要表示[addr, old_end)这段区间 */ if (new_below) new->vm_end = addr; else { new->vm_start = addr; new->vm_pgoff += ((addr - vma->vm_start) >> PAGE_SHIFT); //vma地址区间是page对其的,不用考虑上下圆整 } /* 没有使能 CONFIG_NUMA 是空实现 */ err = vma_dup_policy(vma, new); if (err) goto out_free_vma; /* 复制/挂接 anon_vma 关系。这样分裂后两半匿名映射的反向映射(rmap)关系保持正确 */ err = anon_vma_clone(new, vma); if (err) goto out_free_mpol; /* 若是文件映射,给 file 增加引用,保证 new 生命周期内文件对象有效。*/ if (new->vm_file) get_file(new->vm_file); //f_count++ /* 有新VMA创建时调用 vm_ops->open,让底层映射为 new 建立每-VMA私有状态。*/ if (new->vm_ops && new->vm_ops->open) new->vm_ops->open(new); /* 调整原 vma 边界, 把 new 插入 mm 的链表/红黑树, 维护 rb_subtree_gap/highest_vm_end 等增强信息 */ if (new_below) /* vma 调整为 [addr, old_end) 后半。 */ err = vma_adjust(vma, addr, vma->vm_end, vma->vm_pgoff + ((addr - new->vm_start) >> PAGE_SHIFT), new); else /* vma 调整为 [old_start, addr) 前半。 */ err = vma_adjust(vma, vma->vm_start, addr, vma->vm_pgoff, new); /* Success. */ if (!err) return 0; /* Clean everything up if vma_adjust failed. 失败回滚信息 */ if (new->vm_ops && new->vm_ops->close) new->vm_ops->close(new); if (new->vm_file) fput(new->vm_file); /* 前面 anon_vma_clone() 中失败回滚也会调用到这个函数 */ unlink_anon_vmas(new); out_free_mpol: mpol_put(vma_policy(new)); out_free_vma: vm_area_free(new); return err; }
1.1.1 anon_vma_clone
/* * 把 src 的 anon_vma_chain 复制到 dst。复制的是一批 anon_vma_chain, 让 dst VMA 继承 * src VMA 的 anon_vma 关系网络, * * 背景:每个 VMA 通过 anon_vma_chain 链表维护一个或多个 anon_vma 。rmap、COW、页回收 * 需要这条关系来从页反查到相关 VMA。在 split/merge/fork 等路径中,新 VMA 需要克隆这套关系。 * * 返回:0 成功,-ENOMEM 内存分配失败(并完成必要回滚) */ int anon_vma_clone(struct vm_area_struct *dst, struct vm_area_struct *src) { struct anon_vma_chain *avc, *pavc; struct anon_vma *root = NULL; /* * 倒序遍历 src->anon_vma_chain, 逐个为 dst 分配新的 anon_vma_chain 节点,并链接到同一个 anon_vma。 * 倒序原因: * 这是内核这条路径的约定顺序,便于后续选择可复用 anon_vma 及保持链接关系的稳定性(不影响语义正确性, * 但减少一些边界复杂度)。 * vma->anon_vma_chain 链表上挂的都是 anon_vma_chain 结构。 */ list_for_each_entry_reverse(pavc, &src->anon_vma_chain, same_vma) { struct anon_vma *anon_vma; /* 先尝试向slab轻量分配,不阻塞,失败时不刷屏告警。失败后再走慢路径 GFP_KERNEL(可睡眠) */ avc = anon_vma_chain_alloc(GFP_NOWAIT | __GFP_NOWARN); if (unlikely(!avc)) { /* 进入可睡眠分配前,先把当前已持有的 anon_vma root 锁释放,避免在内存回收路径里形成锁顺序风险。*/ unlock_anon_vma_root(root); //释放 root->rwsem 写锁 root = NULL; avc = anon_vma_chain_alloc(GFP_KERNEL); if (!avc) goto enomem_failure; } /* 取出 src 当前链节点对应的 anon_vma。 */ anon_vma = pavc->anon_vma; /* * 锁住该 anon_vma 的 root:若和上一次是同一个 root,通常不会反复解/加锁,若切换到另一个 root,会先 * 解旧锁再加新锁。此函数支持“同 root 重入优化”。 * root 变量保存“当前持有的 root 锁的 anon_vma 对象”。 */ root = lock_anon_vma_root(root, anon_vma); //只锁一次,不重复上锁,遍历完后释放一次即可 /* * 把新分配的 avc 挂入 dst->anon_vma_chain(通过 same_vma 成员) 和 anon_vma 的 interval tree(通 * 过 rb 成员)。 * 相当于新建一个avc,avc->vma 由src vma改为指向dst vma, avc->anon_vma 指向不变,此avc挂入到 * dst->anon_vma_chain 中, 然后挂入同一个 anon_vma->rb_root 区间树。这里操作的只是avc, 没有修 * 改dst vma的任何成员。 */ anon_vma_chain_link(dst, avc, anon_vma); /* * 尝试给 dst 选择一个 “可复用的 anon_vma” 作为 dst->anon_vma, 条件解释: * 1) !dst->anon_vma: 只选择一次,首次命中即定。split路径是直接src来的,不一定为NULL. * 2) anon_vma != src->anon_vma: 不选父 anon_vma(避免第一个子总复用父,影响层次结构)。 * 不能复用 src 自己的主 anon_vma。原因:src 还在用它,如果 dst 也用同一个,相当于 * 两个 VMA 共享主 anon_vma,之后任何一方触发 COW 产生的新页都写到同一个反查锚点下, * 会混淆页的"谁的页"语义,也会让 degree 计数不正确。 * 3) anon_vma->degree < 2: 该 anon_vma 连接度较低(通常无直接 vma,仅一个子),复用成 * 本低、共享压力小。 * * root anon_vma 通常不会被选中(有自引用 parent,且至少有一个子)。 */ if (!dst->anon_vma && anon_vma != src->anon_vma && anon_vma->degree < 2) //[1.1.1-1] dst->anon_vma = anon_vma; } /* 若成功选中了 dst->anon_vma,补一份 degree 引用计数。degree 近似反映“该 anon_vma * 的连接度/被引用拓扑复杂度”。*/ if (dst->anon_vma) dst->anon_vma->degree++; /* 正常路径收尾:释放最后持有的 root 锁。 */ unlock_anon_vma_root(root); return 0; enomem_failure: /* * 分配失败回滚:先清 dst->anon_vma,再 unlink_anon_vmas(dst) * 原因:unlink_anon_vmas() 会依据 dst->anon_vma/chain 去做 degree 递减等清理,若这里不先置空, * 可能对“未完整建立”的 dst->anon_vma 做错误递减导致 degree 计数失真。 * 调用者在 clone 失败时本来也不会继续依赖 dst->anon_vma。 */ dst->anon_vma = NULL; /* 把已经成功 link 的部分 avc 全部拆掉,恢复到调用前状态。 */ unlink_anon_vmas(dst); //外层函数也有调用,放在外层讲解 return -ENOMEM; }
注[1.1.1-1]: 为什么选中间层的 anon_vma 是安全的?
关键在于:被选中的那个 anon_vma,已经通过 avc->anon_vma 关联到了 src 的 chain 里,也就意味着它和 src(以及 src 的血缘祖先)属于同一棵 anon_vma 家族树,root 相同,锁一致。所以 dst 用它作为主 anon_vma,天然满足:
(1) 新页产生时归属到这棵家族树,被 root 的锁保护,并发正确。
(2) rmap 从这棵树出发能回溯到历史所有相关 VMA,不会丢失反查路径。
总结起来就是: dst->anon_vma 选哪个节点不影响历史页被找到(靠完整 chain 保障),只影响新页的归属方向;
选 degree < 2 的中间节点是一个性能优化:既复用了已有树结构,避免新分配,又不给任何节点增加过高的"连接压力",让家族树保持可控规模。
1.1.2 vma_adjust
见上面实现。
1.1.3 unlink_anon_vmas
/** * unlink_anon_vmas - 从VMA中卸载所有关联的 anon_vma 节点。 * 功能概述:当VMA被销毁/缩小时,需要从其关联的所有 anon_vma 的区间树中移除。 * 由于涉及RW信号量的写锁和 __put_anon_vma() 的需求冲突,需要分两次遍历:#### * 第一次:持写锁卸载 avc 节点、递减 degree 计数、标记空 anon_vma。 * 第二次:释放写锁后,真正释放空 anon_vma 及其 avc 链表节点。 */ void unlink_anon_vmas(struct vm_area_struct *vma) { struct anon_vma_chain *avc, *next; struct anon_vma *root = NULL; /* * 取消与 VMA 关联的每个 anon_vma。此列表按从新到旧的顺序排列,确保根 anon_vma 最后被释放。 * 头插法,后插入的在链表头。在 fork/COW 血缘里,链表越尾越接近 root 那一侧。 */ list_for_each_entry_safe(avc, next, &vma->anon_vma_chain, same_vma) { struct anon_vma *anon_vma = avc->anon_vma; /* 它是在遍历里面持的锁,先遍历进入,然后再保护 */ root = lock_anon_vma_root(root, anon_vma); //持 anon_vma->rwsem /* * 从该 anon_vma 的区间树中移除这个avc节点:移除后,该 anon_vma 对应的虚拟地址范 * 围变得"看不见",这意味着后续匿名页反向映射会找不到这个VMA。 */ anon_vma_interval_tree_remove(avc, &anon_vma->rb_root); /* 列表中不要包含空的 anon_vmas - 我们需要在锁之外释放它们。*/ /* * 检查anon_vma是否变成空树:如果区间树仍有其他avc节点,说明还有其他VMA引用这个 anon_vma, * 如果树为空,说明这个 anon_vma 已经"孤立",无VMA再指向它. * continue跳过 list_del(),保留该avc在链表中, 后续第二阶段需要遍历这个空avc调用 put_anon_vma() */ if (RB_EMPTY_ROOT(&anon_vma->rb_root.rb_root)) { anon_vma->parent->degree--; continue; } /* * anon_vma 区间树非空,说明还有其他VMA需要这个 anon_vma,从 avc->same_vma 链表中删除该avc节点, * 释放avc内存,不调用 put_anon_vma(),因为该 anon_vma 还有其他avc引用。 */ list_del(&avc->same_vma); //将avc从vma->anon_vma_chain 链表上移除 anon_vma_chain_free(avc); //释放回slab机制中 } /* * 递减该VMA自己的 anon_vma 指针的度数:如果 vma->anon_vma 非NULL(VMA的主要anon_vma),递减其度数, * 这表示"VMA对该 anon_vma 的直接连接"被删除. */ if (vma->anon_vma) vma->anon_vma->degree--; /* * 释放所有anon_vma树根的写锁。关键时点:此前所有区间树操作已完成(持写锁安全). * 接下来第二阶段不需要写锁(只需释放对象) */ unlock_anon_vma_root(root); //释放锁 /* * 再次遍历列表,现在它只包含空的和未链接的 anon_vma,销毁它们。之前无法执行此操作,因为 * __put_anon_vma() 需要写入获取 anon_vma->root->rwsem。 */ list_for_each_entry_safe(avc, next, &vma->anon_vma_chain, same_vma) { struct anon_vma *anon_vma = avc->anon_vma; /* * 断言检查:确保该anon_vma的degree已经变成0. * 如果degree非0,表示第一阶段的degree递减逻辑有bug, 或者该anon_vma还有其他VMA在引用(第一阶段应该已检查) */ VM_WARN_ON(anon_vma->degree); put_anon_vma(anon_vma); list_del(&avc->same_vma); anon_vma_chain_free(avc); } }
2. 调用路径
SYSCALL_DEFINE3(mprotect //mprotect.c SYSCALL_DEFINE4(pkey_mprotect //mprotect.c do_mprotect_pkey //mprotect.c mprotect_fixup //mprotect.c SYSCALL_DEFINE2(mlock //mlock.c SYSCALL_DEFINE3(mlock2 //mlock.c do_mlock //mlock.c SYSCALL_DEFINE2(munlock //mlock.c apply_vma_lock_flags //mlock.c SYSCALL_DEFINE1(mlockall //mlock.c SYSCALL_DEFINE0(munlockall) //mlock.c apply_mlockall_flags //mlock.c mlock_fixup //mlock.c SYSCALL_DEFINE1(userfaultfd //fs/userfaultfd.c userfaultfd_fops.unlocked_ioctl //fs/userfaultfd.c userfaultfd_ioctl //fs/userfaultfd.c cmd=UFFDIO_REGISTER userfaultfd_register //userfaultfd.c userfaultfd_ioctl //fs/userfaultfd.c cmd=UFFDIO_UNREGISTER userfaultfd_unregister //userfaultfd.c SYSCALL_DEFINE5(prctl //kernel/sys.c option=PR_SET_VMA prctl_set_vma //sys.c opt=PR_SET_VMA_ANON_NAME prctl_set_vma_anon_name //sys.c prctl_update_vma_anon_name //sys.c split_vma //mmap.c
可能会 split vma 的操作包括但不限于 prctl、userfaultfd、mlock、mprotect。
八、VAM合并
vma_merge
1. 相关函数
static inline struct vm_area_struct *vma_merge(struct mm_struct *mm, struct vm_area_struct *prev, unsigned long addr, unsigned long end, unsigned long vm_flags, struct anon_vma *anon, struct file *file, pgoff_t off, struct mempolicy *pol, struct vm_userfaultfd_ctx uff, const char __user *user) //mm.h { return __vma_merge(mm, prev, addr, end, vm_flags, anon, file, off, pol, uff, user, false); }
1.1 __vma_merge
(1) 函数注释:
/* * 给定一个映射请求 (addr, end, vm_flags, file, pgoff, anon_name), * 判断它是否可以与前驱 VMA 或后继 VMA 合并。也可能两边都能合并(刚好把中间空洞完整填满)。 * * 在大多数场景下(例如由 mmap、brk 或 mremap 调用),调用 vma_merge 时 [addr,end) 一定还没有被映射; * 但在 mprotect 场景下,该区间一定已经映射(要么位于 prev 内部某个偏移,要么正好从 next 开始),并 * 且该区间的标志即将被改成 vm_flags;此时“不发生变化”的情况已经在更早阶段被排除。 * * 下面是需要考虑的 mprotect 场景: * AAAA 表示从 mprotect_fixup 传下来的区间,且不会跨越超过一个 VMA;PPPPPP 表示指定的前驱 VMA;NNNNNN * 表示其后的下一个 VMA: * * AAAA AAAA AAAA AAAA * PPPPPPNNNNNN PPPPPPNNNNNN PPPPPPNNNNNN PPPPNNNNXXXX * 无法合并 可能变为 可能变为 可能变为 * PPNNNNNNNNNN PPPPPPPPPPNN PPPPPPPPPPPP 6 或 * mmap、brk 或 对应下方 case4 对应下方 case5 PPPPPPPPXXXX 7 或 * mremap move: PPPPXXXXXXXX 8 * AAAA * PPPP NNNN PPPPPPPPPPPP PPPPPPPPNNNN PPPPNNNNNNNN * 可能变为 对应下方 case1 对应下方 case2 对应下方 case3 * * 对 case 8 来说有一个关键约束:与 AAAA 重叠的 VMA NNNN 绝不能向右扩展覆盖 XXXX。 * 正确做法是:让 XXXX 在 AAAA 区间内扩展,并移除 NNNN。 * * 这样可以保证:在所有 vma_merge 成功的场景中,一旦 vma_adjust 释放 rmap_locks,合并后 VMA 的属性在 * 整个合并区间内都已经正确。其中一些属性(例如 vm_page_prot/vm_flags)会被 rmap_walk 访问,因此在释 * 放 rmap_locks 后必须立刻对整个合并区间都正确。 * * 否则,如果改成“移除 XXXX、让 NNNN 向右扩展覆盖 XXXX”,那么 remove_migration_ptes 或其他 * rmap walker(尤其在处理超出参数 end 的地址时)可能会按 NNNN 的错误权限建立 PTE,而不是使用 XXXX 的 * 正确权限。 * * * 参数: * mm: 当前进程地址空间对象。作用:在没有 prev 时,从 mm->mmap 拿链表头作为初始候选。合并成功后实际修改 * 的是这个 mm 下的 VMA 结构。 * prev: 候选前驱 VMA(地址上位于请求区间左边)。判断是否满足左并:prev->vm_end == addr。左并/双并时,通 * 常把 prev 扩展成合并后的目标 VMA。右并场景下,某些 case 会先把 prev 截断(case 4)。 * addr: 请求区间起始地址(半开区间左端)。与 prev->vm_end 比较判断左侧是否连续。用于 __vma_adjust 的新 * 边界,决定扩展/截断从哪里开始。 * end: 请求区间结束地址(半开区间右端,不包含)。作用:与 next->vm_start 比较判断右侧是否连续。用于识别 * 注释里的 6/7/8 场景:area->vm_end == end 时 next 需要后移。 * vm_flags: 请求区间最终要具备的 VMA 标志。作用:先拒绝 VM_SPECIAL(特殊映射不参与该路径合并)。参与 * can_vma_merge_before/after 的兼容性判断。合并成功后传给 khugepaged_enter_vma_merge 作为后续 THP 判 * 断依据。 * anon_vma: 请求区间对应的匿名反向映射锚点。作用:参与 merge 兼容性判断,防止把匿名血缘不该合并的 VMA 合 * 在一起。在双并路径里还会配合 is_mergeable_anon_vma(prev->anon_vma, next->anon_vma, ...) 再做一次防线。 * file: 请求区间关联文件对象(匿名映射时为 NULL)。作用:参与 can_vma_merge_before/after,确保后端文件一致 * 才允许并。文件映射时与 pgoff 一起保证映射语义连续。 * pgoff: 请求区间起始地址对应的文件页偏移(单位页,不是字节)。作用:左并时用于判断与 prev 的文件偏移连续性。 * 右并时用 pgoff + pglen 与 next 对齐,保证新区间右端能无缝接 next。在右并 case 3/8 中,扩展 next 时会用 * next->vm_pgoff - pglen 反推新起点偏移。 * policy: 请求区间的 NUMA 内存策略。作用:mpol_equal(vma_policy(prev), policy) / * mpol_equal(policy, vma_policy(next))。策略不一致直接阻断合并,避免错误跨策略合并。 * m_userfaultfd_ctx: userfaultfd 上下文。作用:作为 VMA 兼容属性之一参与 can_vma_merge_before/after。保证 * userfaultfd 相关行为一致,避免合并后语义变化。 * anon_name: 匿名映射名称(用于命名匿名 VMA 的扩展属性)。作用:同样作为兼容性字段参与 * can_vma_merge_before/after。名称不一致时可阻止合并,保持用户可见语义一致。 * keep_locked: 控制 __vma_adjust 后锁状态的策略参数。作用:透传给 __vma_adjust, * 决定合并调整期间/之后锁保持方式。主要服务于上层调用路径的锁时序要求,不改变“是否可合并”的逻辑。 * * 再给你一个“参数分组记忆法”://--这个不要-- * 1) 地址几何组:prev, addr, end。 * 2) 映射语义组:vm_flags, file, pgoff, anon_vma, policy, vm_userfaultfd_ctx, anon_name。 * 3) 执行上下文组:mm, keep_locked。 * 记住这三组,就容易理解为什么 __vma_merge 先判“几何连续”,再判“语义兼容”,最后才调用 __vma_adjust 真正改结构。 * * 返回值: 成功返回包含待操作区间的merge后的VMA,失败返回NULL表示不可merge. #### */
(2) 函数实现
/* vma_merge: (mm, prev, addr, end, vm_flags, anon, file, off, pol, uff, user, false); * * 先尝试左合并,进而尝试左右合并,最后尝试右合并。重点是虚拟地址边界对其(不对齐要先split, * split的逻辑不在这里考虑,然后对其后再考虑merge的情况) */ struct vm_area_struct *__vma_merge(struct mm_struct *mm, struct vm_area_struct *prev, unsigned long addr, unsigned long end, unsigned long vm_flags, struct anon_vma *anon_vma, struct file *file, pgoff_t pgoff, struct mempolicy *policy, struct vm_userfaultfd_ctx vm_userfaultfd_ctx, const char __user *anon_name, bool keep_locked) { /* * pglen: 待处理区间 [addr, end) 的页数, addr和end必须页对齐,且必须满足addr < end。 * 之后与 next 合并前检查时, * 会用到 pgoff + pglen 来验证“新区间尾部”与 next 起始页偏移是否连续。 */ pgoff_t pglen = (end - addr) >> PAGE_SHIFT; /* area: "当前覆盖 end 的 VMA",主要用于 case 3/8 前向合并路径。next: "候选后继 VMA", * 通常是 prev->vm_next。 */ struct vm_area_struct *area, *next; int err; /* * 我们稍后会要求 vma->vm_flags == vm_flags, 因此这也测试了 vma->vm_flags 和 VM_SPECIAL。 * VM_SPECIAL == VM_IO | VM_DONTEXPAND | VM_PFNMAP | VM_MIXEDMAP 带这些标志的VMA是不允许合并的。 */ if (vm_flags & VM_SPECIAL) return NULL; /* 先确定相邻关系:有 prev 时,next 默认取 prev 的后继。无 prev 时, * 说明从链表头开始看(链表为空或只有一个元素)。*/ if (prev) next = prev->vm_next; else next = mm->mmap; /* area 初始指向 next, 语义是“包含 end 的候选 VMA”。*/ area = next; /* 如果 area 的 vm_end (当前next->vm_end) 恰好等于 end,说明 end 落 * 在 area 末端, 真正“后继候选”应再向后挪一位。*/ if (area && area->vm_end == end) /* cases 6, 7, 8 */ next = next->vm_next; /* * 必须保证 prev->vm_start <= addr && end <= next->vm_end, 且只考虑可合并的情况(需 * 要split的不在这里考虑),这些约束条件组成了上面8种Case.#### */ VM_WARN_ON(prev && addr <= prev->vm_start); //即必须满足: prev->vm_start < addr VM_WARN_ON(area && end > area->vm_end); //即必须满足: end <= area->vm_end VM_WARN_ON(addr >= end); //即必须满足: addr < end /* * 第一步:尝试和前驱 prev 合并(左并)。条件解释: * - prev->vm_end == addr:地址连续,前后无空洞。 * - mpol_equal(...):NUMA 策略一致。默认不使能 CONFIG_NUMA 恒返回true * - can_vma_merge_after(...):文件、pgoff、标志、anon_vma、uffd、anon_name 等兼容。 */ if (prev && prev->vm_end == addr && mpol_equal(vma_policy(prev), policy) && can_vma_merge_after(prev, vm_flags, anon_vma, file, pgoff, vm_userfaultfd_ctx, anon_name)) { /* * 左合并条件成立后(prev与[addr, end)合并),继续尝试“左右同时并”。若想把 next 也并进来。Case 6 7 8 中 next * 右后移一个元素, 指向 X, area 指向 N。这里要求: * - end == next->vm_start:新区间右端与 next 连续; * - pgoff + pglen 与 next 对齐; * - prev/next 的 anon_vma 可共存(避免错误合并不同匿名血缘)。 * * 这里还多增加了一个前驱的和后续的 anon_vma 的可合并性判断,这个不能传递吗,前面左并已经判断 * 了 prev 与 area 兼容了,这里右并又判断了 area 与 next 兼容了,又要增加一个 prev 与 next 兼 * 容的判断。 */ if (next && end == next->vm_start && mpol_equal(policy, vma_policy(next)) && can_vma_merge_before(next, vm_flags, anon_vma, file, pgoff+pglen, vm_userfaultfd_ctx, anon_name) && is_mergeable_anon_vma(prev->anon_vma, next->anon_vma, NULL)) { /* cases 1, 6, 左右都可以合并。把 prev 的右边界扩到 next->vm_end,并把 next 删除####。*/ err = __vma_adjust(prev, prev->vm_start, next->vm_end, prev->vm_pgoff, NULL, prev, keep_locked); } else /* cases 2, 5, 7, 只能左侧合并,右侧不可合并。把 prev 的右边界扩展到 end。*/ err = __vma_adjust(prev, prev->vm_start, end, prev->vm_pgoff, NULL, prev, keep_locked); if (err) return NULL; /* 合并后通知 khugepaged,该 VMA 形态发生变化,便于后续 THP 评估。默认不使能 CONFIG_TRANSPARENT_HUGEPAGE 是空实现 */ khugepaged_enter_vma_merge(prev, vm_flags); return prev; } /* 第二步:左合并失败,尝试和后继 next 进行右合并。这里检查新区间尾部页偏移是否与 next 头部连续:pgoff + pglen。*/ if (next && end == next->vm_start && mpol_equal(policy, vma_policy(next)) && can_vma_merge_before(next, vm_flags, anon_vma, file, pgoff+pglen, vm_userfaultfd_ctx, anon_name)) { if (prev && addr < prev->vm_end) /* case 4 */ /* * 这8种case中待处理区间与prev有相交的,只有case4: [addr, end) 位于 prev 内部并对齐着 next 的左边界, * 先把 prev 截断到 addr,剩下的再由 next 向左接管。 */ err = __vma_adjust(prev, prev->vm_start, addr, prev->vm_pgoff, NULL, next, keep_locked); else { /* cases 3, 8 */ /* * prev不存在或待处理区域与prev不相交,对应 case 3/8: 由 area(上图中的N) 触发调整, * 把 next(case3 area=next=N; case8 area=N,next=X) * 向左扩展到 addr: * - 新 pgoff = next->vm_pgoff - pglen(保持文件页偏移连续,向左扩展是减) * - case 3 中 area 并入 next,逻辑上接近 noop+重确认; * - case 8 中会移除中间重叠的 area,再由 next 覆盖该范围。 * 无论 prev 是否存在, next 都能向左与区间合并。 */ err = __vma_adjust(area, addr, next->vm_end, next->vm_pgoff - pglen, NULL, next, keep_locked); /* * 在 Case 3 中, area 已经等于 next,这是一个空操作,但在 Case 8 中,area 已被移除, * next (指向X)已扩展到它上面。统一返回 next 作为合并后的 VMA: */ area = next; } if (err) return NULL; /* 同样告知 khugepaged:目标 VMA 形态已变。默认不使能 CONFIG_TRANSPARENT_HUGEPAGE 是空实现 */ khugepaged_enter_vma_merge(area, vm_flags); return area; } /* 两侧都不能合并,返回 NULL 交由上层走“新建/保留原结构”路径。 */ return NULL; }
2. 调用路径
userfaultfd_release //userfaultfd.c userfaultfd_register //userfaultfd.c userfaultfd_unregister //userfaultfd.c mmap_region //mmap.c do_brk_flags //mmap.c mprotect_fixup //mprotect.c mlock_fixup //mlock.c madvise_behavior //madvise.c prctl_update_vma_anon_name //sys.c vma_merge //mmap.c
诸多路径会执行 vma merge 操作,包括但不限于 prctl、madvise、mlock、mprotect、brk、userfaultfd。
九、VMA拷贝
copy_vma
1. 相关函数
/* * 翻译: 在移动页表项之前,将 vma 结构复制到同一 mm 中的新位置,以执行 mremap 移动。 * 参数: * vmap: 源VMA * addr: 目标VMA的起始虚拟地址,尝试作为merge后的左边界。 * len : 目标地址空间建立的区间长度,目标区间 [addr, addr + len) * pgoff: 目标 VMA 起点对应的页偏移, 表示 addr 处对应文件的哪一页。 * need_rmap_locks: 输出参数,告诉调用者后续搬页表时是否需要显式持有 rmap 相关锁。 */ struct vm_area_struct *copy_vma(struct vm_area_struct **vmap, unsigned long addr, unsigned long len, pgoff_t pgoff, bool *need_rmap_locks) { struct vm_area_struct *vma = *vmap; //vma: 源 VMA(通常是 mremap 等路径中的原区域)。 unsigned long vma_start = vma->vm_start; struct mm_struct *mm = vma->vm_mm; struct vm_area_struct *new_vma, *prev; struct rb_node **rb_link, *rb_parent; bool faulted_in_anon_vma = true; /* 翻译: 如果匿名 vma 尚未出现page fault,请更新新的 pgoff 以匹配新位置,以增加其合并的机会。 */ if (unlikely(vma_is_anonymous(vma) && !vma->anon_vma)) { //!vma->vm_ops && !vma->anon_vma pgoff = addr >> PAGE_SHIFT; faulted_in_anon_vma = false; } /* 查找 [addr, addr + len) 在 VMA红黑树和 mm->mmap 链表中应该插入的位置 */ if (find_vma_links(mm, addr, addr + len, &prev, &rb_link, &rb_parent)) return NULL; /* should never get here */ /* There is 3 cases to manage here in * AAAA AAAA AAAA AAAA * PPPP.... PPPP......NNNN PPPP....NNNN PP........NN * PPPPPPPP(A) PPPP..NNNNNNNN(B) PPPPPPPPPPPP(1) NULL * PPPPPPPPNNNN(2) * PPPPNNNNNNNN(3) * * new_vma == prev in case A,1,2 * new_vma == next in case B,3 * * 判断映射请求 (addr, end, vm_flags, file, pgoff, anon_name) 是否可以与前驱 VMA 或后继 VMA 合并。 * 也可能两边都能合并(刚好把中间空洞完整填满)。 * 若能合并则返回合并后的VMA,若左右两侧都不能合并则返回NULL. */ new_vma = __vma_merge(mm, prev, addr, addr + len, vma->vm_flags, vma->anon_vma, vma->vm_file, pgoff, vma_policy(vma), vma->vm_userfaultfd_ctx, vma_get_anon_name(vma), true); if (new_vma) { //合并后的VMA /* * 翻译: 源 vma 可能已合并到 new_vma 中 * 合并成功后,源 vma 可能已经被并入 new_vma。需要判断旧 vma_start 是否落入 new_vma 区间内, * 若是则更新 *vmap。 */ if (unlikely(vma_start >= new_vma->vm_start && vma_start < new_vma->vm_end)) { /* * 翻译: 在执行 mremap 时,只有当 vma 尚未发生fault时,并且允许将目标 vma 到 vm_pgoff 的映射重 * 置为 mremap 的目标地址,才能实现 vma_merge 操作。为了安全起见,mremap 必须改变源 vma * 和目标 vma 之间的 vm_pgoff 线性度(从而阻止 vma_merge 操作)。只有在尚未映射任何页面 * 的情况下,保持 vm_pgoff 线性度才是安全的。 * 断言执行: if (faulted_in_anon_vma) { dump_vma(new_vma); BUG()} */ VM_BUG_ON_VMA(faulted_in_anon_vma, new_vma); /* 更新参数,返回merge后的VMA, 返回值返回的也是它 */ *vmap = vma = new_vma; } /* 若新形成的VMA的起始页偏移变小了,则需要持锁 */ *need_rmap_locks = (new_vma->vm_pgoff <= vma->vm_pgoff); } else { //不能合并走这里 /* 直接值拷贝一份,然后只重新初始化了 anon_vma_chain 这个链表头*/ new_vma = vm_area_dup(vma); if (!new_vma) goto out; new_vma->vm_start = addr; new_vma->vm_end = addr + len; new_vma->vm_pgoff = pgoff; /* 默认不使能 CONFIG_NUMA 是空实现 */ if (vma_dup_policy(vma, new_vma)) goto out_free_vma; /* 复制的是一批 anon_vma_chain, 让新 VMA 继承旧 VMA 的 anon_vma 关系网络,成功返回0 */ if (anon_vma_clone(new_vma, vma)) goto out_free_mempol; /* 若是文件映射VMA, vm_file->f_count++ */ if (new_vma->vm_file) get_file(new_vma->vm_file); /* 新建VMA调用其open()回调 */ if (new_vma->vm_ops && new_vma->vm_ops->open) new_vma->vm_ops->open(new_vma); /* * 翻译: 由于 VMA 目前处于链接状态,它可能会受到推测性缺页错误处理程序的命中。但我们不希望在调用 * 者可能已经将 pte 从已移动的 VMA 中移出之前,就开始映射此区域的页面。为了防止这种情况发生,我们 * 现在对其进行保护,并在移动完成后允许调用者解除保护。 * * 默认不使能 CONFIG_SPECULATIVE_PAGE_FAULT 是空实现 */ vm_raw_write_begin(new_vma); /* 按起始地址递增的次序插入 mm->mmap 链表和 mm->mm_rb 红黑树, 若是文件映射VMA还会插入VMA区间树 */ vma_link(mm, new_vma, prev, rb_link, rb_parent); *need_rmap_locks = false; } return new_vma; out_free_mempol: mpol_put(vma_policy(new_vma)); out_free_vma: vm_area_free(new_vma); out: return NULL; }
2. 调用路径
SYSCALL_DEFINE5(mremap //mremap.c mremap_to //mremap.c SYSCALL_DEFINE5(mremap //mremap.c move_vma //mremap.c copy_vma //mmap.c
看起来只有 mremap() 路径中才会使用到 copy_vma().
posted on 2026-04-28 15:29 Hello-World3 阅读(4) 评论(0) 收藏 举报
浙公网安备 33010602011771号