新闻详情

新闻详情

首页 / 资讯中心 / 详情

可撤销资源管理:内核内存生命周期设计的攻守之道

发布时间:2026/10/2 10:11:21来源:尧图网络
可撤销资源管理:内核内存生命周期设计的攻守之道
开门见山的说第一次在 LWN 上看到把“revocable”单独拎出来讨论的时候我第一反应是这不就是我们日常处理资源释放时一直在做的事吗不管是设备热拔出、驱动模块卸载还是 io_uring 注销缓冲区背后都绕不开同一个核心问题——一个资源已经发出去给使用方了但资源本身可能要先走一步或者需要在某个时间点强制收回来。如果收不回来轻则内存回收卡死重则被释放后的内存还在被设备 DMA 访问直接变成 use-after-free。这篇文章想聊的就是“可撤销资源管理”这套逻辑在内核里是怎么一步步从零散的补丁变成一种值得统一讨论的设计模式的。我会结合 LWN 上相关的讨论背景从内存固定、MMU 通知、Rust 抽象到 io_uring 和驱动热拔的实战场景尽量把“为什么需要可撤销”和“怎么实现可撤销”这两件事讲清楚。适合正在写内核驱动、做系统虚拟化或者关注 io_uring 内存管理的人参考也适合对 Linux 内存生命周期设计感兴趣、想知道“固定内存为什么惹人嫌”的读者。1. 资源“借了不还”到“随时要回”内核资源管理的一次视角反转1.1 内核资源管理里最容易被忽略的中间态传统的内核资源管理绝大多数模型都是“分配、使用、释放”三件套。文件描述符是 open 拿到句柄、使用、close 释放内存页是某个模块持有引用计数、使用、最后 decrement 清零后回收设备实例是 probe 拿到结构体、remove 时销毁。这套模型最舒服的地方在于所有权是线性的谁持有引用谁负责释放不存在“我把资源给你了但你还用着的时候资源必须先没了”的情况。可现实恰恰经常打破这种线性。举一个大家最熟的例子io_uring 注册一组 buffer调用io_uring_register()之后内核会调用pin_user_pages()把这组用户空间页固定住然后设备就可以直接对这些页做 DMA。这个固定的动作本质上就是“借了不还”——理论上页的所有者还是用户空间进程但内核通过增加页引用计数的方式强制这些页不能 swap、不能回收、甚至在某些场景下不能随进程正常退出而干净释放。只有等 io_uring 实例注销 buffer 或者关闭 fd这组页才会被 unpin。问题就出在这个“只有等”上。假如设备不是一个 io_uring 指令执行完就走的简单设备而是一块有独立生命周期、可能随时掉线的硬件比如一块支持热拔的 PCIe 加速卡用户进程已经把 MMIO BAR 映射进地址空间了这时候卡被管理员拔掉了驱动进 remove 回调设备结构体销毁。但进程视图里的映射还在用户还能继续往那个地址读写。你总不能指望进程“自觉”关闭 fd 再退出更不能接受 driver 直接把进程杀掉。于是内核需要一种能力把已经放出去的资源重新收回来哪怕资源持有方还在使用中。1.2 “可撤销”不是新机制而是把分散的保险丝统一化其实内核里早就存在各种“撤销”动作了。比如sysfs删除 device 时会通知相关的 kobject 和属性文件失效cgroup取消某个任务的资源配额时会强制触发回收fd被强制关闭时会通过文件对象的 RELEASE 回调清理等待队列。这些其实都是可撤销资源管理的零星形态。但 LWN 把 “revocable” 这个概念单独拿出来之后我意识到大家真正关心的是能不能把这种“使用权可以随时收回”的能力做成一种通用原语而不是每个子系统各写一套 flag refcount 事件回调的拼盘类比一下可撤销资源很像门禁卡。员工入职时发一张卡这张卡本身代表进入办公区的资格但员工不一定立刻就把卡刷了可能只是带着。公司随时可以通过后台使卡片失效而不是必须看到员工本人再把卡没收。关键是失效之后员工下一次刷卡就会被拒已经在工位里的人不会马上被暴力清退而是在他们离开之后再收走所有权限。注意这里有一个微妙的点撤销不是“把你从椅子上拽起来丢出去”而是“下一次访问时你已经没有资格进来”。这个语义和传统内核里synchronize_rcu()等待读者退出的方式高度一致。所以可撤销资源管理的核心不是增长出一个新功能而是把“使用中资源的突然失效”从异常路径变成一种受支持的基本路径。它要求设计者在最初分配资源的时候就想清楚这个资源能不能被提前收回如果收回正在使用它的读路径要怎么安全退出如果资源被收回后又被重新分配使用方要怎么识别这是一个新对象而不是旧对象1.3 “撤回”这个词的双重含义标题里“撤回 revocable”可以有两层理解。一层是字面上的即通过 revoke 动作把资源撤回另一层则是说在 LWN 的相关讨论中整个可撤销机制本身也像补丁一样经历过多轮“提出、试用、发现问题、被调整”的过程。这其实很贴近内核开发的真实状态一个美好概念落地时总会踩到并发、语义、ABI 稳定性这些硬地板。可撤销资源管理也一样听着简单真正写好非常难。接下来的内容我尽量把这条路上的关键节点都拆开。2. 从“永久固定”到“可撤销”内存页管理的攻守转换2.1 固定内存页的本质是强制换取安全要理解可撤销内存为什么有吸引力得先理解固定内存页pinned page为什么让人又爱又恨。内核里最常见的固定页接口是pin_user_pages()它和老的get_user_pages()GUP不太一样。pin_user_pages()显式地使用FOLL_PIN标志并配合unpin_user_page()释放。它做的事情本质是把用户空间页面的引用计数增高同时标记这个页处于“被钉住”状态。这样页虽然还在用户空间的 VMA 里虚拟地址仍然指向它但内核在回收页、做迁移、做 swap 的时候必须跳过这些页因为它们背后有 DMA 设备可能正在访问。这个设计的思路是设备 DMA 用的是物理地址不能跟随用户进程的页表走。如果用户进程缺页了页被换出去了设备还在往旧物理地址写数据那就直接写进别人的内存里。为了防止这种事故干脆把页固定住让它在整个 DMA 生命周期内不能动。听起来很合理对不对但代价也很直白固定页越多内核内存管理的弹性就越差。很多时候我们并不需要一直固定页面比如设备只在某一小段命令下发时访问该内存其余时间根本不碰但传统模型下这组页从注册到注销都要被钉住。更糟的是固定页在某些边界条件下会形成“死局”。比如用户进程调用了fork()子进程临时继承了相关映射如果父进程准备退出而子进程没有主动 unpin页的生命周期可能被无限拉长。再比如用户进程中注册了固定页但进程因某种原因被 kill 掉关闭 io_uring fd 的路径如果没做对内存就滞留在那里回收器拿它毫无办法。系统的内存压力会在最不恰当的时候暴露这类问题。2.2 反过来想让设备映射本身可以被作废可撤销内存的思路是把“物理页不能动”变成“设备想用的时候先确认一下确认不了就作废”。具体落地时内核会用几种手段配合。第一看 mmu notifier。设备驱动或者类似 KVM、io_uring 这类组件可以在进程的 MM 上注册一个mmu_notifier。当用户进程的页表发生变化比如 VMA 被截断、页面被 swap out、页面被迁移内核会回调mmu_notifier_invalidate_range_start/end。设备收到这条通知后就会暂停对这个范围内的内存进行 DMA或者把 DMA 请求直接撤销掉。这样就不需要死死固定住每一页因为页表本来就要负责任何时刻的映射关系设备只是搭了个“顺风车”去感知变化。再说 IOMMU。IOMMU 可以给设备建立独立的设备页表用户空间物理页不再直接暴露给设备而是由 IOMMU 做一次地址翻译。当要撤销某个 DMA 映射时直接在 IOMMU 里把对应映射拔掉设备下一次访问就会出现地址错误或者得到一个被填成 0 的“哑页”。这其实是很多虚拟化方案里防 DMA 攻击的标准做法。可撤销和这个思路天然契合因为撤销动作发生在设备和物理内存之间的翻译层而不是发生在用户进程自己的页表里所以对进程开闭映射的侵入性很小。第三种是隔离一个“伪页”做毒丸。某些场景下设备必须有一块固定的物理页做 DMA但实在不能允许它长期占着真正重要的内存页。这时候可以在撤销时把设备的 DMA 映射指向一个专用毒丸页所有撤销之后的访问要么读到 0要么得到错误状态。这个手段听起来粗暴但在驱动热拔路径里非常实用因为热拔真正要解决的往往不是内存回收而是“设备还可能继续写内存”这种安全问题。2.3 固定页和可撤销页的关键对比维度固定页Pinned可撤销映射Revocable页能否被回收/迁移不能引用计数被钉住可以撤销回调先解除设备访问设备访问安全性靠物理页不过期保证靠通知机制保证设备在作废后停止访问实现位置主要在 GUP 相关路径mmu notifier、IOMMU、设备驱动回调访问热路径开销几乎为零多了一层守护逻辑可能增加锁或 RCU 读典型适用场景RDMA 短促但确定的 DMA 区间设备热拔、长时间持有但可中断的映射出错后的后果内存回收受阻潜在 OOM使用方可能读到错误结果需要错误处理从这张表能看出可撤销映射不是要全面替代固定页而是给“生命周期跨度比较长、使用者不一定配合释放”的场景兜底。用到固定页没有问题但要意识到固定页本质上是在透支系统的内存柔韧性当资源使用跨度从毫秒变成分钟、小时甚至天可撤销几乎成了必选项。3. Rust-for-Linux 的 Revocable 用语言特性把撤销做进类型系统3.1 为什么 Rust 抽象对可撤销特别友好LWN 讨论可撤销时绕不开的一个方向是 Rust-for-Linux 里的RevocableT抽象。内核模块用 C 写撤销逻辑通常靠什么呢靠结构体里的一个active标志、一把锁、一堆引用计数然后每个人都要记得在访问前检查标志、在访问后释放锁。这套流程一旦分散在多处就很容易出现疏漏。今天少了一个检查明天某条新路径忘了加锁问题就会在特定竞态下爆发。Rust 抽象要解决的问题是把“必须先检查撤销状态再获取访问权限”这件事固化成类型签名。你可以直接使用RevocableT包住一块资源然后所有对T的访问都不再直接通过裸指针而是必须调用access()拿到一个受保护的 guard。guard 存在时撤销线程会等待 guard 释放guard 不存在时你压根拿不到内部数据。这就把运行时约定变成了编译时约束哪怕代码里出现了某条新调用路径只要它没有正确获取 guard编译器就会拦下。3.2 一个可撤销包装器的大致形态社区常见的 Revocable 抽象大体会长这样pub struct RevocableT: ?Sized { value: UnsafeCellT, revoked: AtomicBool, // 用于协调撤销等待的字段这里省略底层实现 }从使用者的角度看初始化时传入需要保护的值之后就再也拿不到T或mut T只能通过访问接口拿 guardimplT RevocableT { pub fn revocable_init(data: T) - Self { ... } pub fn try_access(self) - OptionRevocableGuard_, T { if self.is_revoked() { return None; } // 获取访问权返回一个持有引用计数的 guard } pub fn revoke(self) - OptionT { // 标记撤销等待所有活跃访问结束取回内部值 } }RevocableGuard实现了Deref所以可以像普通引用一样去读取和访问内部值。不过要注意这是个只读或受锁保护的访问权它保证的是“在 guard 存活期间内部值不会被撤销并释放”。至于能不能写取决于实现是给共享访问还是独占访问一般来说如果把 Revocable 当作一把锁保护的资源写操作也应该在里面完成。撤销线程调用revoke()时会先把撤销标志置位意味着后续try_access()都会返回None。同时它会等待当前已经成立的 guard 全部 drop 掉。这个等待行为和 RCU 的synchronize_rcu()非常像旧读者必须全部退场新的读者已经进不来。等所有人都退场后撤销线程就可以安全取回T的所有权比如把它送到销毁队列或者重新初始化给下一任使用者。3.3 这类抽象为什么能减少内核里大量的手工协议我见过很多内核驱动里靠“注销函数”来手工保护设备数据结构比如一个字符设备驱动open 时把file-private_data指向驱动私有数据release 时清空。如果驱动的核心结构体因为热拔被释放了而用户进程恰好还持着file描述符私有数据指针就成了悬空指针。传统做法通常是用一个kref延迟释放或者靠dev_get_drvdata()拿到的指针去碰运气。如果用 Revocable 的思路就可以把“设备是否还活着”的状态内建到访问路径里。每次 open 拿到的不是原始指针而是一个try_access()的 guard。设备撤销时这个 guard 就会拿不到open 就返回错误码用户进程会得到一个清晰 EIO。这比“拿到悬空指针后随机崩溃”要好太多。但它也有代价。最直观的是性能现在每次访问都要多一轮撤销状态检查和可能的原子操作或 RCU 读区进入。对于十微秒级别的控制路径这无所谓对于每包几十字节的网络报文热路径就是灾难。所以在真实模块里Revocable 更常出现在“控制面”资源上而不是“数据面”路径上。这也是我特别想在文章里强调的一点好抽象不能解决所有性能问题它只是把安全的成本放到明面上。4. io_uring 与设备热拔可撤销资源管理的两大练兵场4.1 io_uring 注册资源里的撤销演练io_uring 这种异步接口对资源生命周期的要求很有意思。注册 buffer 时内核得到一个用户空间的地址范围这个范围不仅要能访问还要能保证在收发请求期间不会被底层页表变化影响。之前很多人在内核邮件列表里反复讨论 io_uring 注册 buffer 的“过度固定”问题就是因为固定页导致的内存压力很棘手。如果引入可撤销就可以改变这个时间窗注册 buffer 时不再把页面永久固定而是为整个注册区间建立一套可撤销映射DMA 发起前再确认映射仍然有效。一旦遇到内存回收压力内核可以选择先撤销一部分映射让页面恢复弹性io_uring 侧收到撤销通知后会暂停后续请求或者把相关请求返回EFAULT。这个思路和 KVM 处理用户内存页迁移的方式非常相似KVM 早就通过 mmu notifier 做到了这一点。不过 io_uring 的痛点比 KVM 更尖锐因为 io_uring 的请求是异步的。撤销发生时可能有一批请求正在排队甚至已经在硬件队列里跑着。此时如果直接把内存映射撤销硬件 DMA 可能会写到已经回收的页面里。所以 io_uring 的可撤销实现必须考虑请求的生命周期边界是在提交点检查映射还是在完成点检查管线较长的情况下能否在每阶段做一次状态确认这些问题没有标准答案都在 LWN 对应讨论里反复拉锯。4.2 设备热拔路径最不可预测的撤销触发点设备热拔可能是最考验可撤销资源管理的地方。这里有一个特点设备失效的原因通常不是软件主动调用而是硬件层面的物理断开。PCIe 设备被移除时驱动会收到remove()回调但这个回调的精确时机和用户进程里的 mmap 映射、正在执行的 DMA、以及内核中对该设备的引用计数并不天然同步。一个比较现实的例子是 GPU 驱动。用户进程通过 DRM 接口分配了一段视频内存并把这个视频内存映射到自己的地址空间。如果 GPU 设备被强制热拔这块视频内存实际上已经被切断了但用户进程的映射还挂在页表里。用户进程下次访问这个地址应该得到一个错误信号而不是卡在总线错误循环里。要做到这一点驱动就得在 remove 路径里用某种 revoke要么把这个范围的页表改指到毒丸页要么通过 mmu notifier 主动丢一个无效化通知让用户进程重新触发缺页异常时发现映射已经不可用。这个场景和 io_uring 的场景结合就出现了另一类复杂问题io_uring 注册了该设备提供的内存映射设备热拔后io_uring 请求队列里还挂着和这块内存相关的请求。两者要联动撤销而不是各撤各的。可现在很多驱动连自己的撤销都还没写好更别说跨子系统的联动。所以我在实际看相关代码时始终建议把“撤销的有序性”放在设计文档里先撤设备 DMA再撤内存映射最后通知使用方。顺序反了任何抽象都救不了。4.3 什么样的资源其实不该做成可撤销如果所有资源都做成可撤销系统会陷入另一种困境过度防御带来的复杂度和死锁风险。可撤销资源要求每次访问都进入受控路径如果资源本身生命周期很明确、使用者退出路径也很清晰直接销毁才是更高效的选择。举两个我的判断标准。第一个是资源生命周期极短的比如一两个时钟周期内就完成的寄存器访问。这种访问不值得为了撤销去加锁、加 RCU 读区直接把硬件资源从系统里摘掉并同步等待硬件完成即可。第二个是资源使用频率极高、每次操作开销必须极小的比如每网络包都要访问的元数据。就算用 RCU 保护RCU 读区的 CPU 开销在每包路径上也可能放大成明显的性能回退。可撤销更适合放在“偶发访问、持有时间长、失败可以接受”的资源上。5. 撤销逻辑容易踩的坑以及我常用的验证思路5.1 撤销和访问并发竞态中最隐蔽的 ABA 问题很多人以为可撤销资源的难点只在“如何安全拿走资源”其实更坑的是“撤销之后资源身份被重新使用”。我见过一个模块定义了设备结构体先 revoke再 kfree。看起来没问题因为 revoke 已经等待所有 guard 结束了。但问题是 revoke 之后设备结构体还没释放新来的 hotplug 事件又触发了一次 probe新设备复用了同一块内存指针值恰好和旧设备一样。此时某个已经过了一次try_access()的代码可能以为自己还拿着旧设备的有效访问权实际上访问到的已经是新设备的数据。要避免 ABA核心是不要让“访问权”跨过撤销点长期缓存。Rust 抽象里的 guard 之所以安全是因为 guard 有明确的生命周期撤销线程会等待 guard 释放不可能出现“撤销已经发生guard 还活着”的状态。而 C 里手动维护的访问权就很容易“游荡”。我把这条经验写进自己的检查清单所有的撤销状态检查结果都要限定在最小临界域里使用不要拿出去反复确认。5.2 验证撤销路径比验证正常路径更重要很多驱动测试只测正常生命周期open、访问、close跑得欢。可撤销资源管理里真正的 bug 往往出现在竞态窗口比如撤销线程正在执行的同一时刻另一个线程刚好在try_access成功之后做第一次写入。这个问题常规单元测试很难暴露因为它是时间窗口问题不是逻辑必然问题。我常用的做法有几个。第一是给撤销点人为加延迟让竞态窗口变大比如在 revoke 函数入口处插入一个msleep或者 udelay再跑高压并发。窗口大了问题就更容易复现。第二是用 syzkaller 或者自定义的 syscall 压力工具不断做“创建资源、注册映射、触发热拔、重新 probe”的循环。第三是在撤销后的旧映射区域故意填上毒丸值比如 0xDEADBEEF然后在测试里断言某些 DMA 路径绝对不能访问到毒丸值。如果毒丸被访问到了说明撤销没有真正阻止 DMA 穿透安全边界已经破防。5.3 常见误解revoke 是同步销毁还是异步标记我见过不少人把 revoke 理解成“直接销毁”。实际上它更偏向“下一次访问失败”。在 LWN 讨论可撤销的语境里几乎所有的可撤销实现都是先标记、再同步等待、最后才是可选销毁。如果你把销毁做在标记之前那把资源拿走那一刻正在访问的人就立刻踩悬空指针。如果你把等待做在标记之后但忘了等待新来的访问也许还能通过状态检查混进来。这两类错误的症状不同本质都一样没有理解 revoke 是一个状态转换而不是一个瞬时销毁动作。配套地还要注意 revoke 的幂等性。因为热拔回调、io_uring 注销、模块卸载这几个撤销路径可能在不同层级上都触发同一个资源的撤销如果revoke()不是幂等的就会重复释放或者重复发送通知。理想的做法是revoke()只返回原来的值一次后续调用返回None。这样即使多次调用也不会出现双重释放。6. 我对可撤销资源管理的个人实操体会如果让我对 revocable 总结一句话我会说它本质上是一种把“资源所有者的意志”和“资源使用者的瞬时访问”解耦的设计。过去我们靠引用计数强制所有使用者点头之后才能销毁可撤销模型则允许资源所有者单方面宣告“资源已失效”同时保证正在使用中的人能安全退场。我在实际写模块时越来越倾向于把可撤销当作“最后一道保险”来用而不是当作常规路径的性能担当。凡是能用生命周期明确、短持有所规避的场景我就用裸指针加引用计数凡是绕不开用户空间长期持有、硬件可能热拔的场景我才引入 Revocable。理由很简单错误处理越少代码越稳撤销机制越靠内嵌调试难度越低。最后再分享一个小技巧设计可撤销资源时先把“撤销之后使用者能拿到什么错误码”定义清楚再写实现。是EIO、EFAULT还是ENODEV一旦定下来使用者就有明确的二分路径。最忌讳的是撤销之后返回一些模棱两可的状态让上层逻辑无法判断到底是设备没了、还是请求本身参数错了。撤销的边界越清晰内核里其它组件就越容易配合出正确的行为。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

DolphinScheduler tenant not exists 根因与修复指南 2026/10/2 11:00:10

DolphinScheduler tenant not exists 根因与修复指南

1. 为什么“tenant not exists”不是配置错了,而是系统在拒绝你登录DolphinScheduler 的租户配置,是整个调度平台权限体系的基石。但凡接触过 3.0 版本部署运维的人,几乎都见过那个红色弹窗——“tenant not exists”。它不像常见的 404 或 5…

阅读更多 →
NestJS生产级限流实战:从装饰器到滑动窗口与可观测性 2026/10/2 11:00:03

NestJS生产级限流实战:从装饰器到滑动窗口与可观测性

1. 为什么 Nest 的限流不是加个装饰器就完事了?在 NestJS 生态里,“限流”这个词常被新手误读成一个“开箱即用”的功能开关——看到Throttle()装饰器,以为贴上就能防爆;看到nestjs/throttler包名,以为装完 npm instal…

阅读更多 →
DTFT核心性质全推导:从定义到卷积定理与Parseval定理 2026/10/2 11:00:03

DTFT核心性质全推导:从定义到卷积定理与Parseval定理

干了这么多年信号处理教学和工程实践,我一直有个很深的体会:很多人在考试或者面试前,会把“离散系统傅里叶变换”(DTFT)的常用结论背得滚瓜烂熟,但真被问到“这个结论是怎么来的”时,往往说不出…

阅读更多 →
基于低频FRF矩阵的刚性体惯性参数反演方法 2026/10/2 10:59:57

基于低频FRF矩阵的刚性体惯性参数反演方法

简介:本资源是一份面向机械工程领域研究人员与工程师的刚性体惯性参数识别技术实践指南,聚焦频响函数(FRF)驱动的参数辨识方法,解决复杂结构(如发动机、航天器部件)在多体动力学仿真中惯性参数难…

阅读更多 →
叠纸闪耀暖暖的3D换装工业化实践 2026/10/2 10:59:56

叠纸闪耀暖暖的3D换装工业化实践

1. 项目概述:当换装游戏不再只是“贴图”,而是一场三维建模的工业革命“叠纸”“闪耀暖暖”“2D到3D”——这三个词在2019年前后几乎同时引爆国内二次元与游戏美术圈。不是因为某张新皮肤海报,也不是因为某个联动活动,而是叠纸在一…

阅读更多 →
开源三天 9900 star!把 CodeX 塞进 Claude Code 后,TaoToken 统一 Key 怎么配? 2026/10/2 10:59:44

开源三天 9900 star!把 CodeX 塞进 Claude Code 后,TaoToken 统一 Key 怎么配?

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉