ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

linux 持锁与内存分配:作用域标志(memalloc scope)机制

linux 持锁与内存分配:作用域标志(memalloc scope)机制 「持锁与内存分配」小系列 · 下篇。上篇 持锁与内存分配三条约束与预分配范式 梳理了持锁分配的三条约束并给出第一条路线——把可能触发回收的分配移出临界区预分配。本篇讲第二条互补路线——分配仍留在临界区内改用作用域标志改变其回收行为从源头切断回收递归与锁反转。两条路线共同回答同一问题如何避免持锁分配与其他模块形成死锁。1. 机制定位Linux 内核提供了一组作用于当前进程current-flags的内存分配作用域标志memalloc scope flags用于在一段临界区内临时改变本线程后续所有内存分配的行为而无需逐个调用点去修改gfp_mask。其典型用途是打破内存回收reclaim的递归避免在持有某些锁时因分配触发回收而发生自死锁。这组接口定义在 include/linux/sched/mm.h均为save/restore成对使用的内联函数。2. 要解决的核心问题回收递归与死锁内核中一次看似普通的GFP_KERNEL分配在内存紧张时会进入**直接回收direct reclaim**路径进而可能回调文件系统、块层、shrinker、MMU notifier 等子系统去释放内存。若当前线程已经持有这些子系统需要的锁就会形成递归乃至 AB-BA 死锁持锁 L └─ 分配内存GFP_KERNEL └─ 内存不足 → 直接回收 └─ 回收回调子系统FS / IO / shrinker / MMU notifier └─ 该回调再次尝试获取锁 L → 死锁解决思路有两种一是在每个分配点手工传入受限的gfp_mask如GFP_NOFS、GFP_NOIO但临界区内分配点众多且分散容易遗漏二是用进程级作用域标志在临界区入口一次性设定出口统一还原期间所有分配自动继承该约束。后者正是本机制的价值所在。2.1 为什么 MMU notifier 会「回来」持有锁 L以 userptr / HMMSVMGPU 驱动为例一把锁 L页表更新锁 / eviction 锁同时保护两条路径——「页表更新」与「MMU notifier 的 invalidate 回调」。页表更新路径持有 L 时会做GFP_KERNEL分配job/fence/BO 元数据等一旦触发直接回收回收解除映射 / 迁移页又会回调 invalidate后者要作废 GPU 页表项于是再次来拿同一把 L再次获取同一把 L页表更新路径持有锁 L持锁期间 GFP_KERNEL 分配job / fence / BO 元数据内存不足 → 直接回收回收解除映射 / 迁移页mmu_notifier_invalidate_range_start()驱动 invalidate 回调要作废 GPU 页表项L 已被本线程占用→ 自死锁 / AB-BA要点触发回收的分配路径与回收回调的 invalidate 路径共用同一把 L是驱动自身在回收路径上被内核回调、对自己锁的重入。memalloc_noreclaim_save()让临界区内分配不进回收从源头切断这条回调。3. 底层原语memalloc_flags_save/memalloc_flags_restore整组接口都建立在两个底层内联函数之上staticinlineunsignedmemalloc_flags_save(unsignedflags){unsignedoldflags~current-flagsflags;/* 只记录本次真正改动的位 */current-flags|flags;returnoldflags;}staticinlinevoidmemalloc_flags_restore(unsignedflags){current-flags~flags;}关键设计是save的返回值~current-flags flags——它只包含本次实际被置上的位。因此当作用域嵌套时若某标志在进入前已被外层置位本次save返回 0对应的restore便不会误清外层设置的位。这保证了各作用域可以安全嵌套。4. 标志家族与语义作用域接口对应进程标志效果memalloc_noio_save/_restorePF_MEMALLOC_NOIO后续分配隐式去掉__GFP_IO等价GFP_NOIO回收不发起 IOmemalloc_nofs_save/_restorePF_MEMALLOC_NOFS后续分配隐式去掉__GFP_FS等价GFP_NOFS回收不回调文件系统memalloc_noreclaim_save/_restorePF_MEMALLOC后续分配隐式加上__GFP_MEMALLOC完全不进入回收并允许动用内存保留区memalloc_pin_save/_restorePF_MEMALLOC_PIN后续分配隐式去掉__GFP_MOVABLE分配到不可迁移区以便长期 pin递进关系NOIO最宽松仅禁 IONOFS更严禁 FS 回调noreclaim最强彻底不回收、动用 reserves。选择哪一级取决于临界区实际会与哪一层产生递归。gfp与这些标志的合并逻辑见同文件的current_gfp_context()对PF_MEMALLOC_NOIO / NOFS / PIN做相应屏蔽以及分配核心对PF_MEMALLOC的处理。5.noreclaim的额外约束PF_MEMALLOCmemalloc_noreclaim_save最强也最危险源码注释给出了明确约束仅当调用者能保证这次分配很快会促成更多内存被释放即「为了释放内存而必须先分配少量内存」时才可使用使用者必须避免耗尽保留区并自行实现基于「已释放内存量」的节流优先考虑预分配池如mempool替代该作用域个别分配可用__GFP_NOMEMALLOC单独退出该作用域不可在中断上下文使用——中断上下文不会获得PF_MEMALLOC的保留区访问权。6. 实战案例1. i915 drop_caches回收自举i915 的调试接口drop_caches会主动运行 GPU shrinker 释放显存对象的后备页见 drivers/gpu/drm/i915/i915_debugfs.cfs_reclaim_acquire(GFP_KERNEL);flagsmemalloc_noreclaim_save();/* 进入不回收作用域 */if(valDROP_BOUND)i915_gem_shrink(NULL,i915,LONG_MAX,NULL,I915_SHRINK_BOUND);if(valDROP_UNBOUND)i915_gem_shrink(NULL,i915,LONG_MAX,NULL,I915_SHRINK_UNBOUND);if(valDROP_SHRINK_ALL)i915_gem_shrink_all(i915);memalloc_noreclaim_restore(flags);/* 还原 */fs_reclaim_release(GFP_KERNEL);这正是noreclaim的教科书式场景i915_gem_shrink*()本身就是回收动作释放 GPU 对象占用的页而它运行过程中可能仍需少量分配。若这些分配再次进入直接回收就会递归地重新触发 shrinker——形成「回收套回收」的自嵌套。用memalloc_noreclaim_save()把整段 shrink 包起来后期间的分配一律不回收从而打破递归与第 5 节所述「为了释放内存而分配、且不能因递归而回收」的约束完全对应。同时注意外层的fs_reclaim_acquire/fs_reclaim_release这是一对 lockdep 注解向内核锁依赖检查声明「此处进入了回收上下文」帮助在开发期提前捕捉潜在的回收递归死锁。保存/还原要点save 与 restore 在同一函数内返回值存入局部变量flagsrestore 时原样传回即可无需像跨函数场景那样借助上下文结构体顺序成对且相反进入是acquire → save退出是restore → release。2. msmGPU 故障现场捕获msm 在 GPU hang / iova fault 的现场捕获路径也用了该作用域见 drivers/gpu/drm/msm/msm_gpu.c 中的msm_gpu_fault_crashstate_capture()mutex_lock(gpu-lock);...noreclaim_flagmemalloc_noreclaim_save();/* 进入不回收作用域 */if(submit){get_comm_cmdline(submit,comm,cmd);submit-fault_dumpedtrue;}/* 记录崩溃现场期间会分配内存保存 dump */pm_runtime_get_sync(gpu-pdev-dev);msm_gpu_crashstate_capture(gpu,submit,fault_info,comm,cmd);pm_runtime_put_sync(gpu-pdev-dev);memalloc_noreclaim_restore(noreclaim_flag);/* 还原 */这里的意图与 i915 略有不同处于 GPU 故障处理路径且持有gpu-lock而崩溃现场捕获msm_gpu_crashstate_capture需要分配不少内存来保存寄存器、命中区等状态。在这种错误处理上下文中若分配进入直接回收既可能递归回 GPU 自身的 shrinker也可能在设备已处于异常状态时阻塞。用memalloc_noreclaim_save()包住捕获过程便避免了在持锁且设备不稳定时因分配而触发回收。两个例子对照可见同一机制的两类典型动机i915是「分配以推进释放」的回收自举避免回收套回收msm是「持锁且设备异常时避免分配路径再递归入驱动」。7. 使用范式与注意事项成对且配对restore的入参必须是同一次save的返回值不能凭空构造否则会错误清除他人设置的位。作用域尽量小标志影响本线程该区间内所有分配范围越大越可能限制无关分配、增加保留区压力。选择恰当的级别能用NOFS解决就不用noreclaimnoreclaim只用于「分配以推进释放」的自举场景。优先预分配对可预估的分配量mempool等预分配手段比进入noreclaim更安全。区分保存位置若 save 与 restore 在同一函数内用局部变量即可若跨函数如 begin/end 分离需存入贯穿两者的上下文结构体。8. 一句话概括memalloc scope 是一组作用于current-flags的进程级开关用「入口 save、出口 restore」的方式为一段临界区统一约束后续所有内存分配的回收行为禁 IO / 禁 FS / 完全不回收 / 禁迁移从而以最小侵入性打破回收递归、避免持锁分配引发的死锁。与上篇的关系。面对「持锁分配可能死锁」两条路线各有适用移出上篇的预分配适合分配可提前、且必须保证临界区不失败的提交路径就地约束本篇的 memalloc scope适合分配无法挪走、甚至分配本身就是为了推进释放如 shrinker 自举的场景。实际代码常两者并用。完整背景见上篇 持锁与内存分配三条约束与预分配范式。
返回列表