只把三页映射的中间一页改成只读,maps 里却可能多出两段;解除中页后,留下的空洞又与 PROT_NONE 不同。下面围绕同一段三页地址空间 ,追踪范围如何变化,再解释并发查找如何保护这些变化。
要解释的现象 阅读位置
mmap 成功为何仍会缺页? 第 1 节:VMA、页表与后备内容
改权限、解除映射、移动地址各改变什么? 第 2~3 节:三页推演与实验
Maple Tree 快在哪,不能省掉哪些工作? 第 4 节:范围查询
刚找到 VMA,另一线程就 munmap 怎么办? 第 5~6 节:同步与缺页解释
1. VMA 描述范围,页表描述范围内的当前翻译
一个普通 VMA 覆盖半开区间 [vm_start, vm_end),包含起点而不包含终点。同一 mm_struct 的正常 VMA 不互相重叠;共享地址空间的线程通常共同引用该 mm,所以一个线程的 mprotect 会改变其他线程所见的映射权限。
手算:VMA 中的地址对应文件哪里
设基础页为 P,映射从地址 A 开始,vm_pgoff=8。对地址 A + 2P + 17,分两步定位:
映射从文件第 8 个基础页开始,再前进 2 页,得到文件第 10 页。
保留页内偏移 17,文件字节偏移为 10P + 17。
vm_pgoff 的单位是基础页,不是字节;它也不随这个文件描述符之后的 read/seek 推进。
层次 此例中记录什么 没有直接回答什么
VMA 范围 A 起、映射文件、起始文件偏移、共享/私有和权限 每页是否驻留、物理页号是多少
页表 已建立的虚拟页到物理页/软件状态的映射 未来未触碰地址应从哪类后端获取内容
文件页缓存 文件各范围当前在内存中的内容 哪些进程都已安装了对应 PTE
rmap 等结构 从页或后备对象反查可能关联的映射 不能单独替代地址空间正向查找
一个文件 MAP_PRIVATE VMA 在写入后可以同时覆盖未改动的文件缓存页和已经分离的匿名 COW 页,因此 vm_file != NULL 不等于“每个脏页都会回写这个文件”。普通共享匿名映射在内核也可能由 shmem 文件对象支撑,用户标志 MAP_ANONYMOUS 与内部 vm_file 是否为空不是简单一一对应。
2. 用三页映射推演四个操作
同一段三页映射:权限改变先改变区间描述
初始
中页只读
中页解除映射
[A, A + 3P) rw-p
一个兼容的地址范围
rw-p
r--p
rw-p
rw-p
空洞:无 VMA
rw-p
P 为基础页大小。虚拟地址连续不代表物理连续;一次 syscall 不必对应一个永久 VMA。
mmap:先在地址空间中建立承诺
普通的非固定 mmap 可以提供地址提示,内核在不覆盖现有映射的前提下选择可用范围,完成权限、文件能力、偏移、资源限制等检查,并建立映射关系。新区域若与邻居满足合并条件,可能直接扩大已有 VMA,所以“一次 mmap 创建一个可永久识别的 VMA 对象”并不成立。
MAP_FIXED 的意义不同:它要求使用指定范围,并可替换已有映射。它不是普通提示失败后的无害强化版。若目标是“只有这段地址完全空闲才占用”,应了解 MAP_FIXED_NOREPLACE 及返回地址检查;不要先读 maps 看见空洞,再用 MAP_FIXED 假定期间无人占用。
通常数据页按需建立,但 MAP_POPULATE、锁定/预取要求和设备映射等路径可能做额外工作。文件 mmap 的建立经 file_operations 的相关接口接入,vm_operations_struct 的 fault 等回调用于之后的映射操作;不能发明一个通用 vm_ops->mmap() 并宣称所有文件都调用它。见 mmap(2) 。
mprotect:为什么只改一页,却多了两个描述对象
假设 [A,A+3P) 原来可读写。把 [A+P,A+2P) 改为只读后,这三段已经不具有相同属性,原来一个 VMA 无法忠实描述它们,需要拆成前、中、后三段。内核还要对已存在的页表应用权限变化,并按要求维护 TLB;没有驻留页的部分则由新的 VMA 语义约束未来缺页。
这解释了一个看似反常的失败:即使没请求增加数据内存,mprotect 也可能因需要更多 VMA 元数据、超过映射数量限制而失败。仅“改 PTE 一个 bit”无法解释这个现象。将中页恢复读写后,兼容的相邻段可能重新合并,但合并还要检查后备对象、偏移连续性、匿名反向映射关系和其他属性,不能只比较 maps 中显示的 rw-p 四个字符。见 mprotect(2) 。
munmap:解除的是映射,不必销毁数据本身
解除中页后,中间成为真正空洞。与仅禁止访问相比,它改变了地址空间的占用关系:
操作 中间范围仍有映射语义吗? 地址空间是否仍被占据?
改成 PROT_NONE 有,只是当前禁止访问 是
munmap 中页 没有,成为空洞 否,可被后续分配占用
munmap 还要撤销翻译并释放相应引用;这一步不直接决定数据页的最终寿命。
如果页被另一个进程共享,或普通文件页还在页缓存中,解除本进程映射并不把它直接变成 buddy 空闲页。反过来,物理页已被回收换出时,合法 VMA 仍可存在。VMA 寿命、页表项寿命和数据页寿命必须分别推理。
mremap:地址可变,数据不必逐字节复制
扩大映射时,邻接范围若无法使用,允许移动的 mremap 可以选择新位置并搬移页表映射等状态。它能够避免某些用户数据复制,但仍有元数据、页表和失效成本,也不是所有映射都可随意移动。成功移动后,旧地址及其中保存的绝对自引用指针都需要按应用设计处理;“内容还在”不等于“原来的所有指针仍有效”。
把三种寿命放在同一画面中:先读取中央页,再解除它的映射,看看哪些范围、PTE 和缓存真的消失。恢复映射与恢复访问权限也可以分别操作。
file_offset = P vm_pgoff + ( a − vm_start )
这条式子用于该 VMA 内的文件地址;a 必须先满足范围与访问条件,空洞不参与计算。
正在呈现知识画面 重新加载
改一页权限,哪些对象会一起变化? mprotect 可以拆分范围,却不必分配数据页。PROT_NONE 仍占据虚拟范围,munmap 留下空洞;撤销映射也不必丢弃文件缓存。
{"id":"vma-range-edits","title":"改一页权限,哪些对象会一起变化?","summary":"mprotect 可以拆分范围,却不必分配数据页。PROT_NONE 仍占据虚拟范围,munmap 留下空洞;撤销映射也不必丢弃文件缓存。","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><p class=\"caption\" data-i18n=\"select\"></p><div class=\"page-strip\"><button data-page=\"0\"><strong>0P</strong><span id=\"prot-0\"></span></button><button data-page=\"1\"><strong>1P</strong><span id=\"prot-1\"></span></button><button data-page=\"2\"><strong>2P</strong><span id=\"prot-2\"></span></button></div><div class=\"actions\"><button id=\"rw\" data-i18n=\"rw\"></button><button id=\"r\" data-i18n=\"readOnly\"></button></div><div class=\"actions\"><button id=\"none\" data-i18n=\"none\"></button><button id=\"unmap\" data-i18n=\"unmap\"></button></div><div class=\"actions\"><button id=\"read\" data-i18n=\"read\"></button><button id=\"map\" data-i18n=\"map\"></button></div><p class=\"caption\" data-i18n=\"ranges\"></p><div id=\"ranges\" class=\"range-ribbon\"></div><div class=\"pte-strip\"><div id=\"pte-0\"></div><div id=\"pte-1\"></div><div id=\"pte-2\"></div></div><p class=\"caption\" data-i18n=\"cache\"></p><div class=\"cache-strip\"><div id=\"cache-0\"><strong>file[8]</strong><span></span></div><div id=\"cache-1\"><strong>file[9]</strong><span></span></div><div id=\"cache-2\"><strong>file[10]</strong><span></span></div></div><div class=\"metrics\"><div><span data-i18n=\"count\"></span><strong id=\"vma-count\"></strong></div><div><span data-i18n=\"resident\"></span><strong id=\"pte-count\"></strong></div></div><div class=\"formula\" id=\"file-offset\"></div><div class=\"copy\"><p id=\"explanation\" role=\"status\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"initial\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"protected\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"unmapped\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"loaded\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"denied\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"mapped\"></p></div><div class=\"actions\"><button id=\"reset\" data-i18n=\"reset\"></button></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.page-strip,.pte-strip,.cache-strip{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:7px;margin:12px 0}.page-strip button{display:block;padding:10px 3px;min-width:0;font-size:12px;min-height:5.5em}.page-strip strong,.page-strip span{display:block}.page-strip button.chosen{border-color:var(--accent);box-shadow:inset 0 0 0 1px var(--accent)}.page-strip button.hole{border-style:dashed;color:var(--muted)}.range-ribbon{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:3px;margin:12px 0;min-height:5.6em;font-size:12px}.range-ribbon div{border:1px solid var(--accent);background:var(--accent-soft);padding:10px 5px;overflow-wrap:anywhere}.range-ribbon div.hole{border-style:dashed;border-color:var(--rule);background:var(--surface);color:var(--muted)}.pte-strip>div{min-width:0;padding:10px 4px;background:var(--surface);border-radius:6px;font:11px/1.6 var(--viz-font-mono);white-space:pre-line;min-height:6em;overflow-wrap:anywhere}.cache-strip>div{border:1px dashed var(--rule);padding:10px 4px;border-radius:6px;min-width:0;font-size:11px;min-height:8em;text-align:center}.cache-strip>div.live{background:var(--second-soft);border-color:var(--second)}.cache-strip strong{font-family:var(--viz-font-mono);font-weight:500;overflow-wrap:anywhere}.cache-strip span{display:block;margin-top:8px}.formula{min-height:6em}","js":"const $=s=>document.querySelector(s);let chosen=1,prot=['RW','RW','RW'],pte=[false,false,false],cache=[false,false,false],message='initial';function draw(){const ranges=[];for(let i=0;i<3;i++){if(i&&prot[i]===prot[i-1])ranges.at(-1).end=i+1;else ranges.push({start:i,end:i+1,p:prot[i]});}for(let i=0;i<3;i++){const b=document.querySelector('[data-page=\"'+i+'\"]');b.classList.toggle('chosen',i===chosen);b.classList.toggle('hole',prot[i]===null);b.setAttribute('aria-current',String(i===chosen));$('#prot-'+i).textContent=prot[i]??viz.t('hole');$('#pte-'+i).textContent='VA '+i+'P\\nPTE '+(pte[i]?prot[i]:'—');$('#cache-'+i).classList.toggle('live',cache[i]);$('#cache-'+i+' span').textContent=viz.t(cache[i]?'cached':'empty');}$('#ranges').replaceChildren(...ranges.map(r=>{const e=document.createElement('div');e.style.gridColumn=(r.start+1)+' / '+(r.end+1);e.textContent='['+r.start+','+r.end+') '+(r.p??viz.t('hole'));e.classList.toggle('hole',r.p===null);return e;}));$('#vma-count').textContent=ranges.filter(r=>r.p!==null).length;$('#pte-count').textContent=pte.filter(Boolean).length;for(const id of ['rw','r','none','unmap'])$('#'+id).disabled=prot[chosen]===null;$('#map').disabled=prot[chosen]!==null;$('#file-offset').textContent=prot[chosen]===null?'VA → '+viz.t('hole'):'VA = A + '+chosen+'P + 17\\nfile offset = (8 + '+chosen+') × 4096 + 17 = '+((8+chosen)*4096+17);$('#explanation').textContent=viz.t(message);}document.querySelectorAll('[data-page]').forEach(e=>e.addEventListener('click',()=>{chosen=+e.dataset.page;draw();}));for(const [id,p]of [['rw','RW'],['r','R'],['none','NONE']])$('#'+id).addEventListener('click',()=>{prot[chosen]=p;message='protected';draw();});$('#map').addEventListener('click',()=>{if(prot[chosen]!==null)return;prot[chosen]='RW';message='mapped';draw();});$('#unmap').addEventListener('click',()=>{prot[chosen]=null;pte[chosen]=false;message='unmapped';draw();});$('#read').addEventListener('click',()=>{if(prot[chosen]===null||prot[chosen]==='NONE')message='denied';else{pte[chosen]=cache[chosen]=true;message='loaded';}draw();});$('#reset').addEventListener('click',()=>{prot=['RW','RW','RW'];pte=[false,false,false];cache=[false,false,false];message='initial';draw();});draw();","audio":false,"strings":{"cache":"同一文件的页缓存","cached":"已缓存","count":"示意 VMA 数量","denied":"该地址是空洞或被 PROT_NONE 禁止读取,不能把它当成普通的按需读入。","empty":"未缓存","heading":"改一页权限,哪些对象会一起变化?","hole":"空洞","initial":"初始有一段读写 VMA,但还没有数据 PTE。文件范围与已建立的硬件翻译不是同一件事。","intro":"点击三页中的一页,再修改权限、读取或解除映射。对照范围条带、PTE 和文件缓存,观察三者的不同寿命。","loaded":"访问得到该文件页内容并建立本映射的 PTE。再次读取可以复用它;改变页权限并不移动文件偏移。","map":"重新映射此页","mapped":"这一页重新建立读写映射,尚未安装它的数据 PTE;已有文件缓存仍可复用。","none":"设为 PROT_NONE","protected":"权限改变后按兼容范围重新组织。它不主动读取数据;PROT_NONE 仍然是占用地址空间的一段映射。","ranges":"VMA 范围 / P","read":"读取选中页","readOnly":"设为只读","reset":"重置","resident":"已安装的 PTE","rw":"设为读写","scope":"三页属于同一普通文件,基础页 P=4096,vm_pgoff=8。只做读取,不修改文件;保留缓存而不做回收。相邻段仅按权限与连续后备合并是简化,真实 VMA 合并还受其他属性限制。重建已解除页是新的示意映射,不使用真实 MAP_FIXED。","scopeLabel":"模型范围与约定","select":"选择要操作的页","unmap":"解除映射","unmapped":"这一页成为空洞,PTE 被撤销。之前读入的文件缓存仍可以留下,它不归这个 VMA 独占。"}}
3. 可运行实验:直接观察这几个边界
下面保留五页 PROT_NONE 空间,用中间三页作实验,两边作为地址边界。每一步读取自身 maps 验证指定地址的属性;不访问只读页的写入地址,也不制造进程崩溃。
#define _GNU_SOURCE
#include < assert.h>
#include < inttypes.h>
#include < stdint.h>
#include < stdio.h>
#include < string.h>
#include < sys/mman.h>
#include < unistd.h>
static int permission ( uintptr_t address , char output [ 5 ] ) {
FILE * file = fopen ( " /proc/self/maps" , " r" ) ;
assert ( file != NULL ) ;
char line[ 1024 ] ;
int found = 0 ;
while ( fgets ( line, sizeof ( line ) , file ) ) {
uintptr_t start, end;
char mode[ 5 ] ;
if ( sscanf ( line, " %" SCNxPTR " -%" SCNxPTR " %4s " ,
& start, & end, mode ) == 3 &&
start <= address && address < end) {
memcpy ( output, mode, 5 ) ;
found = 1 ;
break ;
}
}
assert ( fclose ( file ) == 0 ) ;
return found;
}
static void check ( unsigned char * base , size_t page ,
const char * middle ) {
for ( size_t i = 0 ; i < 3 ; + + i) {
char mode[ 5 ] = " " ;
int found = permission ( ( uintptr_t ) ( base + i * page) , mode) ;
const char * expected = i == 1 ? middle : " rw-p" ;
if ( expected) {
assert ( found && strcmp ( mode, expected ) == 0 ) ;
} else {
assert ( ! found) ;
}
printf ( " page %zu : %s \n " , i, found ? mode : " hole" ) ;
}
}
int main ( void ) {
long size = sysconf ( _SC_PAGESIZE ) ;
assert ( size > 0 ) ;
size_t page = ( size_t ) size;
unsigned char * whole = mmap ( NULL , 5 * page, PROT_NONE,
MAP_PRIVATE | MAP_ANONYMOUS, - 1 , 0 ) ;
assert ( whole != MAP_FAILED ) ;
unsigned char * base = whole + page;
assert ( mprotect ( base, 3 * page, PROT_READ | PROT_WRITE ) == 0 ) ;
puts ( " 初始:" ) ; check ( base, page, " rw-p" ) ;
assert ( mprotect ( base + page, page, PROT_READ ) == 0 ) ;
puts ( " 中页只读:" ) ; check ( base, page, " r--p" ) ;
assert ( mprotect ( base + page, page, PROT_READ | PROT_WRITE ) == 0 ) ;
puts ( " 恢复权限:" ) ; check ( base, page, " rw-p" ) ;
assert ( munmap ( base + page, page ) == 0 ) ;
puts ( " 中页解除映射:" ) ; check ( base, page, NULL ) ;
assert ( munmap ( whole, 2 * page ) == 0 ) ;
assert ( munmap ( whole + 3 * page, 2 * page ) == 0 ) ;
}
以 cc -std=c11 -Wall -Wextra -Werror vma-demo.c -o vma-demo 编译,不加 -DNDEBUG。检查按地址定位,不假定实际 VMA 边界完全等于 mmap 请求边界,也不强行断言恢复权限后内核一定选择某一种合并布局。maps 呈现的是 VMA 属性,无法据此断言中间三页已经实际分配了物理页。
4. Maple Tree 解决的是范围组织问题
地址空间需要回答多种查询:这个地址落在哪个映射中,下一段映射在哪里,某个区间与哪些 VMA 相交,以及哪里有足够大的空洞放置新映射。旧实现用红黑树和链表协作;Maple Tree 用面向范围的多分支结构及迭代接口承担这些工作,在减少树高、改进局部性和维护范围信息方面提供收益。
先明确查询要返回多少结果,再讨论树的收益:
查询 至少要完成的工作
查一个地址 沿范围边界定位候选
输出 K 个交叠 VMA 找到起点后,还需输出 K 个对象
遍历全部 N 个 VMA 至少输出 N 项,不能变成 O(log N)
Maple Tree 改善树高、局部性与范围组织;跨越大量映射的删除和遍历,仍要支付与涉及对象规模相关的工作。
范围索引:先选分支,再取得覆盖范围中的 VMA
内部节点:按边界划分地址范围
多个子槽减少树高;空洞信息支持地址分配
[0x1000, 0x3fff]
VMA A 指针
[0x4000, 0x7fff]
空洞
[0x8000, 0xafff]
VMA B 指针
图是范围检索模型,不是实际节点内存布局;树查到指针后仍需保证 VMA 寿命与稳定性。
VMA 使用半开区间,Maple Tree 范围接口使用含末端的索引范围时,会相应表达为 [vm_start, vm_end-1]。值是 VMA 指针,范围边界用于决定搜索方向。节点类型、槽数和 pivot 布局属于实现细节;树不是像页表一样每一级固定消费地址的某 9 位,也不能按页表层次给 Maple Tree 发明固定地址覆盖表。
存储空洞信息让分配搜索有机会跳过不可能容纳请求的子树,而不必依次试每个字节。例如需要 4P 连续虚拟空间时,总空闲 8P 仍可能由多个 1P 空洞组成;范围索引需要寻找足够长的单段。这是虚拟地址碎片 ,与 buddy 找不到连续物理页的碎片属于不同问题。见 Maple Tree 设计与 API 。
“找到后继”不等于“地址已被覆盖”
一些查找接口允许在空洞中返回下一个 VMA。以 find_vma(mm, address) 的常见契约为例,结果可为第一个 vm_end > address 的 VMA;若 address < vm_start,地址仍在空洞中。调用者必须按接口语义再判定覆盖,不能只判断返回非空。只需要精确覆盖时,应选择对应接口并遵守其锁定要求。
把探针停在区间末端和空洞中,比较“返回了哪个候选”与“候选是否覆盖”。结束边界不属于原范围。
正在呈现知识画面 重新加载
找到 VMA 指针,地址就一定有效吗? find_vma 风格查询可以在空洞中返回后继。半开范围的覆盖还要求 start ≤ address < end;返回非空不等于访问合法。
{"id":"vma-cover-or-successor","title":"找到 VMA 指针,地址就一定有效吗?","summary":"find_vma 风格查询可以在空洞中返回后继。半开范围的覆盖还要求 start ≤ address < end;返回非空不等于访问合法。","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><div class=\"control\"><label for=\"probe\" data-i18n=\"probe\"></label><output for=\"probe\" id=\"probe-value\"></output><input id=\"probe\" type=\"range\" min=\"0\" max=\"10\" value=\"3\" step=\"0.1\"></div><div class=\"range-view\"><i id=\"probe-line\"></i><div class=\"range-track\"><b id=\"range-0\" style=\"left:0%;width:20%\">A</b><b id=\"range-1\" style=\"left:40%;width:10%\">B</b><b id=\"range-2\" style=\"left:70%;width:20%\">C</b></div><div class=\"ticks\"><span>0</span><span>2</span><span>4</span><span>6</span><span>8</span><span>10 P</span></div></div><div class=\"range-list\"><span>A = [0,2)</span><span>B = [4,5)</span><span>C = [7,9)</span></div><div class=\"metrics\"><div><span data-i18n=\"candidate\"></span><strong id=\"candidate\"></strong></div><div><span data-i18n=\"covered\"></span><strong id=\"covered\"></strong></div></div><div class=\"formula\" id=\"inequality\"></div><div class=\"copy\"><p id=\"explanation\" role=\"status\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"hit\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"hole\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"end\"></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.range-view{position:relative;margin:30px 0 14px;padding:14px 0}.range-track{position:relative;height:45px;background:var(--surface);border-bottom:1px solid var(--rule)}.range-track b{position:absolute;height:100%;display:flex;align-items:center;justify-content:center;background:var(--accent-soft);border:1px solid var(--accent);font-size:14px}.range-track b.candidate{background:var(--second-soft);border-color:var(--second)}#probe-line{position:absolute;z-index:1;top:0;bottom:25px;border-left:2px solid var(--ink);transform:translateX(-1px);pointer-events:none;transition:left .2s}.ticks{display:flex;justify-content:space-between;font-size:11px;color:var(--muted);margin-top:12px}.range-list{display:flex;flex-wrap:wrap;gap:6px 14px;font-size:12px;color:var(--muted)}.formula{min-height:4.8em}.metrics strong{min-height:3.2em}","js":"const $=s=>document.querySelector(s),ranges=[[0,2],[4,5],[7,9]];function draw(){const p=+$('#probe').value,i=ranges.findIndex(r=>r[1]>p),covered=i>=0&&ranges[i][0]<=p;$('#probe-value').textContent=viz.number(p,1);$('#probe-line').style.left=p*10+'%';ranges.forEach((r,j)=>$('#range-'+j).classList.toggle('candidate',j===i));$('#candidate').textContent=i<0?'NULL':'ABC'[i];$('#covered').textContent=viz.t(covered?'yes':'no');$('#inequality').textContent=i<0?'end ≤ '+viz.number(p,1):ranges[i][0]+' ≤ '+viz.number(p,1)+' < '+ranges[i][1]+' : '+String(covered);$('#explanation').textContent=viz.t(i<0?'end':covered?'hit':'hole');}$('#probe').addEventListener('input',draw);draw();","audio":false,"strings":{"candidate":"返回的候选","covered":"是否真正覆盖","end":"已经没有结束位置大于探针的范围,查询结果为空。","heading":"找到 VMA 指针,地址就一定有效吗?","hit":"探针位于半开范围内部。此候选覆盖地址;结束边界本身属于范围之外。","hole":"查询返回了后面的 VMA,但探针仍在空洞里。候选的 start 大于地址,因此不能把非空指针当成覆盖证明。","intro":"移动地址探针,比较“第一个结束位置大于地址的范围”与“真正覆盖地址的范围”。特别试试边界和空洞。","no":"未覆盖","probe":"查询地址 / P","reset":"重置","scope":"地址单位为 P,固定范围 [0,2)、[4,5)、[7,9)。只演示范围契约,不模拟 Maple Tree 节点布局、锁或缺页;即使覆盖成立,也要检查权限和后备内容。","scopeLabel":"模型范围与约定","yes":"覆盖"}}
5. 读树、锁 VMA 与锁页表为什么不能合成一把锁
考虑线程 R 在地址 A 缺页,线程 W 同时对 A 执行 munmap。R 从树中得到 VMA 指针后,若 W 把范围摘除并回收对象,R 再访问字段就可能 use-after-free。即使 RCU 延迟了对象回收,R 读到的起点、终点、后备对象也仍需组成允许当前缺页处理的稳定状态。
三层保护,各自稳定不同对象
同步层 主要保护什么 不能据此推断什么
mmap_lock mm 级范围关系及相关操作 不自动代替所有页表修改协议
RCU 查找与 per-VMA 锁 支持路径中的候选查找、VMA 状态稳定 不保证对象内容永远不变
页表锁 PTE/PMD 等实际修改 不单独解决范围撤销和对象寿命
rmap 相关锁还服务于从页反查映射。支持的缺页路径可尝试 per-VMA 方案,无法建立保证时回到更广泛的协调路径;获得一层读锁不自动授予另一层写权限。
竞争发生时:验证失败就重试
读者 R 先找到候选,尝试稳定 VMA 并验证地址;写者 W 在撤销范围前按协议取得排他权。R 若无法建立所需保证,就重试或采用更保守的 mm 级锁路径。
耗时 I/O 还可能要求释放、重取锁并重新验证。等待期间,原先成立的“地址仍在这个 VMA 内”可能已经改变,不能拿旧查找结果直接继续。
Maple Tree 的 RCU 能力只解决其中一部分:遍历节点的并发安全不自动证明数据对象长期不变,也不意味着所有写者都复制整棵树再替换。当前支持的架构已经有 per-VMA 缺页路径,不能继续写成遥远未来;但也不能反过来宣称 mmap_lock 已经消失。锁契约见 Process Addresses ,目标内核可从 lock_vma_under_rcu() 等入口定位。
6. 最后把缺页处理接回这个模型
同样是写保护异常,处理结果取决于软件语义:
当前状态 对这次写入的解释
VMA 允许写,PTE 因 COW 暂时只读 可以进入私有页处理路径
mprotect 已把 VMA 改成只读 不能用 COW 绕过禁止写入的语义
硬件报告现象,VMA 与后备状态决定解释。handle_mm_fault() 因而不是“找到范围就分配一页”的统一动作。
即使 VMA 合法,文件映射访问超出后端文件有效范围也可能产生 SIGBUS;分配失败、特殊映射和换入也有各自处理结果。handle_mm_fault() 因而不能概括为“找到 VMA 就分配一个新页”。可能复用页缓存、处理换入、分离私有页,或按操作契约拒绝访问。
排查时先用 maps 判断范围和权限,再用 smaps/smaps_rollup 区分驻留与共享占用,最后结合 fault、I/O 与内存压力定位成本。大量 mmap/mprotect 可能增加 VMA 数和元数据开销;但 maps 很长并不独自证明它是性能瓶颈。物理分配侧的问题接 物理内存管理 ,驻留页如何被回收接 页面回收与交换 。