内核开发中绕不开一个问题:持锁时到底能不能分配内存。答案取决于锁的类型与锁的使用方式,而非笼统的「持锁与否」。围绕这一点,内核形成了一个反复出现的写法——把一次内存使用拆成两步:先在「宽松」的上下文中预分配好容量(允许失败、可回退),再在「严格」的关键路径中使用它(保证不失败)。
是否需要这样拆分,由三条层层递进的约束决定,它们恰好对应锁语境的三个层面:
- 锁的类型是否允许睡眠;
- 持锁期间的分配会否经回收反向拿锁;
- 以及所持锁保护的提交序列能否容忍失败。
下面沿着「持锁语境」这条主线逐层展开。
1. 锁的类型决定能否睡眠分配
第一个层面看锁的类型。能否在持锁时分配内存,取决于当前上下文能否睡眠,而这由所持锁的类型决定:
| 进程上下文、仅持 mutex/ww_mutex | 能 | GFP_KERNEL 通常可以 |
| 持 spinlock / 关中断 / softirq / RCU 读侧 | 不能 | 只能 GFP_ATOMIC / GFP_NOWAIT |
GFP_KERNEL 含 __GFP_RECLAIM,在内存紧张时会触发**直接回收(direct reclaim)**并可能睡眠。因此在原子上下文中它被禁止——might_sleep() 会告警;而持 mutex/ww_mutex 这类可睡眠锁时,单从「能否睡眠」看,GFP_KERNEL 分配是允许的。
可见,可睡眠锁下就地分配本身是合法的。促使代码采用「预分配 + 关键路径使用」的,是接下来两个更微妙的层面。
2. 约束一:原子上下文不能睡眠
这是最硬的一条,边界清晰,特指原子上下文:
- 持 spinlock、rwlock;
- 关中断(local_irq_disable)、中断处理、softirq/tasklet;
- RCU 读侧临界区(rcu_read_lock)。
在这些上下文里调用可能睡眠的分配是 bug。若确需分配,只能用不睡眠的 GFP_ATOMIC(动用紧急保留,易失败)或 GFP_NOWAIT(尽力而为,更易失败)。这类分配天然可能失败,因此关键路径宁可提前在可睡眠上下文分配好,再进入原子区使用。
KVM 的影子页表填充是典型例子:kvm_mmu_topup_memory_caches() 在进入持 mmu_lock(spinlock)的页表遍历之前,先把每 vCPU 的对象缓存填满;临界区内只从缓存取,不再分配。
3. 约束二:分配可能触发回收,回收可能反向拿锁(锁反转)
这是真正贯穿整个内核的一条,也是那条最容易被忽略却无处不在的约束。
GFP_KERNEL 分配在内存不足时会进入直接回收,而回收路径会反向调用各子系统去释放内存:写回脏页(文件系统、块层)、触发 shrinker、调用 mmu_notifier 使映射失效……如果当前已经持有回收路径也会去获取的锁,就会形成锁反转乃至自死锁:
#mermaid-svg-TILiuUorSTQRLVB9{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-TILiuUorSTQRLVB9 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-TILiuUorSTQRLVB9 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-TILiuUorSTQRLVB9 .error-icon{fill:#552222;}#mermaid-svg-TILiuUorSTQRLVB9 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-TILiuUorSTQRLVB9 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-TILiuUorSTQRLVB9 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-TILiuUorSTQRLVB9 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-TILiuUorSTQRLVB9 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-TILiuUorSTQRLVB9 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-TILiuUorSTQRLVB9 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-TILiuUorSTQRLVB9 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-TILiuUorSTQRLVB9 .marker.cross{stroke:#333333;}#mermaid-svg-TILiuUorSTQRLVB9 svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-TILiuUorSTQRLVB9 p{margin:0;}#mermaid-svg-TILiuUorSTQRLVB9 .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-TILiuUorSTQRLVB9 .cluster-label text{fill:#333;}#mermaid-svg-TILiuUorSTQRLVB9 .cluster-label span{color:#333;}#mermaid-svg-TILiuUorSTQRLVB9 .cluster-label span p{background-color:transparent;}#mermaid-svg-TILiuUorSTQRLVB9 .label text,#mermaid-svg-TILiuUorSTQRLVB9 span{fill:#333;color:#333;}#mermaid-svg-TILiuUorSTQRLVB9 .node rect,#mermaid-svg-TILiuUorSTQRLVB9 .node circle,#mermaid-svg-TILiuUorSTQRLVB9 .node ellipse,#mermaid-svg-TILiuUorSTQRLVB9 .node polygon,#mermaid-svg-TILiuUorSTQRLVB9 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-TILiuUorSTQRLVB9 .rough-node .label text,#mermaid-svg-TILiuUorSTQRLVB9 .node .label text,#mermaid-svg-TILiuUorSTQRLVB9 .image-shape .label,#mermaid-svg-TILiuUorSTQRLVB9 .icon-shape .label{text-anchor:middle;}#mermaid-svg-TILiuUorSTQRLVB9 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-TILiuUorSTQRLVB9 .rough-node .label,#mermaid-svg-TILiuUorSTQRLVB9 .node .label,#mermaid-svg-TILiuUorSTQRLVB9 .image-shape .label,#mermaid-svg-TILiuUorSTQRLVB9 .icon-shape .label{text-align:center;}#mermaid-svg-TILiuUorSTQRLVB9 .node.clickable{cursor:pointer;}#mermaid-svg-TILiuUorSTQRLVB9 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-TILiuUorSTQRLVB9 .arrowheadPath{fill:#333333;}#mermaid-svg-TILiuUorSTQRLVB9 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-TILiuUorSTQRLVB9 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-TILiuUorSTQRLVB9 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-TILiuUorSTQRLVB9 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-TILiuUorSTQRLVB9 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-TILiuUorSTQRLVB9 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-TILiuUorSTQRLVB9 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-TILiuUorSTQRLVB9 .cluster text{fill:#333;}#mermaid-svg-TILiuUorSTQRLVB9 .cluster span{color:#333;}#mermaid-svg-TILiuUorSTQRLVB9 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-TILiuUorSTQRLVB9 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-TILiuUorSTQRLVB9 rect.text{fill:none;stroke-width:0;}#mermaid-svg-TILiuUorSTQRLVB9 .icon-shape,#mermaid-svg-TILiuUorSTQRLVB9 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-TILiuUorSTQRLVB9 .icon-shape p,#mermaid-svg-TILiuUorSTQRLVB9 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-TILiuUorSTQRLVB9 .icon-shape .label rect,#mermaid-svg-TILiuUorSTQRLVB9 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-TILiuUorSTQRLVB9 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-TILiuUorSTQRLVB9 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-TILiuUorSTQRLVB9 :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
持有锁 L
kmalloc(GFP_KERNEL)
内存不足 → 直接回收
回收调用 shrinker / fs 写回 / mmu_notifier
这些路径尝试获取锁 L
自己等自己 → 死锁
内核用两套机制应对:
这条约束在**回收会等待某种「完成信号」**的子系统里尤其尖锐。例如某些异步完成对象存在一个「信号临界区」:回收资源需要等待它完成,而临界区内若分配内存又可能触发回收,回收再去等待另一个尚未完成的信号,最终形成跨子系统的循环等待。内核对这类临界区同样用 lockdep 伪锁建模,禁止在其中做会触发回收的分配——把分配移出临界区,正好规避这一整类风险。
4. 约束三:提交点不容失败(failure atomicity)
前两条讲「能不能」分配,这一条讲「该不该允许失败」——在很多场景里,它才是采用预分配的主因。
许多关键路径存在一个「不可回头点」:一旦跨过它(例如已持有一批锁、已启动一段不可逆的提交序列),后续步骤必须确定性完成。若此刻还可能因 -ENOMEM 中断,错误处理将无从下手:已建立的中间状态难以干净回滚。
通用解法是把「唯一可能失败的分配」提前到可以安全回退的阶段:
#mermaid-svg-bbMOahjXD6fJoR0p{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-bbMOahjXD6fJoR0p .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-bbMOahjXD6fJoR0p .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-bbMOahjXD6fJoR0p .error-icon{fill:#552222;}#mermaid-svg-bbMOahjXD6fJoR0p .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-bbMOahjXD6fJoR0p .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-bbMOahjXD6fJoR0p .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-bbMOahjXD6fJoR0p .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-bbMOahjXD6fJoR0p .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-bbMOahjXD6fJoR0p .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-bbMOahjXD6fJoR0p .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-bbMOahjXD6fJoR0p .marker{fill:#333333;stroke:#333333;}#mermaid-svg-bbMOahjXD6fJoR0p .marker.cross{stroke:#333333;}#mermaid-svg-bbMOahjXD6fJoR0p svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-bbMOahjXD6fJoR0p p{margin:0;}#mermaid-svg-bbMOahjXD6fJoR0p .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-bbMOahjXD6fJoR0p .cluster-label text{fill:#333;}#mermaid-svg-bbMOahjXD6fJoR0p .cluster-label span{color:#333;}#mermaid-svg-bbMOahjXD6fJoR0p .cluster-label span p{background-color:transparent;}#mermaid-svg-bbMOahjXD6fJoR0p .label text,#mermaid-svg-bbMOahjXD6fJoR0p span{fill:#333;color:#333;}#mermaid-svg-bbMOahjXD6fJoR0p .node rect,#mermaid-svg-bbMOahjXD6fJoR0p .node circle,#mermaid-svg-bbMOahjXD6fJoR0p .node ellipse,#mermaid-svg-bbMOahjXD6fJoR0p .node polygon,#mermaid-svg-bbMOahjXD6fJoR0p .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-bbMOahjXD6fJoR0p .rough-node .label text,#mermaid-svg-bbMOahjXD6fJoR0p .node .label text,#mermaid-svg-bbMOahjXD6fJoR0p .image-shape .label,#mermaid-svg-bbMOahjXD6fJoR0p .icon-shape .label{text-anchor:middle;}#mermaid-svg-bbMOahjXD6fJoR0p .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-bbMOahjXD6fJoR0p .rough-node .label,#mermaid-svg-bbMOahjXD6fJoR0p .node .label,#mermaid-svg-bbMOahjXD6fJoR0p .image-shape .label,#mermaid-svg-bbMOahjXD6fJoR0p .icon-shape .label{text-align:center;}#mermaid-svg-bbMOahjXD6fJoR0p .node.clickable{cursor:pointer;}#mermaid-svg-bbMOahjXD6fJoR0p .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-bbMOahjXD6fJoR0p .arrowheadPath{fill:#333333;}#mermaid-svg-bbMOahjXD6fJoR0p .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-bbMOahjXD6fJoR0p .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-bbMOahjXD6fJoR0p .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-bbMOahjXD6fJoR0p .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-bbMOahjXD6fJoR0p .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-bbMOahjXD6fJoR0p .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-bbMOahjXD6fJoR0p .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-bbMOahjXD6fJoR0p .cluster text{fill:#333;}#mermaid-svg-bbMOahjXD6fJoR0p .cluster span{color:#333;}#mermaid-svg-bbMOahjXD6fJoR0p div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-bbMOahjXD6fJoR0p .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-bbMOahjXD6fJoR0p rect.text{fill:none;stroke-width:0;}#mermaid-svg-bbMOahjXD6fJoR0p .icon-shape,#mermaid-svg-bbMOahjXD6fJoR0p .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-bbMOahjXD6fJoR0p .icon-shape p,#mermaid-svg-bbMOahjXD6fJoR0p .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-bbMOahjXD6fJoR0p .icon-shape .label rect,#mermaid-svg-bbMOahjXD6fJoR0p .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-bbMOahjXD6fJoR0p .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-bbMOahjXD6fJoR0p .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-bbMOahjXD6fJoR0p :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
可安全回退(未进入临界区)
失败
成功
提交临界区(不可回头)
使用预分配的容量保证不失败
预分配preload / preallocate / reserve可返回 -ENOMEM
回退,重试或报错
这个范式在内核中反复出现,名字不同、机理一致。按用途大致可分为三类:
数据结构:ID / 索引 / 树节点分配
| IDR | idr_preload() / idr_preload_end() | idr_alloc()(预留段内不失败) |
| radix tree | radix_tree_preload()(填 per-CPU 预留) | radix_tree_insert() |
| XArray | xas_nomem() 重试循环 | xas_store() 在 xa_lock 内不再分配 |
| maple tree(VMA) | mas_preallocate() | mas_store_prealloc() 写侧遍历不再分配节点 |
虚拟化 / IO:缓存池与内存池
| KVM MMU | kvm_mmu_topup_memory_caches() | 持 mmu_lock 的页表构建从 per-vCPU 缓存取用 |
| 块层 IO | mempool_create() / bioset_init() | 内存压力下 mempool_alloc() / bio 提交不失败 |
| tracing | ring_buffer_alloc() 预置页 | ring_buffer_lock_reserve() 写入路径不分配 |
文件系统:事务前预留
| ext4 / jbd2 | ext4_journal_start() 预留 handle credits | 事务内元数据修改不因空间不足而失败 |
| XFS | xfs_trans_reserve() 预留 log space / block | 事务提交阶段确定性完成 |
| btrfs | btrfs_block_rsv_* 预留空间 | 事务内分配从预留中扣减 |
它们共享同一句设计箴言:把可能失败的分配挪到临界区之外,让临界区成为确定性的、不可失败的操作。
5. 三条约束往往叠加出现
上述三条约束并非互斥,真实的热点路径常常同时命中多条,从而更坚定地采用预分配:
- 约束三是最常见的主因:处于提交的不可回头点,不能容忍分配失败;
- 约束二强化它:该路径处在回收 / shrinker / 完成信号的敏感区,即便技术上能在可睡眠锁下分配,也应尽量避免触发回收;
- 约束一在部分路径成立:某些调用点持有 spinlock 或邻近不可睡眠区,睡眠分配本就受限。
以 KVM 影子页表为例:页表构建持 mmu_lock(spinlock,命中约束一),又不能在遍历中途失败(约束三),于是 kvm_mmu_topup_memory_caches() 在加锁前一次性把每 vCPU 缓存填满。VMA 操作的 mas_preallocate() 亦然——先在可失败阶段备好 maple tree 节点,写侧持锁遍历时便不再分配。
预分配往往还能顺带做清理:既然已经付出了一次分配/扩容的代价,就把过期条目的回收一并完成(例如 IDR 预留段整理、树节点复用),让临界区更轻。
6. 小结与判断清单
持锁时能否就地分配内存,由三条约束共同决定:
- 锁的类型决定能否睡眠分配:原子上下文(spinlock / 关中断 / RCU 读侧)禁止睡眠分配(约束一),可睡眠锁下则允许。
- 锁的使用方式决定该不该就地分配:持锁期间的分配可能经回收反向拿锁(约束二,回收递归/锁反转),或落在不容失败的提交序列中(约束三,提交点不容失败)。
实践中判断「这里能不能直接分配」可用三问:
三问中任意一条为「否 / 不允许」,就应采用「预分配 + 临界区不失败」的范式。当三条同时成立时,预分配几乎是唯一正确的选择。
