Linux 内核中 mm_struct 引用计数与从驱动安全访问进程地址空间

发布时间:2026/7/23 7:36:55
Linux 内核中 mm_struct 引用计数与从驱动安全访问进程地址空间 内核中 mm_struct 引用计数与从驱动安全访问进程地址空间本文系统梳理内核mm_struct的两套引用计数(mm_users/mm_count)及其配套 API(mmget/mmput、mmgrab/mmdrop、mmget_not_zero、get_task_mm等),并给出内核驱动需要操作某个进程的 VMA / 页表数据时,如何判定该进程是否还活着的标准范式。全文以drivers/gpu/drm/drm_pagemap.c的 SVM 迁移代码为实例。1. 为什么需要两个计数struct mm_struct描述一个进程的整个用户态地址空间(VMA 树、页表、mmap_lock等)。它的生命周期有两个层次不同的问题:地址空间(用户态映射)是否还在?—— VMA、页表项、匿名内存是否还有效。mm_struct这块结构体本身是否还没被free?—— 即便地址空间已经拆掉,结构体可能仍需短暂存活(例如内核线程借用它做 lazy TLB)。内核用两个 atomic 计数分别回答这两个问题:计数语义增/减 API保护对象mm_users有多少使用者需要访问用户态地址空间mmget/mmget_not_zero/mmputVMA、页表、用户态映射mm_count有多少引用需要mm_struct结构体本身存活mmgrab/mmdropstruct mm_struct内存关键关系(定义于include/linux/sched/mm.h、kernel/fork.c):mm_users归 0→ 触发__mmput():调用exit_mmap()拆除所有 VMA、释放页表和用户态页、触发 mmu_notifier release 等。地址空间从此不可访问。mm_users本身对mm_count持有1 个引用。因此只要mm_users 0,mm_count必然 0。mm_count归 0→ 触发__mmdrop():真正free掉mm_struct结构体(以及 pgd 等)。一句话:mm_users保护地址空间可用,mm_count只保护结构体没被释放。结构体还在(mm_count0)不代表地址空间还能访问(可能mm_users0)。2. 计数生命周期图mm_count (struct mm_struct)mm_users (address space)释放内部那 1 个引用后 mmdropmm_users 对 mm_count 持有 1 个引用mmget() / mmget_not_zero()1 mm_usersmmput()-1 mm_usersmm_users 0__mmput()exit_mmap() 拆 VMA/页表 mmu_notifier releasemmgrab()1 mm_countmmdrop()-1 mm_countmm_count 0__mmdrop()free(mm_struct) free pgd进程退出(do_exit→exit_mm→mmput)时,通常先把mm_users降到 0,地址空间被exit_mmap()拆掉;但如果此时还有人持有mm_count(如 lazy TLB 的内核线程、或驱动mmgrab了),mm_struct结构体会继续存活到最后一个mmdrop。3. API 逐个说明以下摘自include/linux/sched/mm.h。3.1 mmgrab / mmdrop —— 操作mm_countstaticinlinevoidmmgrab(structmm_struct*mm){atomic_inc(mm-mm_count);}staticinlinevoidmmdrop(structmm_struct*mm){if(unlikely(atomic_dec_and_test(mm-mm_count)))__mmdrop(mm);}作用:只保证mm_struct结构体不被释放,不保证地址空间可用。适用:需要长期(甚至无界时间)持有对mm的弱引用,后续再用mmget_not_zero尝试升级为可访问地址空间的强引用。头文件注释原话:“This doesn’t guarantee that the associated address space will still exist later on and mmget_not_zero() has to be used before accessing it.”3.2 mmget / mmput —— 操作mm_usersstaticinlinevoidmmget(structmm_struct*mm){atomic_inc(mm-mm_users);}externvoidmmput(structmm_struct*);voidmmput_async(structmm_struct*);/* 可在原子上下文调用的慢路径版本 */作用:保证用户态地址空间在mmput前不被拆除,可以安全遍历 VMA、访问页表(仍需按需持mmap_lock/ PTL,见第 5 节)。mmget只在你已确定mm_users 0(例如就是current-mm)时使用,因为它无条件1,对一个已经归 0 的mm用mmget会造成复活已死地址空间的错误。头文件注释警告:“Never use this function to pin this address space for an unbounded/indefinite amount of time.”—— 因为它会阻止进程退出时回收内存。mmput_async:当你可能在原子上下文(持自旋锁、IRQ 等)释放最后一个引用时使用,把可能睡眠的__mmput(内含exit_mmap)甩到 workqueue。3.3 mmget_not_zero —— 安全地升级为地址空间引用staticinlineboolmmget_not_zero(structmm_struct*mm){returnatomic_inc_not_zero(mm-mm_users);}语义:仅当mm_users ! 0时才1,并返回是否成功。返回true:抢到了一个 user 引用,地址空间保证存活到你配对的mmput()/mmput_async(),可以安全访问 VMA / 页表。返回false:mm_users已经是 0 —— owner 进程正在退出、exit_mmap()已经或即将拆地址空间。绝不能再访问它的 VMA / 页表。这是驱动持有别的进程mm时访问其地址空间的唯一正确入口。3.4 get_task_mm / mm_access —— 从 task 取 mmexternstructmm_struct*get_task_mm(structtask_struct*task);externstructmm_struct*mm_access(structtask_struct*task,unsignedintmode);get_task_mm:在task_lock下读task-mm,若非内核线程则mmget后返回(即返回时已持有mm_users引用)。用完mmput。mm_access:在get_task_mm基础上叠加ptrace_may_access权限检查,用于跨进程访问(如/proc/pid/mem)。4. 选择矩阵:该用哪个你的需求用什么释放就地访问current自己的地址空间已隐含mm_users0,直接mmap_read_lock(current-mm)解锁即可从task_struct拿到某进程 mm 并访问get_task_mm()/mm_access()mmput()长期缓存别的进程的 mm 指针(弱引用)mmgrab()保命mmdrop()之后要真正访问被缓存的那个 mm 的地址空间先mmget_not_zero()判活成功后mmput()/mmput_async()可能在原子上下文释放最后引用mmput_async()—驱动里典型的两段式:注册时mmgrab缓存mm(弱引用,允许进程退出);真正要迁移/访问地址空间那一刻,mmget_not_zero判活并升级,失败就放弃。5. 计数 ≠ 锁:仍需 mmap_lock / PTL引用计数只解决这块 mm / 地址空间还在不在,不解决并发修改。访问地址空间时还需要相应的锁:按虚拟地址遍历 VMA(vma_lookup、读vm_start/vm_end):需要mmap_read_lock(mm),以排除并发munmap/mremap/mprotect(它们持mmap_write_lock)。修改单个 PTE(present ↔ migration entry 的原子替换):需要 PTL(pte_offset_map_lock/pmd_lock)。以page 为中心、走 rmap定位 PTE(如migrate_device_*):不需要mmap_lock,但需要 folio lock 反向映射时的anon_vma锁 PTL。组合规则:mmget_not_zero()成功 “地址空间还在,可以去拿锁”;具体访问 VMA 还要mmap_read_lock,改 PTE 还要 PTL。两者缺一不可。6. 实例:drm_pagemap SVM 迁移6.1 缓存弱引用(注册期)SVM BO 初始化时保存所属进程的mm(drivers/gpu/drm/drm_pagemap.cdrm_pagemap_devmem_init()):devmem_allocation-mmmm;/* 通常调用方已 mmgrab 保命 */BO 可能长期存活(显存里放着),而 owner 进程随时可能退出,所以这里只能是弱引用语义 —— 不能用mm_users钉住,否则进程退不干净。6.2 fault 路径:mmap_lock 已由缺页框架持有drm_pagemap_populate_mm()是填充 device 内存的入口,它先判活再上锁:intdrm_pagemap_populate_mm(structdrm_pagemap*dpagemap,...,structmm_struct*mm,...){if(!mmget_not_zero(mm))/* ① 判活并升级为地址空间引用 */return-EFAULT;mmap_read_lock(mm);/* ② 再锁 VMA */errdpagemap-ops-populate_mm(dpagemap,start,end,mm,...);mmap_read_unlock(mm);mmput(mm);/* ③ 归还引用 */returnerr;}这就是第 4、5 节范式的教科书式落地:mmget_not_zero判活 →mmap_read_lock锁 VMA → 访问 → 解锁 →mmput。6.3 eviction 路径:判活后走 page-centric,不锁 VMAdrm_pagemap_evict_to_ram()由驱动主动腾显存时调用,访问的可能是另一个进程的 BO:retry:if(!mmget_not_zero(devmem_allocation-mm))/* 判活 */return-EFAULT;/* owner 正在退出 */...errmigrate_device_pfns(src,npages);/* page-centric靠 rmap不遍历 VMA */...err_out:mmput_async(devmem_allocation-mm);/* 可能在受限上下文用 async */if(completion_done(devmem_allocation-detached))return0;/* BO 已被并发释放视作成功 */if(retry_count--){cond_resched();gotoretry;}returnerr?:-EBUSY;要点:mmget_not_zero成功只是保活地址空间;这里并不mmap_read_lock,因为migrate_device_pfns以 page 为中心、通过 rmap 定位并在 PTL 下替换 PTE(见第 5 节),无需按地址遍历 VMA。用mmput_async而非mmput,因为释放最后一个引用会触发exit_mmap这类可能睡眠的重活,而本路径可能在不宜睡眠的上下文中收尾。mmget_not_zero返回false → -EFAULT是owner 进程正在退出的唯一信号来源。6.4 -EFAULT 的下游语义(驱动侧处理)在多进程显存过量(overcommit)场景中,进程 A 分配 VRAM 会触发 TTM 驱逐进程 B 的 SVM BO;若此刻 B 被CtrlC杀掉,drm_pagemap_evict_to_ram的mmget_not_zero(B-mm)失败返回 -EFAULT。该错误若原样上抛,会被 TTM 当作致命错误中止整个 LRU 驱逐 walk,从而误伤 A 的分配。amdgpu 侧的正确处理是把这个owner 正在退出的 -EFAULT 翻译成可重试的 -EBUSY(amdgpu_svm_bo_evict()),让 TTM 跳过这个 victim 去驱逐别的 BO,而 B 的exit_mmap会异步回收其 device 页并释放 BO:rdrm_pagemap_evict_to_ram(sbo-devmem);if(r-EFAULT)/* owner 退出中页将被 exit_mmap 回收 */r-EBUSY;/* 让 TTM 换一个 victim而非致命失败 */这印证了本文的核心结论:mmget_not_zero失败不是错误,而是进程已死这一合法状态的信号,上层应把它当作该对象即将被自然回收来处理,而不是硬失败。7. 常见错误与陷阱只mmgrab就去访问 VMA:结构体在不代表地址空间在。必须再mmget_not_zero判活。对可能已死的mm用mmget(而非mmget_not_zero):会把mm_users从 0 拉回非 0,复活一个正在/已经exit_mmap的地址空间,导致 UAF / 数据损坏。持有mm_users无界时间:阻止进程退出时回收内存,等价于内存泄漏 拖住 OOM。弱引用要用mmgrab。把mmget_not_zero失败当致命错误上抛:在跨进程场景会误伤无辜的其他进程(见 6.4)。在原子上下文用mmput:最后一次mmput会走exit_mmap可能睡眠;受限上下文用mmput_async。有了引用就不加锁:计数不等于锁,访问 VMA 仍需mmap_lock,改 PTE 仍需 PTL。8. 速查表我持有我能保证我能做我不能做mm_count(mmgrab)mm_struct结构体不被 free读mm指针字段、后续mmget_not_zero判活访问 VMA / 页表 / 用户内存mm_users(mmget/mmget_not_zero成功)地址空间不被拆加mmap_lock遍历 VMA、加 PTL 改 PTE无界长期持有mmap_read_lockVMA 结构不被并发改按地址遍历 VMA修改 VMA(需 write)PTL单个 PTE 原子性present↔migration entry 替换遍历 VMA

相关新闻

最新新闻

日新闻

周新闻

月新闻