内存管理-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)    收藏  举报

导航