对象已经 kfree,系统空闲内存却没有立即增加。这一现象需要两本账解释:调用者还持有多少对象,分配器还保留多少承载页。
从一个 200 字节对象开始,先算槽内开销与备用容量,再追踪分配、跨 CPU 释放、失败回滚和地址复用。
| 要解释的现象 | 阅读位置 |
| 200 字节对象为什么占用更多空间? | 第 1 节:对象、槽与整页 |
| kmalloc、cache 和 kvmalloc 如何选择? | 第 2 节:申请与释放契约 |
| kfree 之后页面何时归还? | 第 3~4 节:缓存与所有权 |
| 泄漏和 UAF 的证据有什么不同? | 第 5~6 节:检测与只读统计 |
1. 用 200 字节对象算清三种空间
假设业务需要 200 字节,所用 cache 的实际对象步长为 256 字节,承载页为 4096 字节,并忽略页外管理元数据。一个 slab 页可以放 16 个槽;若 16 个槽全部占用,业务有效内容为 3200 字节,槽内对齐等开销合计 896 字节。
若只有 10 个对象存活,同一页的 4096 字节应这样记账:
| 去向 | 计算 | 字节数 |
| 业务内容 | 10 × 200 | 2000 |
| 已用槽内部开销 | 10 × (256 − 200) | 560 |
| 可复用空槽 | 6 × 256 | 1536 |
| 合计 | 一张承载页 | 4096 |
除了业务内容之外的 2096 字节,有明确的用途,不能全部算成泄漏。
少量长寿命对象,为什么能留住很多页
如果两个 slab 各只有一个对象存活,总共只有 400 字节业务内容,却可能仍需保留两页。分配器不能把任何对象随意搬到一起,因为调用者保存的是指针,任意搬迁会使这些指针失效。某些子系统可以通过自己的回收协议释放对象,但这与页整理迁移用户页的能力不同。
这也解释了为什么增大 slab order 有双面效果:一次获取更多对象槽可减少向共享资源补充的频率,却更依赖高阶连续页;一个长寿命对象又可能使更大的承载块不能归还。调试 redzone、分配追踪和对齐会改变对象步长,因此启用检查后空间占用与分配成功率可能变化。见 slab 管理与调试。
保持对象数量相同,切换集中与分散的布局。下面按同一套槽尺寸把内容、槽内开销、空槽和承载页分别记账;点击槽位也能构造自己的寿命分布。
SP=Nq+N(s−q)+(Sn−N)s,n=P/s
本模型每页 n=16 槽,P=4096 B、q=200 B、s=256 B;S 是仍有存活对象的页数,N 是存活对象总数。
正在呈现知识画面
同样四个对象,为什么能占住四倍的页?有效内容、已用槽的内部开销和整页保留是不同数量。分散的少量活对象能让多张 slab 无法归还,释放一个槽也不等于释放一页。
{"id":"slab-retained-pages","title":"同样四个对象,为什么能占住四倍的页?","summary":"有效内容、已用槽的内部开销和整页保留是不同数量。分散的少量活对象能让多张 slab 无法归还,释放一个槽也不等于释放一页。","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><div class=\"control\"><label for=\"count\" data-i18n=\"count\"></label><output for=\"count\" id=\"count-value\"></output><input id=\"count\" type=\"range\" min=\"0\" max=\"64\" value=\"4\" step=\"1\"></div><div class=\"presets\" role=\"group\" data-i18n-label=\"layout\"><button data-mode=\"packed\" data-i18n=\"packed\"></button><button data-mode=\"spread\" data-i18n=\"spread\"></button></div><div class=\"slabs\"><section class=\"card slab\" id=\"slab-0\"><header>slab 0</header><div class=\"slots\"><button data-slot=\"0\" aria-label=\"slab 0, slot 0\"><i></i></button><button data-slot=\"1\" aria-label=\"slab 0, slot 1\"><i></i></button><button data-slot=\"2\" aria-label=\"slab 0, slot 2\"><i></i></button><button data-slot=\"3\" aria-label=\"slab 0, slot 3\"><i></i></button><button data-slot=\"4\" aria-label=\"slab 0, slot 4\"><i></i></button><button data-slot=\"5\" aria-label=\"slab 0, slot 5\"><i></i></button><button data-slot=\"6\" aria-label=\"slab 0, slot 6\"><i></i></button><button data-slot=\"7\" aria-label=\"slab 0, slot 7\"><i></i></button><button data-slot=\"8\" aria-label=\"slab 0, slot 8\"><i></i></button><button data-slot=\"9\" aria-label=\"slab 0, slot 9\"><i></i></button><button data-slot=\"10\" aria-label=\"slab 0, slot 10\"><i></i></button><button data-slot=\"11\" aria-label=\"slab 0, slot 11\"><i></i></button><button data-slot=\"12\" aria-label=\"slab 0, slot 12\"><i></i></button><button data-slot=\"13\" aria-label=\"slab 0, slot 13\"><i></i></button><button data-slot=\"14\" aria-label=\"slab 0, slot 14\"><i></i></button><button data-slot=\"15\" aria-label=\"slab 0, slot 15\"><i></i></button></div><small id=\"live-0\"></small></section><section class=\"card slab\" id=\"slab-1\"><header>slab 1</header><div class=\"slots\"><button data-slot=\"16\" aria-label=\"slab 1, slot 0\"><i></i></button><button data-slot=\"17\" aria-label=\"slab 1, slot 1\"><i></i></button><button data-slot=\"18\" aria-label=\"slab 1, slot 2\"><i></i></button><button data-slot=\"19\" aria-label=\"slab 1, slot 3\"><i></i></button><button data-slot=\"20\" aria-label=\"slab 1, slot 4\"><i></i></button><button data-slot=\"21\" aria-label=\"slab 1, slot 5\"><i></i></button><button data-slot=\"22\" aria-label=\"slab 1, slot 6\"><i></i></button><button data-slot=\"23\" aria-label=\"slab 1, slot 7\"><i></i></button><button data-slot=\"24\" aria-label=\"slab 1, slot 8\"><i></i></button><button data-slot=\"25\" aria-label=\"slab 1, slot 9\"><i></i></button><button data-slot=\"26\" aria-label=\"slab 1, slot 10\"><i></i></button><button data-slot=\"27\" aria-label=\"slab 1, slot 11\"><i></i></button><button data-slot=\"28\" aria-label=\"slab 1, slot 12\"><i></i></button><button data-slot=\"29\" aria-label=\"slab 1, slot 13\"><i></i></button><button data-slot=\"30\" aria-label=\"slab 1, slot 14\"><i></i></button><button data-slot=\"31\" aria-label=\"slab 1, slot 15\"><i></i></button></div><small id=\"live-1\"></small></section><section class=\"card slab\" id=\"slab-2\"><header>slab 2</header><div class=\"slots\"><button data-slot=\"32\" aria-label=\"slab 2, slot 0\"><i></i></button><button data-slot=\"33\" aria-label=\"slab 2, slot 1\"><i></i></button><button data-slot=\"34\" aria-label=\"slab 2, slot 2\"><i></i></button><button data-slot=\"35\" aria-label=\"slab 2, slot 3\"><i></i></button><button data-slot=\"36\" aria-label=\"slab 2, slot 4\"><i></i></button><button data-slot=\"37\" aria-label=\"slab 2, slot 5\"><i></i></button><button data-slot=\"38\" aria-label=\"slab 2, slot 6\"><i></i></button><button data-slot=\"39\" aria-label=\"slab 2, slot 7\"><i></i></button><button data-slot=\"40\" aria-label=\"slab 2, slot 8\"><i></i></button><button data-slot=\"41\" aria-label=\"slab 2, slot 9\"><i></i></button><button data-slot=\"42\" aria-label=\"slab 2, slot 10\"><i></i></button><button data-slot=\"43\" aria-label=\"slab 2, slot 11\"><i></i></button><button data-slot=\"44\" aria-label=\"slab 2, slot 12\"><i></i></button><button data-slot=\"45\" aria-label=\"slab 2, slot 13\"><i></i></button><button data-slot=\"46\" aria-label=\"slab 2, slot 14\"><i></i></button><button data-slot=\"47\" aria-label=\"slab 2, slot 15\"><i></i></button></div><small id=\"live-2\"></small></section><section class=\"card slab\" id=\"slab-3\"><header>slab 3</header><div class=\"slots\"><button data-slot=\"48\" aria-label=\"slab 3, slot 0\"><i></i></button><button data-slot=\"49\" aria-label=\"slab 3, slot 1\"><i></i></button><button data-slot=\"50\" aria-label=\"slab 3, slot 2\"><i></i></button><button data-slot=\"51\" aria-label=\"slab 3, slot 3\"><i></i></button><button data-slot=\"52\" aria-label=\"slab 3, slot 4\"><i></i></button><button data-slot=\"53\" aria-label=\"slab 3, slot 5\"><i></i></button><button data-slot=\"54\" aria-label=\"slab 3, slot 6\"><i></i></button><button data-slot=\"55\" aria-label=\"slab 3, slot 7\"><i></i></button><button data-slot=\"56\" aria-label=\"slab 3, slot 8\"><i></i></button><button data-slot=\"57\" aria-label=\"slab 3, slot 9\"><i></i></button><button data-slot=\"58\" aria-label=\"slab 3, slot 10\"><i></i></button><button data-slot=\"59\" aria-label=\"slab 3, slot 11\"><i></i></button><button data-slot=\"60\" aria-label=\"slab 3, slot 12\"><i></i></button><button data-slot=\"61\" aria-label=\"slab 3, slot 13\"><i></i></button><button data-slot=\"62\" aria-label=\"slab 3, slot 14\"><i></i></button><button data-slot=\"63\" aria-label=\"slab 3, slot 15\"><i></i></button></div><small id=\"live-3\"></small></section></div><div class=\"legend\"><span data-i18n=\"payload\"></span><span class=\"second\" data-i18n=\"padding\"></span></div><div class=\"space-bar\"><i id=\"payload-bar\"></i><i id=\"padding-bar\"></i><i id=\"empty-bar\"></i></div><div class=\"metrics\"><div><span data-i18n=\"payload\"></span><strong id=\"payload\"></strong></div><div><span data-i18n=\"padding\"></span><strong id=\"padding\"></strong></div><div><span data-i18n=\"capacity\"></span><strong id=\"empty-space\"></strong></div><div><span data-i18n=\"retained\"></span><strong id=\"retained\"></strong></div></div><div class=\"formula\" id=\"sum\"></div><div class=\"copy\"><p id=\"explanation\" role=\"status\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"emptyNote\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"packedNote\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"spreadNote\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"customNote\"></p></div><details class=\"scope\"><summary data-i18n=\"scopeLabel\"></summary><p data-i18n=\"scope\"></p></details>","css":".intro{margin:8px 0 18px;color:var(--muted);font-size:14px}.caption,.hint{font-size:12px;color:var(--muted);margin-top:12px}.control{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px 12px;align-items:center;margin:14px 0;font-size:13px}.control label{font-size:13px}.control output{color:var(--accent);min-width:5em;text-align:right;font-variant-numeric:tabular-nums}.control input{grid-column:1/-1;width:100%;margin:0}.presets{display:flex;gap:4px;align-items:stretch}.presets button{flex:1 1 0;min-width:0;overflow-wrap:anywhere;font-size:13px}.actions{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}.actions button{flex:1;min-width:0;font-size:13px;min-height:42px}.copy{display:grid;font-size:14px;margin:16px 0}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.scope{margin-top:14px;color:var(--muted);font-size:12px}.scope summary{padding:8px 0;cursor:pointer}.scope p{margin-top:10px}.two{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.card{border:1px solid var(--rule);border-radius:9px;padding:14px;min-width:0}.card header{font-size:14px;font-weight:600;margin-bottom:10px}.card button{font-size:12px;width:100%;min-width:0}.card small{display:block;font-size:12px;color:var(--muted)}.card code{font-size:12px;overflow-wrap:anywhere}.metrics{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.metrics>div{border-left:2px solid var(--accent);padding-left:10px;min-width:0}.metrics span{display:block;min-height:3.2em;color:var(--muted);font-size:12px}.metrics strong{font-size:23px;display:block;font-weight:500;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.formula{border-top:1px solid var(--rule);padding-top:12px;margin-top:14px;font-size:13px;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.value{font:500 20px var(--viz-font-ui)}.legend{display:flex;flex-wrap:wrap;gap:6px 14px;font-size:12px;color:var(--muted);margin:12px 0}.legend span:before{content:'';display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--accent);margin-right:6px}.legend .second:before{background:var(--second)}.check{display:flex;align-items:center;gap:8px;font-size:13px;margin:12px 0}.check input{width:16px;height:16px;flex-shrink:0;accent-color:var(--accent)}select,input[type=text],input[type=number]{background:var(--paper);color:var(--ink);font:inherit;font-size:13px;padding:10px;border:1px solid var(--rule);border-radius:7px;max-width:100%;min-width:0}select{width:100%;min-height:42px}.band{height:20px;display:flex;background:var(--surface);border-radius:4px;overflow:hidden}.band i{height:100%;display:block;background:var(--accent);transition:width .25s}.band i.second{background:var(--second)}button:disabled{cursor:default}.status{font-size:13px;min-height:4.8em}.mono{font-family:var(--viz-font-mono);font-variant-numeric:tabular-nums}.muted{color:var(--muted)}\n@media(max-width:480px){.two{gap:8px}.card{padding:12px 9px}.actions button,.presets button{font-size:12px}.metrics strong{font-size:20px}.metrics{gap:8px}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n.track-row{margin:16px 0}.track-row>span{display:block;font-size:12px;color:var(--muted);margin-bottom:7px}.track{display:flex;gap:5px;min-width:0}.cell{flex:1;min-width:0;min-height:48px;border:1px solid var(--rule);border-radius:5px;background:var(--surface);display:flex;align-items:center;justify-content:center;text-align:center;padding:5px 3px;font-size:12px;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.cell.on{background:var(--accent-soft);border-color:var(--accent)}.cell.gold{background:var(--second-soft);border-color:var(--second)}.cell.empty{border-style:dashed;color:var(--muted)}.cell.error{border-color:var(--second);text-decoration:line-through}.choices{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;margin:14px 0}.choices button{min-width:0;padding:10px 4px;font-size:12px;overflow-wrap:anywhere}.choices button[aria-current=true]{border-color:var(--accent);background:var(--accent-soft)}.relation{display:grid;grid-template-columns:minmax(0,1fr) 28px minmax(0,1fr);align-items:center;gap:6px;margin:12px 0}.relation .cell{min-height:72px}.relation>i{text-align:center;font-style:normal;color:var(--accent)}.readout{font-size:13px;white-space:pre-line;min-height:6.4em;overflow-wrap:anywhere}.plot{position:relative;border-left:1px solid var(--rule);border-bottom:1px solid var(--rule);padding:8px 0;margin:18px 0}.plot-row{height:28px;position:relative;margin:4px 0;background:var(--surface)}.plot-row i{position:absolute;inset-block:5px;background:var(--accent);min-width:2px;transition:width .2s,left .2s}.plot-row b{position:relative;padding:4px 6px;font:11px/28px var(--viz-font-ui);background:var(--paper);border-radius:3px}.axis{display:flex;justify-content:space-between;font-size:11px;color:var(--muted)}\n\n.slabs{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:18px 0}.slab{transition:opacity .2s}.slab.empty{opacity:.5}.slots{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:4px}.slots button{min-width:0;min-height:26px;height:26px;padding:0;border:1px solid var(--rule);background:var(--surface);border-radius:3px;display:block;overflow:hidden}.slots button.live{background:var(--second-soft);border-color:var(--second)}.slots button i{display:block;height:100%;width:78.125%;background:transparent}.slots button.live i{background:var(--accent)}.slab small{min-height:3.2em;margin-top:10px}.space-bar{display:flex;height:24px;border:1px solid var(--rule);background:var(--paper);margin:16px 0}.space-bar i{display:block;height:100%;background:var(--accent);transition:width .25s}#padding-bar{background:var(--second)}#empty-bar{background:var(--surface)}.formula{min-height:4.8em}@media(max-width:480px){.slabs{gap:8px}.slab{padding:12px 6px}.slots{gap:3px}}","js":"const $=s=>document.querySelector(s);let slots=Array(64).fill(false),mode='packed';function seed(n){slots.fill(false);for(let i=0;i<n;i++)slots[mode==='spread'?(i%4)*16+Math.floor(i/4):i]=true;}function draw(){const n=slots.filter(Boolean).length,live=Array.from({length:4},(_,s)=>slots.slice(s*16,s*16+16).filter(Boolean).length),retained=live.filter(Boolean).length*4096,payload=n*200,padding=n*56,empty=retained-n*256;$('#count').value=n;$('#count-value').textContent=n;document.querySelectorAll('[data-slot]').forEach(e=>{e.classList.toggle('live',slots[+e.dataset.slot]);e.setAttribute('aria-pressed',String(slots[+e.dataset.slot]));});for(let s=0;s<4;s++){$('#slab-'+s).classList.toggle('empty',!live[s]);$('#live-'+s).textContent=live[s]?viz.t('alive')+' '+live[s]+' / 16':viz.t('returned');}for(const[id,value]of [['payload',payload],['padding',padding],['empty-space',empty],['retained',retained]])$('#'+id).textContent=value+' B';$('#payload-bar').style.width=payload/16384*100+'%';$('#padding-bar').style.width=padding/16384*100+'%';$('#empty-bar').style.width=empty/16384*100+'%';$('#sum').textContent=payload+' + '+padding+' + '+empty+' = '+retained+' B';$('#explanation').textContent=viz.t(n===0?'emptyNote':mode==='packed'?'packedNote':mode==='spread'?'spreadNote':'customNote');document.querySelectorAll('[data-mode]').forEach(e=>e.setAttribute('aria-pressed',String(e.dataset.mode===mode)));}document.querySelectorAll('[data-mode]').forEach(e=>e.addEventListener('click',()=>{const n=slots.filter(Boolean).length;mode=e.dataset.mode;seed(n);draw();}));$('#count').addEventListener('input',()=>{if(!mode)mode='packed';seed(+$('#count').value);draw();});document.querySelectorAll('[data-slot]').forEach(e=>e.addEventListener('click',()=>{const i=+e.dataset.slot;slots[i]=!slots[i];mode='';draw();}));seed(4);draw();","audio":false,"strings":{"alive":"活对象","capacity":"空闲但仍被页保留的槽","count":"活对象数量","customNote":"槽的状态直接决定哪些页仍有活对象。释放该页最后一个对象,才会消除这张页的必需保留原因。","emptyNote":"所有对象都已释放。这个模型的必需承载页为零;真实分配器仍可能按策略缓存空 slab。","heading":"同样四个对象,为什么能占住四倍的页?","intro":"比较相同数量对象的集中与分散布局。点击对象槽可申请或释放,特别观察一张 slab 的最后一个活对象。","layout":"分配位置快照","packed":"集中到较少 slab","packedNote":"相同对象数集中在较少 slab,剩余整页不再被活对象强制保留。有效内容与槽内部开销没有因此改变。","padding":"已用槽内部开销","payload":"业务有效内容","reset":"重置","retained":"必须保留的页空间","returned":"没有活对象","scope":"对象 200 B、槽 256 B、slab 4096 B,共四张 slab。只统计含活对象的必需承载页,省略页外元数据及分配器对空 slab 的缓存。布局按钮重建“相同数量、不同分配位置”的快照,不表示内核会移动活对象。槽中蓝色部分为内容,金色部分为槽内开销。","scopeLabel":"模型范围与约定","spread":"分散到四张 slab","spreadNote":"活对象分散时,每张涉及的 slab 都必须保留。即使大多数槽已经空闲,也不能任意移动剩下的对象来归还页。"}}
2. kmalloc 与 kmem_cache 分别替调用者决定什么
kmalloc 按请求大小与标志选择通用分配路径,小对象通常进入合适的大小类;较大申请可直接依赖高阶页,而不是所有大小都必然存在一个同名 cache。大小类和各类用途分组依架构、配置及版本而变,不能列一组 cache 名字就认为所有机器都如此。
专用 kmem_cache 则表达固定对象布局、对齐、标志与可选构造需求。相近的 cache 在满足条件时可以合并,但条件不只是大小接近,还涉及 flags、对齐、构造函数及调试要求;sysfs 中不同名字可能指向同一底层 cache。定义专用 cache 的理由应来自对象用途和管理需求,不是仅为获得一个好看的统计名称。
复用对象时,初始化责任仍在调用者
构造函数通常用于 slab 中新对象的初始建立,不是在每次分配到调用者时重新运行。上一个使用者写过的字段,不能指望下一次 alloc 会被 ctor 自动恢复。kmalloc/kmem_cache_alloc 也不保证零初始化;需要清零时使用相应接口或明确初始化字段,再把对象发布给其他线程。
选择接口时,连续性和释放接口是一组契约
| 申请方式 | 适合的需求 | 释放与边界 |
kmalloc/kzalloc | 常规内核缓冲,物理连续的分配结果 | kfree;不能越过请求的有效范围访问 |
kmalloc_array/kcalloc | 元素数乘元素大小的数组 | 检查乘法溢出的分配接口,仍需处理 NULL |
kmem_cache_alloc | 已创建 cache 的固定对象 | 对应 kmem_cache_free,不是混用其他 cache |
kvmalloc/kvzalloc | 可以接受虚拟连续、物理不连续的较大缓冲 | kvfree;使用场景与 GFP 需符合回退路径要求 |
vmalloc | 虚拟连续映射多个后备页 | vfree;不可当作单段 DMA 物理缓冲 |
实际分配大小可能大于请求大小,但尾部空间不是业务可随意使用的隐含许可;若需要按大小类取整,应使用相应接口并以请求/分配契约为准。DMA 设备能否访问缓冲还要经过 DMA API,不能把“kmalloc 连续”推导成任意设备都能直接使用其 CPU 地址。见 内存分配指南。
3. 走完一次申请、跨 CPU 释放和页归还
CPU 0 申请对象时,如果本地已有合适的空槽,就可快速取得它。没有本地对象时,需要从共享储备或 partial slab 补充;这些也不足时才向页分配器申请承载页并建立更多槽。小对象申请同样可能进入页分配慢路径,因此“只申请 200 字节”不等于不可能睡眠或失败。
对象交给 CPU 1 完成处理并释放时,它可能不属于 CPU 1 当前正在使用的本地 slab/缓存。分配器需要维护所属 slab、空闲数和缓存归属等信息,必要时使用原子更新或锁;不能假设所有 free 都只是往当前 CPU 的一个链表头写两次指针。
释放一个对象只是把其使用权交还分配器。等某些对象批次排空、slab 满足可归还条件,承载页才可能回到页分配器。保留一定空 slab 或本地对象容量能加速下一轮申请,却使低负载后的内存下降不是瞬间完成。判断异常要结合稳定负载下是否持续增长、cache 的设计保留策略及引用链,而不是只比较一次 kfree 前后的 MemFree。
源码阅读:缓存组织随版本变化
旧版 SLUB 常用 per-CPU active slab/freelist 解释快路径;本次核对的 Linux 7.2 mm/slub.c 已使用 CPU 侧 sheaves 对象数组、NUMA node 的 barn 进行批量缓存与交换。
它们都在减少共享资源操作频率,但同步、RT 和 debug 路径需按目标版本阅读。学习 kmalloc 等接口契约时,应把这类内部组织变化单独看待。
4. 分配失败时,半成品必须有明确所有者
考虑一个初始化过程,需要创建 cache,再申请一个指针数组,再分配 32 个对象。第 19 个对象失败时,前 18 个已成功的对象仍然属于初始化者;只释放数组会遗失它们的地址,只销毁 cache 又不能替代释放存活对象。
第 19 次对象申请失败时,清理责任按成功记录反向展开:
| 撤销顺序 | 要释放什么 | 为什么必须在这一步处理 |
| 1 | 前 18 个已成功对象 | 它们仍属于初始化者,不能遗失地址 |
| 2 | 指针数组 | 对象全部释放后,索引才不再需要 |
| 3 | cache | 确认没有存活对象再销毁承载机制 |
复杂驱动还需先停止入口、后台任务和异步引用。分配器知道槽是否空闲,却不知道外部回调是否还会访问它。
实验代码:先看回滚顺序,再看各错误标签
下面是完整教学模块的初始化路径。它分配少量对象、验证字段,然后在返回前清理;代码用于展示逐级回滚,编译检查不要求加载模块。
#include <linux/errno.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/slab.h>
struct kb_item {
unsigned int id;
unsigned char payload[196];
};
static int __init kb_alloc_init(void)
{
struct kmem_cache *cache;
struct kb_item **items;
unsigned int made = 0;
int result = -ENOMEM;
cache = kmem_cache_create("kb_alloc_example", sizeof(struct kb_item),
0, 0, NULL);
if (!cache)
return -ENOMEM;
items = kcalloc(32, sizeof(*items), GFP_KERNEL);
if (!items)
goto destroy_cache;
while (made < 32) {
struct kb_item *item = kmem_cache_zalloc(cache, GFP_KERNEL);
if (!item)
goto free_items;
item->id = made;
items[made++] = item;
}
result = 0;
for (unsigned int i = 0; i < made; ++i) {
if (items[i]->id != i || items[i]->payload[0] != 0) {
result = -EINVAL;
break;
}
}
free_items:
while (made)
kmem_cache_free(cache, items[--made]);
kfree(items);
destroy_cache:
kmem_cache_destroy(cache);
return result;
}
static void __exit kb_alloc_exit(void) { }
module_init(kb_alloc_init);
module_exit(kb_alloc_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Slab allocation and rollback teaching example");
Kbuild 可使用 obj-m += kb_alloc_example.o。真实故障分支是否按预期执行,需要另外的受控失败注入;仅编译成功不能当作已跑过第 19 次失败路径。不要在生产对象仍被引用时通过销毁 cache 来“修复泄漏”。
5. 释放后复用解释了 UAF,也解释了检测工具的不同证据
设对象 A 被释放,原地址随后分配给对象 B。A 的旧指针仍指向相同地址,所以一次错误写入可能破坏 B 而不是立即崩溃。把释放内存填成 poison 有助于之后识别异常,却不能保证每次错误访问都在发生当刻被截获;分配/释放追踪也可能在之后的检查点才发现破坏。
检测工具:非法访问与不可达分配是两类证据
KASAN 的不同模式利用影子内存或标签等机制检查非法访问,代价、覆盖范围和粒度不同。Generic KASAN 的 1:8 shadow 比例不是整个系统唯一开销,还可能有 quarantine、对齐和其他元数据。基于标签的检测也不能保证识别所有具有相同标签或落在检查粒度内的错误。见 KASAN。
kmemleak 主要通过扫描可疑对象是否仍可由已知根到达来报告潜在泄漏。它不是追踪所有 C 指针语义的证明器:隐藏或编码指针可能带来误报,偶然像指针的值又可能使泄漏看似可达。因此它回答“哪些分配值得检查”,而 KASAN 更直接针对某次非法访问。见 kmemleak。
普通 slab 对象的回收责任属于使用它的子系统。带有可回收缓存语义的子系统可以注册 shrinker,在压力下释放不再需要的对象;仅把 cache 标为某种可回收用途,不会让内核自动理解并销毁仍在使用的业务对象。
6. 只读比较对象大小、槽步长与页阶数
from pathlib import Path
import os
root = Path("/sys/kernel/slab/dentry")
if not root.exists():
print("当前未暴露 dentry slab sysfs")
else:
print("实际 cache 路径:", root.resolve())
values = {}
for name in ("object_size", "slab_size", "order", "objects", "slabs"):
try:
values[name] = (root / name).read_text().strip()
except OSError as error:
values[name] = type(error).__name__
print(name, values[name])
if values["order"].isdigit():
print("按当前 order 计算的 slab 字节数:",
os.sysconf("SC_PAGE_SIZE") * (1 << int(values["order"])))
读取结果时先确认单位:
| 字段 | 应怎样理解 |
| object_size | 对象内容大小相关字段 |
| slab_size | 对象槽大小相关字段,不是整块 slab 字节数 |
| order | 需结合基础页大小计算整块承载空间 |
| objects、slabs | 可能附带节点分布信息,不能整行直接转整数 |
已有 slab 的实际阶数、调试模式和回退可能需要更详细统计。当前配置值不能反推全部历史分配。
结合 /proc/slabinfo 时分别看 active objects、容量和承载页。统计也受本地缓存与实现记账方式影响,不能无条件将 active 数视为业务仍持有的精确对象数。接下来在 页面回收与交换 中,把 slab shrinker 与普通文件页、匿名页的回收放进同一个压力模型。