linux migrate_device_* APIs 的功能分析
1. 概述migrate_device_*是 Linux 内核中用于从物理设备页帧PFN出发将 ZONE_DEVICE 页面迁移到系统内存的 API 集合。与migrate_vma_*从虚拟地址空间出发不同该系列 API 的设计前提是调用者只知道物理页不知道或不需要知道哪些进程的哪些虚拟地址映射了这些页面。源码位置mm/migrate_device.c2. API 列表API签名功能migrate_device_range()int migrate_device_range(unsigned long *src_pfns, unsigned long start, unsigned long npages)锁定一段连续PFN 范围内的设备页通过 rmap 解除所有映射migrate_device_pfns()int migrate_device_pfns(unsigned long *src_pfns, unsigned long npages)同上但输入为非连续的 PFN 数组预填充migrate_device_pages()void migrate_device_pages(unsigned long *src_pfns, unsigned long *dst_pfns, unsigned long npages)迁移 struct page/folio 元数据mapping、flags、refcount 转移migrate_device_finalize()void migrate_device_finalize(unsigned long *src_pfns, unsigned long *dst_pfns, unsigned long npages)移除 migration entry安装指向目标页的新 PTE完成迁移3. 工作流程┌─────────────────────────────────────────────────────────────────┐ │ migrate_device_range() / migrate_device_pfns() │ │ │ │ 1. 对每个 PFNfolio_get folio_trylock获取引用并加锁 │ │ 2. 调用 migrate_device_unmap() │ │ a. 对 ZONE_DEVICE 页直接处理 │ │ b. 对普通页从 LRU 隔离 │ │ c. 对有映射的页调用 try_to_migrate(folio, 0) │ │ → rmap 遍历所有映射该 folio 的 VMA │ │ → 每个 VMA 发 MMU_NOTIFY_CLEAR │ │ → 用 migration entry 替换原 PTE │ │ d. 检查是否被 pin被 pin 的页恢复映射清除 MIGRATE 标志 │ │ │ │ 返回时可迁移页标记 MIGRATE_PFN_MIGRATE已解除所有 CPU 映射 │ └─────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────┐ │ 驱动代码调用者 │ │ │ │ 1. 为每个标记 MIGRATE_PFN_MIGRATE 的源页分配系统内存目标页 │ │ 2. 执行数据拷贝DMA / CPU copydevice → system memory │ │ 3. 设置 dst_pfns[i] migrate_pfn(dst_page_pfn) | │ │ MIGRATE_PFN_VALID │ │ 4. lock_page(dst_page) │ └─────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────┐ │ migrate_device_pages(src_pfns, dst_pfns, npages) │ │ │ │ 对每对 (src, dst) │ │ 1. folio_migrate_mapping()转移 address_space mapping │ │ 2. folio_migrate_flags()转移 PG_dirty、PG_uptodate 等标志 │ │ 3. 处理 THP/compound page 的拆分或合并 │ └─────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────┐ │ migrate_device_finalize(src_pfns, dst_pfns, npages) │ │ │ │ 1. remove_migration_ptes(src, dst) │ │ 遍历所有指向 src 的 migration entry替换为指向 dst 的 PTE │ │ 2. folio_unlock(src) folio_put(src)释放源页引用 │ │ 3. 如果 dst 不是 ZONE_DEVICE加入 LRU │ │ 4. folio_unlock(dst) folio_put(dst)解锁目标页 │ └─────────────────────────────────────────────────────────────────┘4. 驱动端典型代码模板intmy_device_evict_pages(unsignedlongstart_pfn,unsignedlongnpages){unsignedlong*src_pfns,*dst_pfns;inti,ret;src_pfnskvmalloc_array(npages,sizeof(*src_pfns),GFP_KERNEL);dst_pfnskvmalloc_array(npages,sizeof(*dst_pfns),GFP_KERNEL);/* Step 1: 锁定并解映射设备页 */retmigrate_device_range(src_pfns,start_pfn,npages);if(ret)gotoout;/* Step 2: 分配目标系统页执行 DMA 拷贝 */for(i0;inpages;i){structpage*dpage;if(!(src_pfns[i]MIGRATE_PFN_MIGRATE))continue;dpagealloc_page(GFP_HIGHUSER_MOVABLE);if(!dpage){src_pfns[i]~MIGRATE_PFN_MIGRATE;continue;}lock_page(dpage);dst_pfns[i]migrate_pfn(page_to_pfn(dpage))|MIGRATE_PFN_VALID;/* DMA copy: device VRAM → system RAM */dma_copy_from_device(src_pfns[i],dpage);}/* Step 3: 迁移 struct page 元数据 */migrate_device_pages(src_pfns,dst_pfns,npages);/* Step 4: 安装新 PTE完成迁移 */migrate_device_finalize(src_pfns,dst_pfns,npages);out:kvfree(src_pfns);kvfree(dst_pfns);returnret;}5. 内部关键机制5.1 rmap 逆向解映射migrate_device_unmap()对每个有 CPU 映射的 folio 调用if(folio_mapped(folio))try_to_migrate(folio,0);try_to_migrate()使用 rmapreverse mapping遍历所有映射了该物理页的 VMA对每个 VMA 中的 PTE 调用try_to_migrate_one()发送MMU_NOTIFY_CLEAR无 owner将 PTE 替换为 migration swap entry调用folio_remove_rmap_pte()5.2 MMU 通知// try_to_migrate_one() in mm/rmap.cmmu_notifier_range_init(range,MMU_NOTIFY_CLEAR,0,vma-vm_mm,address,range.end);使用mmu_notifier_range_init()非_init_owner变体event 类型MMU_NOTIFY_CLEAR不携带 owner订阅者无法区分是谁发起了这次通知粒度每个 folio × 每个 VMA 独立通知5.3 Pin 检测迁移前检查引用计数是否超过预期if((folio_ref_count(folio)-extra)folio_mapcount(folio))returnfalse;// 页被 pin不可迁移被 pin 的页如通过get_user_pagespin 住的会被恢复映射不参与迁移。5.4 Compound Page / THP 支持migrate_device_range()和migrate_device_pfns()检测 compound pagenrfolio_nr_pages(folio);if(nr1){src_pfns[i]|MIGRATE_PFN_COMPOUND;for(j1;jnr;j)src_pfns[ij]0;// 后续子页标记为空}6. 应用场景6.1 TTM/VRAM EvictionGPU 显存驱逐情景GPU VRAM 已满新的分配请求需要腾出空间。TTM 内存管理器选择一个牺牲 BO 进行驱逐。TTM memory manager → amdgpu_svm_bo_evict() → drm_pagemap_evict_to_ram() → migrate_device_pfns(src_pfns, npages)为什么用migrate_device_*TTM 只知道这块 VRAM 的物理地址需要回收它不持有 VMA 引用也不知道哪些进程通过 SVM 映射了这些页面。必须从 PFN 出发通过 rmap 找到所有映射。6.2 驱动 Unload / Unbind设备解绑情景GPU 驱动被卸载或设备被热插拔移除所有 ZONE_DEVICE 页面必须迁回系统内存。amdgpu_device_fini() → 遍历所有 device memory pages → migrate_device_range(src_pfns, start_pfn, npages)为什么用migrate_device_*驱动卸载时需要清空所有设备内存。此时可能有多个进程、多个 VMA 映射了这些页面逐一查找 VMA 不现实。从物理页出发通过 rmap 是唯一可行的方式。6.3 设备内存错误恢复Hardware Error Recovery情景硬件报告某个 VRAM bank 出现不可纠正错误需要将该区域的所有页面迁出。error_handler() → identify affected PFN range → migrate_device_range(src_pfns, bad_start_pfn, bad_npages)为什么用migrate_device_*错误恢复从物理地址出发驱动知道的是哪段物理内存坏了不是哪些虚拟地址需要迁移。6.4 设备内存碎片整理Device Memory Compaction情景设备内存碎片化严重需要整理出连续物理空间供大块分配使用。compaction_worker() → 选择需要搬移的 PFN 集合 → migrate_device_pfns(src_pfns, npages) // 非连续 PFN → 分配新的连续 device memory 作为目标 → migrate_device_pages() migrate_device_finalize()为什么用migrate_device_pfns需要迁移的页面在物理上不连续碎片migrate_device_pfns()专门支持非连续 PFN 数组。6.5 NUMA 感知的设备内存再平衡情景多 GPU 系统中某个 GPU 的 VRAM 中存放了更适合放在另一个 GPU 上的数据跨设备迁移的第一步device→RAM。rebalance_worker() → migrate_device_range() // GPU_A VRAM → system RAM → migrate_vma_setup() // system RAM → GPU_B VRAM (需要 VMA 上下文)7.migrate_device_range()vsmigrate_device_pfns()选择条件使用源页在物理上连续同一 BO、同一 VRAM bankmigrate_device_range(src, start_pfn, n)源页在物理上不连续散落在不同位置migrate_device_pfns(src, n)预填充 PFN 数组二者内部逻辑完全相同区别仅在输入方式migrate_device_range自动从start_pfn开始填充连续 PFNmigrate_device_pfns调用者预先填好src_pfns[]数组中的每个 PFN8. 与migrate_vma_*的对比维度migrate_vma_*migrate_device_*出发点虚拟地址范围VMA start/end物理 PFN设备页帧号需要struct vm_area_struct *vma只需 PFN 数组MMU 通知MMU_NOTIFY_MIGRATEownerMMU_NOTIFY_CLEAR无 owner解映射方式直接遍历页表migrate_vma_collect_pmd通过 rmap 逆向查找try_to_migrate空洞处理可以为pte_none()分配 device 页populate不处理空洞只迁移已存在的页通知粒度整个范围一次通知每个 folio/每个 VMA 单独通知发起者标识有pgmap_owner订阅者可跳过自己的迁移无方向双向RAM↔VRAM主要用于 device→RAM驱逐9. 限制与注意事项不能迁移被 pin 的页面get_user_pages()/pin_user_pages()锁定的页面会被跳过只能从 device 迁移到 system或 device-to-device 如果目标也是 ZONE_DEVICE不处理虚拟地址空洞只迁移已存在的物理页不能像migrate_vma_*那样 populate 新页面MMU_NOTIFY_CLEAR 无 owner所有 MMU notifier 订阅者都会被通知无法像MMU_NOTIFY_MIGRATE那样让发起者跳过自己的通知。这在某些场景下可能引发额外的无效化操作如 VRAM overcommit 场景下的 livelock 问题rmap 开销每个页面都需要通过 rmap 反向遍历所有映射对大量页面的迁移可能产生可观的锁竞争