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

概述

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


1. Fork 的基本原理

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

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

子进程获得:

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

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

  1. 物理页面共享 :父子进程的虚拟地址都映射到相同的物理页面

  2. 页表项标记为只读:即使原本是可写的页面,也被标记为只读(写保护)

  3. 设置 COW 标志:在页表项或相关数据结构中设置特殊标志,表示这是 COW 页面

    复制代码
     物理内存                       页表映射

    ┌─────────────┐ 父进程页表 子进程页表
    │ 物理页 A │ <───────── │ │
    │ (共享页面) │ │ │
    └─────────────┘ ↓ ↓
    虚拟地址 虚拟地址
    (只读,COW) (只读,COW)

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


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

2.1 为什么需要 COW?

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

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

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

2.2 COW 的工作机制

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

阶段 1:Fork 时刻
复制代码
     物理内存                      页表映射
-------------------------------------------------
┌─────────────┐            父进程页表    子进程页表
│  物理页 A    │ <─────────    │             │
│ (0xdeadbeef)│               │             │
└─────────────┘               ↓             ↓
                          虚拟地址        虚拟地址
                          (只读)          (只读)

Fork 后发生的变化:

  1. 物理页面共享 :父子进程的虚拟地址都映射到相同的物理页面
  2. 页表项标记为只读:即使原本是可写的页面,也被标记为只读(写保护)
  3. 设置 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 的关键回调函数
  1. invalidate_range_start:在内核即将修改页表前调用
  2. invalidate_range_end:在内核完成页表修改后调用
  3. change_pte:当页表项改变时调用

GPU 驱动注册这些回调,在 COW 发生时更新 GPU 的页表映射。


4. MMU Notifier 实现要点

4.1 驱动注册

GPU 驱动需要注册 MMU Notifier:

c 复制代码
// 伪代码示例
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 回调处理

c 复制代码
// 伪代码示例
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 关键考虑因素

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

5. SVM 一致性的实现

5.1 内存一致性模型

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

复制代码
虚拟地址空间 (CPU & GPU 共享)
         │
         ↓
┌─────────────────────────────┐
│    虚拟地址 0x1000           │
└─────────────────────────────┘
         │           │
    CPU 访问      GPU 访问
         │           │
         ↓           ↓
┌──────────┐   ┌──────────┐
│ CPU 页表  │   │ GPU 页表 │
└──────────┘   └──────────┘
         │           │
         └─────┬─────┘
               ↓
      必须指向相同的物理页面
               ↓
        ┌─────────────┐
        │  物理页 B    │
        │(0xD00BED00) │
        └─────────────┘

5.2 MMU Notifier 保证的属性

  1. 虚拟地址透明性:应用程序使用统一的虚拟地址
  2. 物理映射一致性:CPU 和 GPU 访问相同的物理内存
  3. 自动同步:内核自动处理页表变化
  4. 进程隔离:不同进程的 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 关键时序点

  1. Invalidate Range Start

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

    • 时机:在两次 notifier 回调之间
    • 作用:将虚拟地址映射到新的物理页面
    • 状态:此时 CPU 页表已更新,但 GPU 页表可能尚未更新
  3. 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. 总结

关键要点有:

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

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

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

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

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

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

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

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

🔗 导航

相关推荐
习惯就好zz8 天前
从奶牛NPC到完整场景构建
godot·cow·house·npc·tilemaplayer·bed
DeeplyMind20 天前
第4章: MMU notifier内核实现机制
linux·驱动开发·mmu·mmu notifier
core5121 个月前
SVM (支持向量机):寻找最完美的“分界线”
算法·机器学习·支持向量机·svm
DeeplyMind1 个月前
Linux MMU Notifier 机制与应用系列目录
linux·驱动开发·mmu notifier
芥子沫1 个月前
《人工智能基础》[算法篇5]:SVM算法解析
人工智能·算法·机器学习·支持向量机·svm
俊俊谢1 个月前
【机器学习】python使用支持向量机解决兵王问题(基于libsvm库)
python·机器学习·支持向量机·svm·libsvm
呆子罗2 个月前
Exsi虚拟机操作系统拷贝到本地VMware
svm·exsi
alwaysrun3 个月前
Rust中的智能指针
rust·智能指针·pin·cow·box·arc
xchenhao4 个月前
人脸图像识别实战:使用 LFW 数据集对比四种机器学习模型(SVM、逻辑回归、随机森林、MLP)
机器学习·支持向量机·人脸识别·数据集·逻辑回归·svm·cv