Linux内核分析之内存管理-04

This language version is unavailable; showing the other language.

22.1 用户地址空间布局 —— mmap 与栈

每个进程一出生,内核就为它的用户半区规划了"城市规划图":哪里放代码与数据、哪里允许堆向上长、mmap 区从哪个边界向哪头分配、栈顶在哪。规划的核心参数是 mm->mmap_base(mmap 区边界)与布局模式(top-down 或 legacy),两者由架构代码 arch_pick_mmap_layout() 在进程创建时确定并掺入随机化。本节解析这张规划图。


22.1.1 x86_64 用户半区的标准版图

// include/linux/mm_types.h:1142;1149;1256-1257
    unsigned long mmap_base;    /* base of mmap area */
    unsigned long task_size;    /* size of task vm space */
    ...
    unsigned long start_brk, brk, start_stack;
    unsigned long arg_start, arg_end, env_start, env_end;

x86_64 四级页表下 task_size = 2^47(128TB),五级 LA57 下扩大到 2^56。默认 top-down 布局的分段:

低地址                                                                       task_size
 0   ┌─────────────┬───────┬──────────────┬──────────────────┬─────────┬──────┐
     │ text+rodata │ data  │ bss          │ brk 堆(向上)      │ (空洞)  │ mmap │
     │  代码/常量   │ 已初始化│ 零页按需      │                  │         │ 区   │
     └─────────────┴───────┴──────────────┴──────────────────┴─────────┴──┬───┘
                                                              mmap_base ^ │ v 向下分配
     ─────────────────────────────────────────────────────────────────────────────
                                        ┌──────────────┬──────────────┐
                                        │ 栈(向下生长)   │  arg/env 块   │ 顶部
                                        └──────────────┴──────────────┘
                                              ^ start_stack     task_size
 mmap_base 之下到 brk 之间的空档: MAP_FIXED 与库加载的战场

为什么 mmap 区要"贴着栈"向下长:top-down 布局让 mmap 区的空档与 brk 堆的空档各自独立增长、互不相撞的时间最大化;两者最终相遇即 ENOMEM(地址空间耗尽),位置由 mmap_base 与 RLIMIT_DATA 等边界共同控制。

22.1.2 布局模式:top-down 与 legacy

// arch/x86/mm/mmap.c:82-133(节选)
static unsigned long mmap_base(unsigned long rnd, unsigned long task_size,
                   const struct rlimit *rlim_stack)
{
    unsigned long gap = rlim_stack->rlim_cur;
    unsigned long pad = stack_maxrandom_size(task_size) + stack_guard_gap;

    /* Values close to RLIM_INFINITY can overflow. */
    if (gap + pad > gap)
        gap += pad;

    /*
     * Top of mmap area (just below the process stack).
     * Leave an at least ~128 MB hole with possible stack randomization.
     */
    gap = clamp(gap, SIZE_128M, (task_size / 6) * 5);

    return PAGE_ALIGN(task_size - gap - rnd);       /* :99 */
}

static unsigned long mmap_legacy_base(unsigned long rnd,
                      unsigned long task_size)
{
    return __TASK_UNMAPPED_BASE(task_size) + rnd;       /* :104 */
}

static void arch_pick_mmap_base(unsigned long *base, unsigned long *legacy_base, ...)
{
    *legacy_base = mmap_legacy_base(random_factor, task_size);
    if (mmap_is_legacy())
        *base = *legacy_base;
    else
        *base = mmap_base(random_factor, task_size, rlim_stack);    /* :119 */
}

void arch_pick_mmap_layout(struct mm_struct *mm, const struct rlimit *rlim_stack)
{
    if (mmap_is_legacy())
        mm_flags_clear(MMF_TOPDOWN, mm);        /* :125-130 */
    else
        mm_flags_set(MMF_TOPDOWN, mm);
    ...
}

两种模式的分野:

模式 mmap 起点与方向 触发条件(mmap_is_legacy())
top-down(默认) task_size - gap - rnd,向下分配 正常情况(非 personality 标记 + RLIMIT_STACK 未"无限")
legacy TASK_UNMAPPED_BASE(约 1/3 处),向上分配 personality & ADDR_COMPAT_LAYOUT 或栈 rlimit 近乎无限(旧版 ld.so 兼容)

mmap_base() 的 gap 计算有三重意义:预留 RLIMIT_STACK 大小的栈空间、再垫上栈最大随机化幅度 + stack_guard_gap(栈向下的生长余量与保护洞),最后 clamp 到"至少 128MB、至多 5/6 地址空间"——注释原文"Leave an at least ~128 MB hole with possible stack randomization"(mmap.c:93-95)正是 32 位时代栈与 mmap 区冲撞事故的制度化反思。

22.1.3 随机化:两处熵的注入

// arch/x86/mm/mmap.c:77-79
unsigned long arch_mmap_rnd(void)
{
    return arch_rnd(mmap_is_ia32() ? mmap32_rnd_bits : mmap64_rnd_bits);
}

mmap 区基址按 mmap64_rnd_bits(默认 28 位 ≈ 1TB 摆动范围)随机偏移。用户态 ASLR 与 20.5 节内核 KASLR 的关键差异在此显现:随机化是每进程独立的——arch_pick_mmap_layout() 在每个 exec/线程组创建时重新掷骰子,同一时刻系统内各进程的 mmap 区位置互不相同,一处泄露不再全局有效。栈的随机化(stack_maxrandom_size)叠加在 gap 之上,堆 start_brk 的随机化(brk_randomized)见 22.5.3 节。

randomize_va_space sysctl 三档:0 全关、1 保守(mmap/栈/库)、2 完整(额外堆随机化)——生产默认 2。

22.1.4 exec 装配的顺序

地址空间不是 exec 一步建成的,而是"清场 → 装载 → 补栈"三段:

execve → load_elf_binary() 内:

 [1] 旧地址空间整体拆除 (exec_mmap 换新 mm)
 [2] 逐 PT_LOAD 段 mmap:
       text → MAP_PRIVATE|PROT_EXEC   (文件页, 只读共享)
       data → MAP_PRIVATE|PROT_WRITE  (COW: 原始页来自文件)
       bss  → 匿名零页 (VM_ACCOUNT)
       装配后 mm->start_brk/brk 定位 (heap 起点=end_data 对齐+随机)
 [3] setup_arg_pages() (fs/exec.c:598-722):
       把 argv/envp 从临时页搬到栈顶区
       建立栈 VMA (VM_GROWSDOWN), 记录 arg_start/end, env_start/end
       mm->start_stack 定位
 [4] arch_pick_mmap_layout() 已在更早的 mm 初始化时定下 mmap_base

setup_arg_pages()(fs/exec.c:598)是栈 VMA 的"出生证明":它不是等缺页才创建栈区,而是先把参数页真实拷入顶部,后续向下生长才走 22.5 节的自动扩展路径。

22.1.5 布局的安全推论

布局不只是功能性分区,它直接决定两类经典攻击的可行性边界:

[1] 栈喷射与 mmap 区相邻性。top-down 布局中栈与 mmap 区被 gap 分开(22.1.2 节 stack_guard_gap 的 128MB 保底),这让"从栈精确落点到 mmap 区内的库数据"需要跨过随机化区间——32 位时代 task_size/6 的 clamp 就是为了在 3GB 地址空间里仍保住这个缓冲。ASLR 关闭(randomize_va_space=0)的系统里,布局退化为确定值——mmap_base = task_size - gap、库加载于固定点,这正是"禁用 ASLR 的容器/固件"被单独列为风险项的原因。

[2] 用户/内核地址位判别。canonical hole(20.1.1 节)让"一个 64 位值是不是内核地址"变成一次符号位检查——copy_from_user 的有效性预检、SMAP 缺席平台的软件兜底都建立其上。LAM(Linear Address Masking,mm.rst 注释提及)放宽用户指针的 canonical 检查以支持指针标签,但只放宽用户半区(LAM_SUP 单独控制内核侧)——布局的对称性成为新特性必须保留的接口。

布局退化实验 (只读观测, 无需改内核):

 setarch -R ./app     关闭 ASLR 运行
 cat /proc/self/maps  两次启动对比:
   ASLR on:  mmap 段基址每次不同 (22.1.3 节 28 位熵)
   ASLR off: 段基址固定 — 库/栈/堆全部可预测
 这一对比直观展示 top-down 布局 + 随机化的叠加关系

小结

用户半区的规划由 arch_pick_mmap_layout() 一锤定音:top-down(默认)把 mmap 区从 task_size - gap - rnd 向下生长,legacy 兼容模式从 1/3 处向上;gap 里显式预留了栈 rlimit、栈随机化幅度与 stack_guard_gap。mmap 基址按每进程独立的 arch_mmap_rnd() 随机化,与内核 KASLR 的全局性形成安全模型的对照。exec 的三段装配(清场、段映射、参数搬栈)把这张规划图填上第一批分区。下一节深入分区表本身——VMA 与它的 maple tree 索引。

22.2 VMA (vm_area_struct) 管理

VMA 是进程地址空间的最小管理单元:一段 [vm_start, vm_end) 的连续地址共享同一组权限与同一个后备存储。缺页处理、munmap、mprotect、fork 的页表复制——一切地址空间操作的第一步都是"找到(或拆出)相关 VMA"。本节拆解 vm_area_struct 的字段语义、maple tree 索引、并发模型(mmap_lock + per-VMA 锁),以及 anon_vma 反向映射挂点。


22.2.1 vm_area_struct 逐段解析

// include/linux/mm_types.h:913-980(节选)
struct vm_area_struct {
    /* The first cache line has the info for VMA tree walking. */

    union {
        struct {
            /* VMA covers [vm_start; vm_end) addresses within mm */
            unsigned long vm_start;     /* :919 */
            unsigned long vm_end;       /* :920 */
        };
        freeptr_t vm_freeptr; /* SLUB_TYPESAFE_BY_RCU 的释放指针 */
    };

    /*
     * The address space we belong to.
     * Unstable RCU readers are allowed to read this.
     */
    struct mm_struct *vm_mm;            /* :929 */
    pgprot_t vm_page_prot;          /* 本 VMA 的访问权限(PTE 模板) :930 */

    /*
     * Flags, see mm.h.
     * To modify use vm_flags_{init|reset|set|clear|mod} functions.
     */
    union {
        const vm_flags_t vm_flags;      /* :939 */
        vma_flags_t flags;
    };
    ...
    unsigned int vm_lock_seq;           /* :958 per-VMA 锁序号 */
    ...
    struct list_head anon_vma_chain; /* Serialized by mmap_lock & :966
                      * page_table_lock */
    struct anon_vma *anon_vma;  /* Serialized by page_table_lock :968 */

    /* Function pointers to deal with this struct. */
    const struct vm_operations_struct *vm_ops;  /* :971 */

    /* Information about our backing store: */
    unsigned long vm_pgoff;     /* 文件内偏移(页单位)  :974 */
    struct file * vm_file;      /* 映射的文件(可 NULL) :976 */
    void * vm_private_data;     /* 私有数据(共享内存用) :980 */
    ...
};

字段分四组:范围与权限(vm_start/end、vm_page_prot、vm_flags——前者是建立 PTE 时的权限模板,后者是管理语义位);归属与并发(vm_mm、vm_lock_seq 与文件尾部的 vm_lock.refcnt 构成 per-VMA 锁);后备存储(vm_file/vm_pgoff 定位文件页缓存槽位,anon_vma 挂反向映射);多态分发(vm_ops->fault 决定缺页时找谁要数据——文件系统、共享内存、设备映射各有实现)。

vm_flags 的核心位(include/linux/mm.h:410-485):

标志 语义 典型来源
VM_READ/WRITE/EXEC 访问权限(与 vm_page_prot 呼应) mmap prot
VM_SHARED 共享映射(写穿透到后备) MAP_SHARED
VM_MAYREAD/WRITE/EXEC 权限"天花板"(mprotect 只能在此范围收紧放开) do_mmap 恒置
VM_GROWSDOWN/UP 可向下/向上扩展(栈) :410/:475
VM_LOCKED mlock 锁定不可换出 :420
VM_ACCOUNT 计入 overcommit 统计 :427
VM_DONTCOPY / VM_DONTEXPAND fork 不复制 / mremap 不扩张 :424-425
VM_SEQ_READ / VM_RAND_READ 访问模式提示(预读策略) madvise :422-423

22.2.2 mm_struct:地址空间容器

// include/linux/mm_types.h:1123-1260(节选)
struct mm_struct {
    ...
    atomic_t mm_count;      /* :1137 mm 本体引用(含 mm_users 折算 1) */
    struct maple_tree mm_mt;    /* :1140 VMA 索引: 按 vm_end 排的 maple tree */
    unsigned long mmap_base;    /* :1142 mmap 区边界 (22.1 节) */
    unsigned long task_size;    /* :1149 */
    pgd_t * pgd;            /* :1150 顶级页表 (19.2.3 节) */
    ...
    atomic_t mm_users;      /* :1171 使用者计数(线程/借 mm 者) */
    ...
    struct rw_semaphore mmap_lock;  /* :1196 全局地址空间写锁 */
    ...
    seqcount_t mm_lock_seq;     /* :1222 写锁代序号(per-VMA 锁协作) */
    unsigned long hiwater_rss;  /* :1235 RSS 高水位 */
    unsigned long total_vm;     /* :1238 已映射总页数 */
    ...
    vm_flags_t def_flags;       /* :1244 新 VMA 默认标志(如 VM_LOCKED) */
    unsigned long start_brk, brk, start_stack;      /* :1256 */
    unsigned long arg_start, arg_end, env_start, env_end;   /* :1257 */
    struct rlimit rlim_stack;   /* 栈上限 */
    ...
};

mm_users 与 mm_count 的两级计数(注释原文见 :1129-1137)是 17 章 kref 思想的变体:线程共享 mm 使 mm_users 增,mm_users 归零触发 exit_mm→mmput,此时 mm_count 仍在的持有者(如内核线程借 mm)保证结构不被释放,全部引用放完才 mmdrop。

mmap_lock 是整个地址空间的写锁:任何 VMA 增删改(mmap/munmap/mprotect/brk/栈生长)都要写锁;读侧(缺页、GUP)拿读锁。它是多线程进程可扩展性的头号瓶颈——由此催生 per-VMA 锁(22.2.4 节)。

22.2.3 maple tree:VMA 的索引结构

VMA 索引从 6.1 起由红黑树换成 maple tree(区间感知的 B 树变体):

// mm/mmap.c:902-907
struct vm_area_struct *find_vma(struct mm_struct *mm, unsigned long addr)
{
    unsigned long index = addr;

    mmap_assert_locked(mm);
    return mt_find(&mm->mm_mt, &index, ULONG_MAX);
}
EXPORT_SYMBOL(find_vma);
maple tree vs 红黑树 (VMA 场景):

                红黑树 (旧)                maple tree (7.0)
 索引方式        按 vm_end 单点              按 [start, end) 区间
                find_vma(addr)=首个 end>addr
                再手查 start<=addr
 遍历            树游标, 每步 O(log n)        VMA_ITERATOR: 批量 walk,
                                           节点内多个槽位缓存友好
 fork            逐 VMA 复制+rebalance       树快照复制(maple 原生)
 RCU 读          支持                        原生支持 (mt_find 无锁路径)
 vma_iter       vma_iter_init/load/next     (mm/mmap.c:926-940 的
                 find_vma_prev 即用例)        find_vma_prev 用例)

find_vma_prev()(mmap.c:922-940)展示了迭代器风格:vma_iter_load 取当前、vma_prev 回退一格——分配/合并逻辑频繁需要"地址两侧的邻居",区间索引+迭代器让邻居查询从两次树搜索变成一次游标移动。fork 性能是换树的实际动机:大线程数进程(数万 VMA)的复制从"n 次插入"变为"一次快照"。

22.2.4 并发模型:mmap_lock + per-VMA 锁

7.0 的读侧路径(缺页)默认不再拿 mmap_lock 读锁,而是按 VMA 粒度加轻量锁:

// include/linux/mm_types.h:946-958(注释要点)
#ifdef CONFIG_PER_VMA_LOCK
    /*
     * vm_lock_seq: 可在 mmap_lock 写锁 + 特定位组合下用 WRITE_ONCE 修改;
     * 读者持 vm_lock.refcnt 或比对 vm_lock_seq 与 mm->mm_lock_seq 判断
     * "我这轮读到的 VMA 是否仍与写者视角一致"。
     * 序号显式允许回绕: 回绕最多导致偶发回退慢路径。
     */
    unsigned int vm_lock_seq;
#endif
读侧 (缺页) 快路径                    写侧 (mmap/munmap)

 拿 per-VMA 读锁(refcnt++)            拿 mmap_lock 写锁
  ├─ 若 VMA 正被写锁: 回退慢路径        mm->mm_lock_seq++ (奇偶翻转)
  ├─ 读 VMA 字段做缺页处理              改 VMA / 拆分 / 删除
  └─ 释放 refcnt                       遍历解锁所有读者(refcnt 清零)
                                       释放写锁
 一致性保证: 读者比对 vm_lock_seq != mm->mm_lock_seq
 说明写者动过 → 弃用本次结果回退慢路径 (mmap_lock 读锁重做)

设计要点在 mm_types.h:946-957 注释中写明:写者修改 VMA 前必须把 vm_lock_seq 推进;读者要么持有 mmap_lock(全局安全)要么用"seq 比对 + 回退"的乐观协议。这把多线程进程的缺页并发从"一把全局读锁"提升为"每 VMA 独立"——不同线程同时在不同 VMA 上缺页互不阻塞,只有同一 VMA 被写者改动时才整体回退。22.4 节的缺页入口将看到这条快慢路径的实际分流。

22.2.5 anon_vma:为回收与迁移铺路

anon_vma/anon_vma_chain(mm_types.h:966-968)解决反向映射问题:"这一页物理页被哪些页表映射着?"——回收(24 章要 unmap/换出)与迁移(18.5 节要换 pte)都必须能从 page 找回全部映射点。匿名页的答案:分配页时建 anon_vma 节点挂进 page,VMA 侧经 anon_vma_chain 双向挂链;文件页则走 address_space 的 i_mmap 树(36 章)。COW 时 MAP_PRIVATE 文件 VMA 会同时挂在两棵结构里(mm_types.h:959-961 的注释原文正是这一规则),因为 COW 后的私有副本是匿名页。


小结

VMA 以 [start,end)+权限+后备 三元组描述地址空间的分区,四组字段分别服务范围、并发、后备与多态分发;mm_struct 以 maple tree mm_mt 索引全部 VMA(区间感知、fork 快照、RCU 读),以 mmap_lock 统摄写操作,并以 per-VMA 锁(vm_lock_seq/refcnt + mm_lock_seq 比对回退)把缺页读并发提升到 VMA 粒度。anon_vma 系统为回收与迁移提供反向映射通路。下一节看 VMA 是如何被 mmap 系统调用创建与合并的。

22.3 mmap 系统调用与映射类型

mmap 是进程地址空间的"万能编辑器":文件映射、匿名内存、共享内存、设备寄存器、线程栈全部经它进入分区表。本节沿 do_mmap()(mm/mmap.c:335)到 mmap_region()(mm/vma.c:2826)走通调用链,并区分 MAP_PRIVATE/MAP_SHARED × 匿名/文件四种组合在内核中的真实形态。


22.3.1 第一段:do_mmap 的校验与选址

// mm/mmap.c:335-470(节选)
unsigned long do_mmap(struct file *file, unsigned long addr,
            unsigned long len, unsigned long prot,
            unsigned long flags, vm_flags_t vm_flags,
            unsigned long pgoff, unsigned long *populate,
            struct list_head *uf)
{
    struct mm_struct *mm = current->mm;
    int pkey = 0;

    *populate = 0;
    mmap_assert_write_locked(mm);       /* 调用者已持写锁 */

    if (!len)
        return -EINVAL;

    /*
     * PROT_READ 是否隐含 PROT_EXEC?
     * (noexec 挂载的文件系统除外)
     */
    if ((prot & PROT_READ) && (current->personality & READ_IMPLIES_EXEC))
        if (!(file && path_noexec(&file->f_path)))
            prot |= PROT_EXEC;
    ...
    /* Careful about overflows.. */
    len = PAGE_ALIGN(len);
    if (!len)
        return -ENOMEM;

    /* offset overflow? */
    if ((pgoff + (len >> PAGE_SHIFT)) < pgoff)
        return -EOVERFLOW;

    /* Too many mappings? */
    if (mm->map_count > sysctl_max_map_count)   /* 默认 65530 */
        return -ENOMEM;
    ...
    vm_flags |= calc_vm_prot_bits(prot, pkey) | calc_vm_flag_bits(file, flags) |
            mm->def_flags | VM_MAYREAD | VM_MAYWRITE | VM_MAYEXEC;  /* :404 */

    /* 选址: 验证或挑选地址 */
    addr = __get_unmapped_area(file, addr, len, pgoff, flags, vm_flags);    /* :410 */
    if (IS_ERR_VALUE(addr))
        return addr;

    if (flags & MAP_FIXED_NOREPLACE) {
        if (find_vma_intersection(mm, addr, addr + len))
            return -EEXIST;
    }
    ...
}

校验清单浓缩了 mmap 的全部边界语义:READ_IMPLIES_EXEC 的历史兼容、页对齐与溢出检查、sysctl_max_map_count 配额、权限位到 VM 位翻译(calc_vm_prot_bits 把 PROT_* 折成 VM_READ|WRITE|EXEC 与 PKEY 保护键)、VM_MAY* 天花板恒置(此后 mprotect 只能在天花板内浮动)。选址三态:MAP_FIXED 用调用者地址(覆写既有映射!)、MAP_FIXED_NOREPLACE 有冲突即 EEXIST、无提示则 __get_unmapped_area 沿 mmap_base 向下(22.1 节布局)扫描空档。

22.3.2 第二段:mmap_region 建 VMA

// mm/vma.c:2729-2800(节选, __mmap_region 主体)
static unsigned long __mmap_region(struct file *file, unsigned long addr,
        unsigned long len, vm_flags_t vm_flags, unsigned long pgoff,
        struct list_head *uf)
{
    struct mm_struct *mm = current->mm;
    struct vm_area_struct *vma = NULL;
    bool have_mmap_prepare = file && file->f_op->mmap_prepare;
    VMA_ITERATOR(vmi, mm, addr);
    ...
    error = __mmap_setup(&map, &desc, uf);
    if (!error && have_mmap_prepare)
        error = call_mmap_prepare(&map, &desc);
    if (error)
        goto abort_munmap;
    ...
    /* Attempt to merge with adjacent VMAs... */
    if (map.prev || map.next) {
        VMG_MMAP_STATE(vmg, &map, /* vma = */ NULL);
        vma = vma_merge_new_range(&vmg);        /* :2756 */
    }

    /* ...but if we can't, allocate a new VMA. */
    if (!vma) {
        error = __mmap_new_vma(&map, &vma);     /* :2761 */
        if (error)
            goto unacct_error;
        allocated_new = true;
    }
    ...
    __mmap_complete(&map, vma);
    return addr;
    ...
}
mmap_region 的执行序列:

 __mmap_setup()          冲突区拆除(munmap 语义) + 记账(VM_ACCOUNT)
      │
      ├─ f_op->mmap_prepare?   新式驱动钩子: 先描述后建 VMA
      │      (7.0 的 desc 化重构: 驱动不再直接摸 VMA 内部)
      v
 vma_merge_new_range()   能合并? prev/next 与新区间同权限同后备
      │                  且中间无洞 → 扩展旧 VMA, 免分配
      ├─ 合并成功 ────────────> 完成 (maple tree 原地改区间)
      v  不能
 __mmap_new_vma()        vm_area_alloc 分配 VMA (21 章 slab!)
      │                  f_op->mmap() 驱动初始化
      │                  vma_store 挂入 mm->mm_mt
      v
 __mmap_complete()       填充 populate(POPULATE_MAP 预触) / 统计

合并(vma_merge_new_range)是长寿命进程防 map_count 膨胀的关键:同权限、同文件、pgoff 连续、标志兼容的相邻区间被并成一个 VMA。VM_DONTEXPAND(如特殊驱动映射)与 VM_SPECIAL 组合(VM_IO|VM_DONTCOPY|VM_PFNMAP 等)是合并的否决项——某些映射"长得像"但语义不可并。

22.3.3 四种映射形态

                MAP_PRIVATE                    MAP_SHARED
        ┌───────────────────────────┬────────────────────────────┐
 匿名   │ 普通 malloc/malloc'ed 大块  │ 共享匿名: fork 前建          │
 (无文件)│ vm_file=NULL, anon_vma 挂链 │ (MAP_SHARED|MAP_ANONYMOUS   │
        │ 写时复制到 swap(24 章)       │  + tmpfs 驱动 shmem)        │
        │                           │ 进程间真正共享页帧            │
        ├───────────────────────────┼────────────────────────────┤
 文件   │ 库/程序加载: 读走页缓存       │ 共享内存映射: 写回文件        │
 (有文件)│ 写触发 COW → 私有匿名副本    │ 脏页经回写落盘(36 章)        │
        │ 原文件页永远只读共享          │ msync/msync 语义            │
        └───────────────────────────┴────────────────────────────┘

 vm_file / anon_vma 的组合判别 (mm_types.h:959-963 注释):
   匿名 PRIVATE: anon_vma only          文件 PRIVATE+COW 后: 两者都挂
   文件 SHARED: i_mmap 树 only          brk/栈: anon_vma only (file=NULL)

MAP_PRIVATE 文件映射的两段式是理解库加载内存账目的钥匙:进程的"私有写的库数据段"由两部分物理页构成——未写过的页仍指向页缓存共享页帧(几百个进程共享同一份 libpthread 文本与只读数据),写过一次的页变成进程私有匿名页。RSS 的"共享/私有"拆分(/proc/pid/smaps 的 Shared_Clean/Private_Dirty)正是这个形态的报表。

MAP_SHARED 匿名并不"无中生有":内核用 tmpfs(shmem)充当后备文件——vm_file 指向 tmpfs inode,页在文件页缓存里天然跨进程共享。这也是它在 overcommit 记账里按"文件页"对待的原因。

22.3.4 驱动视角:f_op->mmap 与 mmap_prepare

设备驱动通过 file_operations->mmap 实现 remap_pfn_range()/vm_insert_page() 把自己的内存(MMIO、DMA 缓冲、DAX)暴露进用户空间;vm_ops->fault 则实现"缺页时供货"的设备端。7.0 引入的 mmap_prepare 钩子(vma.c:2742-2744 的 have_mmap_prepare 分支与 struct vm_area_desc)把驱动的参与时机从"VMA 已建好之后"提前到"建 VMA 之前"——驱动在 desc 上声明所需属性,内核统一完成 VMA 构造与 maple 挂载,杜绝驱动代码直接改 VMA 内部字段引发的竞态。旧 f_op->mmap 依然兼容,两类钩子在 __mmap_region 内分流。

22.3.5 munmap 的镜像操作

munmap(start, len):
  mmap_write_lock
  do_vmi_align_munmap():
    ├─ 找覆盖区间的全部 VMA
    ├─ 端点切割: 区间部分重叠时把首尾 VMA 一分为二
    ├─ 从 maple tree 摘除命中 VMA (批量 detach)
    ├─ zap_page_range: 清 pte + 刷 TLB (19.5 节 mmu_gather)
    │    匿名页引用减、文件页减映射
    └─ RCU/引用时序释放 VMA 结构

切割(split)与合并(merge)是一对逆操作:munmap 把大 VMA 切小,mmap 把相邻同类并大;长寿命进程的碎片化程度取决于两者的净流量。sysctl_max_map_count 配额(do_mmap :388-390)最终约束的是这套结构的规模。


小结

mmap 的两段式实现——do_mmap 负责校验、权限翻译与选址,mmap_region 负责冲突拆除、合并或新建 VMA 并挂树——把"万能编辑器"落实为分区表上的原子编辑。四种映射形态由 MAP_PRIVATE/SHARED × 文件/匿名 组合决定页帧的共享与私有语义,tmpfs 为共享匿名充当隐形后备;COW 让文件私有映射在写之前零成本共享。7.0 的 mmap_prepare 钩子重构把驱动的参与时机前移、收敛 VMA 构造权。下一节看这张分区表如何被缺页异常逐页兑现。

22.4 页面换入 —— 按需分配与缺页处理

mmap 建立的只是分区表记录;真正把物理页填进页表的动作发生在第一次访问时的缺页异常里。这个"按需分页"(demand paging)设计让 fork+exec 的启动路径只需建结构、不供数据,进程的第一条指令触发缺页才开始供货。本节以分配者的视角走一遍缺页链——入口 do_user_addr_fault()、分发 handle_mm_fault() 与三个主要供货者(匿名页、文件页、COW 页)——异常入口的体系结构细节留给 25 章展开。


22.4.1 为什么按需:记账与供货分离

fork + exec 的时间账:

 立即完成 (微秒级):
   fork:   复制 mm_struct + maple tree 快照 + 页表页 (复制 pte 而非页!)
           全部 pte 标只读 (COW 预埋)
   exec:   拆旧分区表, 建 3-5 个 VMA, 搬参数页
 延迟完成 (按访问):
   第 1 条指令  → text 文件页缺页 → 页缓存供货
   第 1 次写栈  → 栈匿名页缺页   → 伙伴系统供货
   第 1 次写 data → COW 缺页     → 复制新页
   malloc 大块   → brk/mmap 只建 VMA
   逐页触碰      → 逐页缺页供货  (mmap POPULATE 可选择提前全付)

分离的收益是双重的:启动快(不触碰的页永不分配——RSS 远小于 VSZ 的根本原因),记账准(overcommit 允许承诺大于物理内存,22.5 节 acct_stack_growth 的最后一道检查即 overcommit 统计)。代价是缺页异常成为常态路径,异常入口必须优化到纳秒级——这正是 22.2.4 节 per-VMA 锁存在的理由。

22.4.2 入口:do_user_addr_fault

// arch/x86/mm/fault.c:1207(签名与主流程要点)
void do_user_addr_fault(struct pt_regs *regs,
            unsigned long error_code,
            unsigned long address)
{
    ...
    /* 1. 快速过滤: SMAP/SMEP、NX、内核地址误入等 → SIGSEGV 直达 */
    ...
    /* 2. 找 VMA (per-VMA 锁快路径) */
    vma = lock_vma_under_rcu(mm, address);
    if (likely(vma)) {
        /* VMA 就位且未被并发改动: 直接进入 handle_mm_fault */
        ...
        goto handle;
    }
    /* 3. 回退: mmap_lock 读锁 + find_vma 重来 (22.2.4 节协议) */
    ...
handle:
    fault = handle_mm_fault(vma, address, flags, regs);
    ...
}

x86 入口的职责是异常语义解码:error_code 的 P/W/U/I/RPK 等位区分"页不存在/权限违反、读/写、用户/内核、取指/数据、保护键违反",把硬件事件翻译成内存管理问题。地址合法性检查(find_vma 命中且区间含 fault 地址、访问模式不超 VMA 权限)不通过即进入 SIGSEGV 发送路径;通过则进入通用层 handle_mm_fault()(mm/memory.c:6602)。

22.4.3 分发:handle_mm_fault → handle_pte_fault

// mm/memory.c:6286 与 4484-4490(节选)
static vm_fault_t handle_pte_fault(struct vm_fault *vmf)
{   /* :6286 */
    ...
    if (!vmf->pte)
        return do_pte_missing(vmf); /* pte 为空: 缺页 */
    ...
    if (vmf->flags & FAULT_FLAG_WRITE) {
        if (!pte_write(entry))
            return do_wp_page(vmf); /* 写只读 pte: COW 判定 */
        entry = pte_mkdirty(entry);
    }
    ...
}

// mm/memory.c:4484-4490
static vm_fault_t do_pte_missing(struct vm_fault *vmf)
{
    if (vma_is_anonymous(vmf->vma))
        return do_anonymous_page(vmf);  /* 匿名: 分配零页 */
    else
        return do_fault(vmf);       /* 文件/设备: vm_ops->fault */
}
handle_pte_fault 的判别矩阵:

 pte 状态  ×  访问           匿名 VMA                 文件 VMA
 ─────────────────────────────────────────────────────────────────
 pte 空      读              do_anonymous_page         do_fault →
             写              (零页/COW 预埋)            vm_ops->fault
                                                     (文件页入缓存)
 pte 在但只读  写            do_wp_page:                同左, 但
             (COW 预埋/换出/  单映射直接改写;            先重取文件页
              mprotect 效果)  多映射复制新页              再判定
 pte 在且可写  写            只需置 dirty/accessed 位 (软缺页, 零分配)
 pte 是 swap 槽  任意          do_swap_page: 从交换区换回 (24 章)
 pte 是 marker  任意          userfaultfd/PTE marker 专用路径

"软缺页"(pte 存在、只补标志位)与"硬缺页"(真分配)的成本差一个数量级,回收器(24 章)利用 accessed/dirty 位的硬件置位(19.2.2 节)把大量冷访问留在软路径处理。

22.4.4 匿名页供货:do_anonymous_page

// mm/memory.c:5230(要点)
static vm_fault_t do_anonymous_page(struct vm_fault *vmf)
{
    ...
    /* 首次读且页可整体零页化: 映射共享 zero page, 零成本 */
    if (pte_alloc(vmf->vma->vm_mm, vmf->pmd))
        ...
    /* 分配物理页 */
    folio = vma_alloc_zeroed_movable_folio(vmf->vma, vmf->address); /* 18.3 节 */
    if (!folio)
        return VM_FAULT_OOM;
    /* anon_vma 记账 + 页表映射 */
    ...
    entry = mk_pte(page, vma->vm_page_prot);
    entry = pte_sw_mkyoung(entry);
    if (write) {
        entry = maybe_mkwrite(pte_mkdirty(entry), vmf->vma);    /* 只读预埋 */
        ...
    }
    ...
}

三个细节体现"分配者"的精打细算:首次读匿名页映射共享零页(zero page,全进程共享一份全零页帧,第一次写才触发 COW 分配);分配走 vma_alloc_zeroed_movable_folio(GFP_HIGHUSER_MOVABLE,页可被迁移规整——18.5 节)并零化(信息残留消毒,21.5.4 节呼应);写路径的 maybe_mkwrite(pte_mkdirty(...)) 把"写过"状态一次性置齐,避免二次软缺页。

22.4.5 文件页供货:do_fault → vm_ops->fault

// mm/memory.c:5350(__do_fault 要点)
static vm_fault_t __do_fault(struct vm_fault *vmf)
{
    ...
    ret = vma->vm_ops->fault(vmf);      /* 分发到后备实现 */
    ...
}

vm_ops->fault 的通用文件实现是 filemap_fault()(mm/filemap.c):按 vm_pgoff + 偏移 查页缓存(36 章),命中即映射;未命中则发起读 I/O(带预读——VM_SEQ_READ/RAND_READ 提示影响预读窗口,mm.h:422-423)。设备/共享内存 VMA 提供各自的 fault(如 shmem_fault 从 tmpfs 取页)。文件路径把"内存供货"无缝接进页缓存——这是 mmap I/O 比 read/write 少一次拷贝的结构原因(36.1 节对照)。

22.4.6 供货失败的三种表情

返回值 含义 用户可见
VM_FAULT_OOM 供货时内存耗尽 触发 OOM 流程(24.4 节)
VM_FAULT_SIGBUS 非法访问(越界映射/IO 错误) 进程收 SIGBUS(14 章)
VM_FAULT_SIGSEGV 访问无 VMA/权限非法 进程收 SIGSEGV
VM_FAULT_RETRY 需重试(mmap_lock 竞争/IO 阻塞) 入口循环重来

VM_FAULT_RETRY 是 22.2.4 节并发协议的闭环:per-VMA 快路径发现冲突时,供货方以 RETRY 让入口放弃快状态、转 mmap_lock 慢路径重来——正确性不靠锁内完美,靠"可重试"的幂等设计。

22.4.7 缺页成本的量级账

把各路径的成本量级放在一起,"按需分页为什么可行、瓶颈在哪"就一目了然:

路径                        量级 (现代 x86, 粗略)
───────────────────────────────────────────────────────
软缺页 (补 accessed/dirty)   ~0.2-0.5μs
硬缺页-匿名 (分配+映射)       ~1-3μs
硬缺页-文件 (页缓存命中)      ~1-2μs
硬缺页-文件 (需读盘)          ~100μs-数 ms  (SSD/HDD)
swap-in (zswap 命中)         ~10-50μs (解压)
swap-in (真读盘)             ~100μs-数 ms
MGLRU look_around 摊派        接近零 (邻居批量分代)

设计含义:
 - 异常本身 (进入/退出/查 VMA) 是固定税 → 热路径优化
   都在压这段 (per-VMA 锁 22.2.4、CR2 读取 25.1.2)
 - 命中页缓存的文件缺页已接近"免费" → mmap I/O 的
   优势区间 (36 章对照)
 - 磁盘 IO 的 100 倍落差是回收策略 (24 章) 的一切
   — 把 IO 型缺页挡在稳态之外

/proc/vmstat 的 pgfault/pgmajfault 分别是总缺页与"需要 IO 的重大缺页"计数——两者的差值即软缺页量;性能调优的第一步永远是看 majflt 是否为零。


小结

按需分页把"分区表承诺"与"物理供货"解耦:入口解码异常语义并按 per-VMA 锁协议找到 VMA,handle_pte_fault 以 pte 状态×访问方式的矩阵分发给匿名供货(零页起步、写时分配、movable 页)、文件供货(页缓存 + vm_ops 多态)或 COW 判定(25.3 节展开),软缺页路径让标志位修复零分配完成。VM_FAULT_* 的返回契约把 OOM、SIGBUS/SIGSEGV 与重试语义编成统一协议。下一节看两个最活跃的动态扩张源——栈与堆——如何在这张分区表上做"增量记账"。

22.5 栈自动扩展与堆管理 (brk/mmap)

进程的地址空间不是 exec 时一次画完的静态图:栈向下生长、堆向上生长、glibc 的大块分配转投 mmap。这三个动态源各有一套内核约束——栈生长要过 acct_stack_growth() 的五道检查与 stack_guard_gap 的安全间隙,brk 的收缩要防"与别的映射交错",mmap 化的堆则完全复用 22.3 节的机制。本节解析两边的实现与它们的容量边界。


22.5.1 栈的自动扩展

栈 VMA 由 setup_arg_pages()(fs/exec.c:598)在 exec 时创建并标注 VM_GROWSDOWN(mm.h:410)。此后"栈用超了一页"不表现为越界错误,而是触发缺页异常走进扩展判定:

// mm/vma.c:3184-3240(节选)
int expand_downwards(struct vm_area_struct *vma, unsigned long address)
{
    struct mm_struct *mm = vma->vm_mm;
    ...
    if (!(vma->vm_flags & VM_GROWSUP))
        return -EFAULT;     /* 只对栈类 VMA 放行 */
    ...
    mmap_assert_write_locked(mm);

    /* Guard against exceeding limits of the address space. */
    address &= PAGE_MASK;
    if (address >= (TASK_SIZE & PAGE_MASK))
        return -ENOMEM;
    address += PAGE_SIZE;

    /* Enforce stack_guard_gap */
    gap_addr = address + stack_guard_gap;

    /* Guard against overflow */
    if (gap_addr < address || gap_addr > TASK_SIZE)
        gap_addr = TASK_SIZE;

    next = find_vma_intersection(mm, vma->vm_end, gap_addr);
    if (next && vma_is_accessible(next)) {
        ...
        /* 不能撞进别的可访问映射 */
    }
    ...
    if (unlikely(anon_vma_prepare(vma)))
        return -ENOMEM;
    ...
    error = acct_stack_growth(vma, size, grow); /* 五道检查 */
    ...
    /* 通过后: vm_start 下移, 补建 pte */
}
// mm/vma.c:3059-3086(节选, 五道检查)
static int acct_stack_growth(struct vm_area_struct *vma,
                 unsigned long size, unsigned long grow)
{
    struct mm_struct *mm = vma->vm_mm;
    unsigned long new_start;

    /* 1. address space limit tests */
    if (!may_expand_vm(mm, vma->vm_flags, grow))
        return -ENOMEM;

    /* 2. Stack limit test */
    if (size > rlimit(RLIMIT_STACK))
        return -ENOMEM;

    /* 3. mlock limit tests */
    if (!mlock_future_ok(mm, vma->vm_flags & VM_LOCKED, grow << PAGE_SHIFT))
        return -ENOMEM;

    /* 4. 不得长进 hugetlb 专属区 */
    new_start = (vma->vm_flags & VM_GROWSUP) ? vma->vm_start :
            vma->vm_end - size;
    if (is_hugepage_only_range(vma->vm_mm, new_start, size))
        return -EFAULT;

    /*
     * 5. Overcommit.. 最后测, 因为它要更新安全统计
     */
    if (security_vm_enough_memory_mm(mm, grow))
        return -ENOMEM;

    return 0;
}

guard gap 的安全问题(mm/mmap.c:932-944 的 stack_guard_gap,默认 256 页 = 1MB):扩展前的 find_vma_intersection(mm, vma->vm_end, gap_addr) 检查的不是紧邻的一页,而是 fault 地址之后一整个 gap——这是对历史漏洞(栈与相邻 mmap 区之间只隔一页时,用精确控制缺页地址的攻击"跨洞"探入相邻映射,如著名的 Guard-page 类漏洞)的修复。注释原文保留的语义:栈可以越过 gap 与同为栈类的映射相邻(vma_expand_down 路径的 VM_GROWSDOWN 判定,mmap.c:979-1010),但绝不越过可访问的普通映射。

22.5.2 sys_brk:堆的增删

// mm/mmap.c:116-200(节选)
SYSCALL_DEFINE1(brk, unsigned long, brk)
{
    unsigned long newbrk, oldbrk, origbrk;
    struct mm_struct *mm = current->mm;
    struct vm_area_struct *brkvma, *next = NULL;
    unsigned long min_brk;
    ...
    min_brk = mm->start_brk;
#ifdef CONFIG_COMPAT_BRK
    /*
     * CONFIG_COMPAT_BRK can still be overridden by setting
     * randomize_va_space to 2, which will still cause mm->start_brk
     * to be arbitrarily shifted
     */
    if (!current->brk_randomized)
        min_brk = mm->end_data;
#endif
    if (brk < min_brk)
        goto out;

    /*
     * Check against rlimit here...
     */
    if (check_data_rlimit(rlimit(RLIMIT_DATA), brk, mm->start_brk,
                  mm->end_data, mm->start_data))
        goto out;

    newbrk = PAGE_ALIGN(brk);
    oldbrk = PAGE_ALIGN(mm->brk);
    if (oldbrk == newbrk) {
        mm->brk = brk;
        goto success;
    }

    /* Always allow shrinking brk. */
    if (brk <= mm->brk) {
        /* Search one past newbrk */
        vma_iter_init(&vmi, mm, newbrk);
        brkvma = vma_find(&vmi, oldbrk);
        if (!brkvma || brkvma->vm_start >= oldbrk)
            goto out; /* mapping intersects with an existing non-brk vma. */
        ...
        mm->brk = brk;
        if (do_vmi_align_munmap(&vmi, brkvma, mm, newbrk, oldbrk, &uf,
                    /* unlock = */ true))
            ...
    }
    ...
    ret = do_brk_flags(&vmi, vma, addr, len, vm_flags); /* :1236 扩张 */
brk 的两种流向:

 增长:  mm->brk ──页对齐──> do_brk_flags()                  vma.c:2874
        ├─ 堆 VMA 已存在且可扩: 直接推进 vm_end (等价一次 merge)
        ├─ 否则新建 brk VMA (匿名, VM_ACCOUNT)
        └─ 只改分区表, 不供货 (页后到, 22.4 节)

 收缩:  mm->brk ──> do_vmi_align_munmap() 拆掉 [newbrk, oldbrk)
        ├─ 释放对应页帧 (清 pte + mmu_gather 刷 TLB)
        └─ brk VMA 相应回缩或消失

 两条越界防线:
   brk < start_brk            不许低于堆底
   RLIMIT_DATA (check_data_rlimit)  数据总量配额

收缩路径的防御性检查值得细读:!brkvma || brkvma->vm_start >= oldbrk 的判定确保"要缩掉的范围完全属于 brk VMA 自己"——如果中间混入了用户的 MAP_FIXED 映射,收缩被拒(mapping intersects with an existing non-brk vma)。brk 语义要求堆是"自己的一亩三分地",任何外来映射穿插都会使其不可收缩。

CONFIG_COMPAT_BRK 与 brk_randomized(mmap.c:130-136)是堆随机化的兼容开关:现代默认 randomize_va_space=2 时 exec 把 start_brk 在 end_data 之上随机偏移(brk_randomized 置位后 min_brk 用随机后的起点),防"堆地址可预测"的旧式利用。

22.5.3 malloc 的双层策略

用户态分配器不直接决定"走 brk 还是 mmap",但两者在 glibc 中的分工塑造了堆的形态:

小分配 (glibc 默认 < MMAP_THRESHOLD, 动态 128KB):
  brk 堆内切分 (ptmalloc arena)
  → 内核视角: 一次 brk 扩张, 之后零系统调用
  → free 不还内核 (arena 保留), RSS 稳态高

大分配 (≥ MMAP_THRESHOLD):
  每次 mmap 一个匿名段, free 即 munmap
  → 内核视角: 大量 VMA 创建/销毁 (map_count 压力)
  → 释放立即可见 (RSS 回落), 但每块多两次系统调用 + TLB 刷新
  → 阈值动态调节: 频繁 munmap 的大块会抬高阈值 (避免抖动)

 内核侧的对应观测:
   /proc/pid/smaps 的匿名段分布
   mm->map_count / sysctl_max_map_count 配额 (22.3.1 节)
   brk VMA 与匿名 mmap VMA 在 smaps 中可用地址区分

22.5.4 容量边界的对照表

边界 检查点 默认值/来源
栈上限 acct_stack_growth 第 2 道 RLIMIT_STACK(软 8MB 常见)
栈与邻居间隙 stack_guard_gap 256 页(mm/mmap.c:935)
地址空间总量 may_expand_vm task_size 与锁定配额
mlock 总量 mlock_future_ok 第 3 道 RLIMIT_MEMLOCK
overcommit security_vm_enough_memory_mm 第 5 道 sysctl_overcommit_memory 策略
堆底 brk < min_brk mm->start_brk(随机化后)
堆/数据配额 check_data_rlimit RLIMIT_DATA

三套边界(栈五道、堆两道、mmap 的 max_map_count/overcommit)共同把"进程能吃掉多少地址空间"变成可配置的安全参数——这正是 overcommit 模型把"承诺"与"物理"分开后必须补上的账。


小结

栈与堆是分区表上仅有的两个"自动编辑"源:栈的向下生长由缺页驱动,经 expand_downwards 的边界、邻居(含 guard gap)与 acct_stack_growth 五道检查(空间限额、RLIMIT_STACK、mlock、hugepage 区、overcommit)后改 VMA 补页;brk 由用户显式驱动,增长走 do_brk_flags 的建/并,收缩走严格归属检查后的 munmap,堆底与 RLIMIT_DATA 构成双重下界。glibc 把小分配留在 brk arena、大分配转投 mmap,两种策略在系统调用成本、VMA 压力与内存归还及时性上互为镜像。至此进程地址空间全章闭环;下一章进入 Huge Pages 与透明大页——分区表上的"大块论"。