欢迎光临
我们一直在努力

引文:当SVM转角遇上Copy-on-Write (COW)

概述

本文档详细说明了 Linux 内核中 Fork 系统调用的 Copy-on-Write (COW) 机制实现原理,包括内存页面共享、写时复制触发流程,以及在 Shared Virtual Memory (SVM) 环境中与 MMU Notifier 的协作机制。


1. Fork 的基本原理

当调用 fork() 系统调用时,操作系统创建一个子进程,它是父进程的几乎完全拷贝:

父进程内存布局 Fork后
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ 代码段 │ │ 代码段 │ │ 代码段 │
├─────────────┤ ├─────────────┤ ├─────────────┤
│ 数据段 │ ────────> │ 数据段 │ │ 数据段 │
├─────────────┤ ├─────────────┤ ├─────────────┤
│ 堆 │ │ 堆 │ │ 堆 │
├─────────────┤ ├─────────────┤ ├─────────────┤
│ 栈 │ │ 栈 │ │ 栈 │
└─────────────┘ └─────────────┘ └─────────────┘
父进程 子进程

子进程获得:

  • 独立的进程 ID
  • 相同的内存映射(虚拟地址空间)
  • 相同的打开文件描述符
  • 相同的寄存器状态

为了优化性能,Linux 内核并不会立即复制所有内存页,而是作了如下处理:

  • 物理页面共享:父子进程的虚拟地址都映射到相同的物理页面
  • 页表项标记为只读:即使原本是可写的页面,也被标记为只读(写保护)
  • 设置 COW 标志:在页表项或相关数据结构中设置特殊标志,表示这是 COW 页面
  • 物理内存 页表映射
    ————————————————–
    ┌─────────────┐ 父进程页表 子进程页表
    │ 物理页 A │ <───────── │ │
    │ (共享页面) │ │ │
    └─────────────┘ ↓ ↓
    虚拟地址 虚拟地址
    (只读,COW) (只读,COW)

    这种设计使得 fork 操作非常快速,因为不需要复制任何内存页,只需要复制页表结构并调整页表项的权限。


    2. Copy-on-Write (COW) 优化机制

    2.1 为什么需要 COW?

    如果 fork 时立即复制所有内存页:

    • 效率低:大量内存复制操作耗时
    • 浪费空间:很多页面可能永远不会被修改
    • 不必要:只读的页面无需复制

    COW 机制的优化策略:延迟复制,只在需要时才复制。

    2.2 COW 的工作机制

    下面按阶段分析下页表的变化。

    阶段 1:Fork 时刻

    物理内存 页表映射
    ————————————————-
    ┌─────────────┐ 父进程页表 子进程页表
    │ 物理页 A │ <───────── │ │
    │ (0xdeadbeef)│ │ │
    └─────────────┘ ↓ ↓
    虚拟地址 虚拟地址
    (只读) (只读)

    Fork 后发生的变化:

  • 物理页面共享:父子进程的虚拟地址都映射到相同的物理页面
  • 页表项标记为只读:即使原本是可写的页面,也被标记为只读(写保护)
  • 设置 COW 标志:在页表项或相关数据结构中设置特殊标志,表示这是 COW 页面
  • 阶段 2:写入触发 COW

    当任一进程(父或子)尝试写入共享页面时:

    写入操作

    1. CPU 检测到写入只读页面

    2. 触发页面错误(Page Fault)

    3. 内核异常处理程序检查:是 COW 页面?

    4. 是:执行 COW 处理流程
    ├─ 分配新的物理页面
    ├─ 复制原页面内容到新页面
    ├─ 更新写入进程的页表,指向新页面
    ├─ 将新页面标记为可写
    └─ 调用 MMU Notifier 回调(通知相关子系统)

    5. 返回用户空间,重新执行写入指令

    6. 写入成功完成

    阶段 3:COW 后的内存状态

    ┌─────────────┐ ┌─────────────┐
    │ 旧物理页 A │ │ 新物理页 B │
    │ (0xdeadbeef)│ │ (0xD00BED00)│
    └─────────────┘ └─────────────┘
    ↑ ↑
    │ │
    未写入进程 写入进程
    页表(只读,COW) 页表(可写,非COW)


    3. 在 SVM 环境中的挑战

    3.1 问题描述

    在 Shared Virtual Memory (SVM) 系统中:

    • CPU 和 GPU 共享相同的虚拟地址空间
    • GPU 有自己的页表(GPU MMU)
    • GPU 页表需要与 CPU 页表保持同步

    当 COW 发生时:

    CPU 页表:虚拟地址 0x1000 → 新物理页 B (0xD00BED00)
    GPU 页表:虚拟地址 0x1000 → 旧物理页 A (0xdeadbeef) ❌ 不一致!

    3.2 MMU Notifier 机制

    Linux 内核提供 MMU Notifier 机制来解决这个问题:

    ┌──────────────────────────────────────────────────────┐
    │ Linux 内核 │
    │ │
    │ ┌─────────────┐ ┌──────────────────┐ │
    │ │ 内存管理子系统│ ─────> │ MMU Notifier │ │
    │ │ (MM) │ 通知 │ 回调接口 │ │
    │ └─────────────┘ └──────────────────┘ │
    │ │ │ │
    │ │ COW 事件 │ 回调 │
    │ ↓ ↓ │
    │ ┌─────────────┐ ┌──────────────────┐ │
    │ │ 更新 CPU │ │ GPU 驱动程序 │ │
    │ │ 页表 │ │ (amdgpu) │ │
    │ └─────────────┘ └──────────────────┘ │
    └──────────────────────────────────┬───────────────────┘


    ┌──────────────────┐
    │ 更新 GPU 页表 │
    │ (GPU MMU) │
    └──────────────────┘

    MMU Notifier 的关键回调函数
  • invalidate_range_start:在内核即将修改页表前调用
  • invalidate_range_end:在内核完成页表修改后调用
  • change_pte:当页表项改变时调用
  • GPU 驱动注册这些回调,在 COW 发生时更新 GPU 的页表映射。


    4. MMU Notifier 实现要点

    4.1 驱动注册

    GPU 驱动需要注册 MMU Notifier:

    // 伪代码示例
    struct mmu_notifier_ops amdgpu_mn_ops = {
    .invalidate_range_start = amdgpu_mn_invalidate_range_start,
    .invalidate_range_end = amdgpu_mn_invalidate_range_end,
    };

    // 注册 notifier
    mmu_notifier_register(&amdgpu_mn, mm);

    4.2 回调处理

    // 伪代码示例
    static int amdgpu_mn_invalidate_range_start(
    struct mmu_notifier *mn,
    const struct mmu_notifier_range *range)
    {
    // 1. 确定受影响的虚拟地址范围
    // 2. 查找相关的 GPU 映射
    // 3. 使现有 GPU TLB 条目失效
    // 4. 等待 GPU 完成相关操作
    // 5. 标记映射需要更新

    return 0;
    }

    static void amdgpu_mn_invalidate_range_end(
    struct mmu_notifier *mn,
    const struct mmu_notifier_range *range)
    {
    // 1. 获取新的物理地址映射
    // 2. 更新 GPU 页表
    // 3. 刷新 GPU TLB
    }

    4.3 关键考虑因素

  • 同步:确保 GPU 操作完成后才更新页表
  • 性能:最小化 GPU 停顿时间
  • 粒度:只更新受影响的页表条目
  • 错误处理:处理页表更新失败的情况

  • 5. SVM 一致性的实现

    5.1 内存一致性模型

    在 SVM 系统中,CPU 和 GPU 必须看到一致的内存视图:

    虚拟地址空间 (CPU & GPU 共享)


    ┌─────────────────────────────┐
    │ 虚拟地址 0x1000 │
    └─────────────────────────────┘
    │ │
    CPU 访问 GPU 访问
    │ │
    ↓ ↓
    ┌──────────┐ ┌──────────┐
    │ CPU 页表 │ │ GPU 页表 │
    └──────────┘ └──────────┘
    │ │
    └─────┬─────┘

    必须指向相同的物理页面

    ┌─────────────┐
    │ 物理页 B │
    │(0xD00BED00) │
    └─────────────┘

    5.2 MMU Notifier 保证的属性

  • 虚拟地址透明性:应用程序使用统一的虚拟地址
  • 物理映射一致性:CPU 和 GPU 访问相同的物理内存
  • 自动同步:内核自动处理页表变化
  • 进程隔离:不同进程的 SVM 空间相互独立
  • 5.3 COW 与 MMU Notifier 协作流程

    当 COW 在 SVM 环境中发生时,完整的处理流程如下:

    用户进程写入操作

    触发 Page Fault

    ┌───────────────────────────────────────┐
    │ 内核页面错误处理程序 │
    │ │
    │ 1. 检查错误类型:写入只读页面? │
    │ 2. 检查页面标志:是 COW 页面? │
    │ 3. 是:开始 COW 处理 │
    └───────────────────────────────────────┘

    ┌───────────────────────────────────────┐
    │ COW 处理流程 │
    │ │
    │ 1. 分配新物理页面 │
    │ 2. 复制原页面内容到新页面 │
    │ 3. 调用 MMU Notifier 回调 │
    │ └─> invalidate_range_start() │
    │ 4. 更新进程页表 PTE │
    │ 5. 将新页面标记为可写 │
    │ 6. 调用 MMU Notifier 回调 │
    │ └─> invalidate_range_end() │
    └───────────────────────────────────────┘
    ↓ ↓
    返回用户空间 GPU 驱动回调
    重新执行写入 ↓
    ↓ ┌──────────────────┐
    写入成功完成 │ 1. 查找 GPU 映射 │
    │ 2. 失效 GPU TLB │
    │ 3. 更新 GPU 页表 │
    │ 4. 刷新 GPU TLB │
    └──────────────────┘

    5.4 关键时序点

  • Invalidate Range Start:

    • 时机:在修改 CPU 页表之前
    • 作用:通知 GPU 驱动页面即将改变,暂停相关 GPU 操作
    • 要求:必须等待 GPU 完成当前对该页面的访问
  • 更新 CPU 页表:

    • 时机:在两次 notifier 回调之间
    • 作用:将虚拟地址映射到新的物理页面
    • 状态:此时 CPU 页表已更新,但 GPU 页表可能尚未更新
  • Invalidate Range End:

    • 时机:在修改 CPU 页表之后
    • 作用:通知 GPU 驱动更新完成,可以更新 GPU 页表
    • 要求:必须在 GPU 下次访问之前完成页表更新
  • 5.5 内核实现细节

    页面错误处理路径如下:

    do_page_fault() // arch/x86/mm/fault.c

    handle_mm_fault() // mm/memory.c

    __handle_mm_fault()

    handle_pte_fault()

    do_wp_page() // 写保护页面错误处理

    wp_page_copy() // COW 实际实现

    ├─ 检查页面引用计数
    ├─ 引用计数 > 1: 需要复制
    │ ├─ alloc_page() // 分配新页面
    │ ├─ copy_user_highpage() // 复制内容
    │ ├─ mmu_notifier_invalidate_range_start()
    │ ├─ set_pte_at() // 更新页表项
    │ ├─ page_remove_rmap() // 解除旧页面映射
    │ ├─ page_add_new_anon_rmap() // 添加新页面映射
    │ └─ mmu_notifier_invalidate_range_end()
    └─ 引用计数 = 1: 直接标记为可写


    6. 总结

    关键要点有:

  • Fork + COW:Linux 标准的进程创建优化机制

    • 延迟复制,提高 fork 性能
    • 节省内存,只复制实际修改的页面
  • 物理页面共享:Fork 后父子进程共享物理页面

    • 通过页表项的写保护实现
    • 使用引用计数管理页面生命周期
  • 写时复制触发:首次写入时才分配新物理页面

    • Page Fault 机制触发
    • 内核自动处理,对应用透明
  • MMU Notifier:内核机制,通知外部子系统页表变化

    • 双回调设计:start 和 end
    • 保证外部页表与 CPU 页表同步
  • GPU 页表同步:GPU 驱动通过 MMU Notifier 更新 GPU 页表

    • 失效旧映射,建立新映射
    • 刷新 GPU TLB,保证一致性
  • SVM 一致性:确保 CPU 和 GPU 访问相同的物理内存

    • 统一虚拟地址空间
    • 自动的页表同步机制

  • 本篇作为引子,来引出MMU notifier在SVM中的应用,接下来我会详细分析MMU notitifer的实现。SVM的深度分析在2026年的2月专栏里放送。敬请持续关注我的博客。

    🔗 导航

    • 下一章: 第1章: MMU Notifier的背景与问题域
    • 返回目录: MMU Notifier 机制与应用系列目录
    赞(0)
    未经允许不得转载:171主机测评 » 引文:当SVM转角遇上Copy-on-Write (COW)
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址