区间树:反向映射的前置数据结构
这篇只讲一个前置数据结构:区间树。
先不展开 anon_vma、anon_vma_chain、vma、folio 这些内核结构。真正讲匿名页反向映射时,再把这些结构套进来。
先给一个简短定义:
区间树可以理解成一棵被增强过的有序平衡树。 原本平衡树的节点通常保存一个具体的 key;区间树把节点保存的内容换成一个区间 [start,end],排序和查找时主要比较区间的 start 端点。然后,它在每个节点上额外维护一个 maxEnd 字段,表示这棵子树里所有区间最大的 end。当树上的区间插入、删除或变化时,底层平衡树要继续维护平衡,区间树这一层还要同步维护受影响节点的 maxEnd。
现在只抓住一个核心问题:
text
给定一个查询点或查询区间,
在很多已经保存好的区间里,
快速找出所有和它重叠的区间。
两个闭区间:
text
A = [a_start, a_end]
B = [b_start, b_end]
相交条件是:
text
a_start <= b_end && b_start <= a_end
如果查询是一个点 q = 100,可以把它看成区间 [100,100]。所以点查询和区间查询可以统一起来。
一、最小问题:找所有重叠区间
先有一批区间:
text
[ 80, 120]
[100, 140]
[300, 360]
[ 96, 111]
现在查询:
text
Q = [100, 101]
逐个判断:
text
[ 80, 120] 命中
[100, 140] 命中
[300, 360] 不命中
[ 96, 111] 命中
最直接的实现是数组扫描:
text
for each interval:
if interval.start <= Q.end && Q.start <= interval.end:
hit
复杂度是:
text
O(n)
区间数量少时,这没问题。区间很多、查询频繁、还要插入删除时,就需要索引。
二、先把区间放进普通搜索树
一个自然想法是:按区间起点 start 建一棵搜索树。
text
key = interval.start
每个节点先只保存自己的区间和左右孩子:
text
node
├─ interval = [start, end]
├─ left 所有 start 更小的区间
└─ right 所有 start 更大的区间
把这些区间插进去:
text
[ 80, 120]
[100, 140]
[300, 360]
[ 96, 111]
一棵可能的树是:
text
[80,120]
\
[100,140]
/ \
[96,111] [300,360]
这时候树已经形成了。它是一棵按 start 排序的普通区间搜索树,但还不是完整的 interval tree。
这棵树能帮我们做一部分剪枝。比如查询 [100,101] 时,节点 [300,360] 的 start = 300 已经大于查询右端 101,它和它右边那些 start 更大的节点都不可能命中。
但只按 start 排序还不够。
三、只按 start 排序为什么不够
看一棵按 start 排序的树:
text
[100,140]
/ \
[80,120] [300,360]
/
[1,1000000]
查询:
text
Q = [500, 501]
站在根节点 [100,140] 看,左子树里的区间起点都更小:
text
[80,120].start = 80
[1,1000000].start = 1
但是左子树不能直接跳过。因为:
text
[80,120]
80 <= 501 && 500 <= 120
不命中
[1,1000000]
1 <= 501 && 500 <= 1000000
命中
也就是说,只知道左子树的 start 都更小,不能说明左子树里所有区间都已经结束了。
普通按 start 排序的树缺少一个信息:
text
这棵子树里,最大的 end 是多少?
如果不知道这个信息,就无法判断一整棵子树能不能被跳过。最坏情况下,还是会退化成大量扫描。
到这里,再把层次关系分清楚就比较自然了:
text
底层树:
负责维护有序结构,比如红黑树、AVL 树、B-tree。
区间树这一层:
把每个树节点里的数据换成一个区间 [start,end];
用 start 作为主要排序 key;
start 相同时再用 end 或节点身份做 tie-break;
再额外维护"这棵子树里最大的 end"这个摘要字段;
查询时用这个摘要字段跳过不可能命中的整棵子树。
所以 interval tree 不是说发明了一套新的平衡规则。它更像是:
text
ordered balanced tree
+ interval stored in each node
+ subtree summary field
+ overlap query algorithm
下一节就给这个摘要字段起一个名字:maxEnd。
四、加增强字段:maxEnd
区间树的关键就是在普通搜索树节点上加一个增强字段:
text
maxEnd = 以当前节点为根的整棵子树里,所有区间最大的 end
节点变成:
text
node
├─ interval = [start, end]
├─ left
├─ right
└─ maxEnd
要注意:
text
x.maxEnd
表示以 x 为根的整棵子树里的最大 end。
node.left.maxEnd
表示 node 的左子树里的最大 end。
node.right.maxEnd
表示 node 的右子树里的最大 end。
maxEnd 的作用是判断某棵子树有没有必要进去查。
比如查询:
text
Q = [100, 101]
有这样一棵树:
text
[100,140], maxEnd=360
/ \
[40,60], maxEnd=90 [300,360], maxEnd=360
/ \
[10,20], maxEnd=20 [70,90], maxEnd=90
站在根节点 [100,140],看左孩子:
text
left.maxEnd = 90
Q.start = 100
因为:
text
90 < 100
所以左子树里所有区间都在 Q.start 之前结束,不可能和 [100,101] 相交。整棵左子树可以跳过:
text
[40,60]
[10,20]
[70,90]
当前节点 [100,140] 和查询区间相交,命中。
右子树 [300,360] 的 start > Q.end,不命中。
这就是 maxEnd 的价值:它不是判断某个单独节点,而是判断一整棵子树有没有可能包含答案。
五、查询逻辑
查询一棵区间树时,可以按下面的逻辑走:
text
query(node, Q):
if node == null:
return
if node.left != null && node.left.maxEnd >= Q.start:
query(node.left, Q)
if node.interval 和 Q 相交:
记录 node.interval
if node.interval.start <= Q.end:
query(node.right, Q)
左子树判断:
text
node.left.maxEnd >= Q.start
含义是:
text
左子树里至少可能有一个区间的 end 到达 Q.start;
所以左子树可能有命中,需要进去查。
如果:
text
node.left.maxEnd < Q.start
说明:
text
左子树里所有区间都在 Q.start 之前结束;
整棵左子树都不可能命中。
右子树判断:
text
node.interval.start <= Q.end
因为右子树里的 start 都大于等于当前节点的 start。如果当前节点的 start 已经大于 Q.end,右子树只会更靠右,也不可能命中。
如果底层树保持平衡,查找所有重叠区间可以理解成:
text
O(log n + k)
n = 树里区间数量
k = 本次命中的区间数量
这里的 k 不能省。如果确实有很多区间都和查询区间重叠,算法至少要把这些结果返回出来。
六、动态更新:插入、删除和区间变化
如果区间集合永远不变,那建好树之后只查就行。
但真实系统里的区间经常变化:
text
新增一个区间;
删除一个区间;
一个旧区间缩小、扩大或者被拆成多个区间。
所以区间树还要解决第二个问题:
text
树变了以后,maxEnd 怎么快速维护?
插入一个新区间时,过程和普通搜索树插入很像:
text
1. 按 start 找到它应该插入的位置。
2. 插入新节点。
3. 从插入路径往上回溯,重新计算沿途节点的 maxEnd。
maxEnd 的重新计算只依赖三个值:
text
node.maxEnd =
max(node.interval.end,
node.left.maxEnd,
node.right.maxEnd)
所以不需要重扫整棵树,只要更新被插入路径影响到的祖先节点。
删除一个区间也是类似的:
text
1. 按 start/end 找到要删除的节点。
2. 按普通搜索树或红黑树删除节点。
3. 从结构发生变化的位置往上,重新计算 maxEnd。
如果底层树是红黑树或 AVL 树,插入和删除过程中还可能发生旋转。旋转属于底层平衡树的维护动作;interval tree 这一层要做的是:旋转完成后,把被旋转影响到的几个节点重新计算 maxEnd,再继续向上维护即可。
这篇 demo 选择 AVL 树做底层平衡树,所以代码里会看到一个 height 字段。它的作用不是查重叠区间,而是让 AVL 树判断自己有没有失衡:
text
balance = height(left) - height(right)
balance > 1 左子树太高,需要旋转
balance < -1 右子树太高,需要旋转
如果不维护 height,每次判断一个节点是否失衡都要重新计算左右子树高度,插入删除会多出很多重复扫描。维护 height 后,判断平衡因子就是 O(1)。
所以这两个字段的职责不同:
text
height:
底层 AVL 树用来维护平衡。
maxEnd:
区间树增强层用来跳过不可能命中的子树。
如果底层用红黑树实现,代码里通常就不会维护 height,而是维护红黑树自己的颜色字段;但 maxEnd 仍然需要维护。
也就是说,动态维护的成本取决于树高:
text
普通二叉搜索树:O(h)
平衡树: O(log n)
其中 h 是树高。工程实现里通常会把 interval tree 建在红黑树、AVL 树、B-tree 这类平衡结构上,所以插入、删除、查询都不会因为树长歪而退化。
区间发生变化时,最稳妥的理解方式是:
text
先删除旧区间;
再插入变化后的新区间。
比如旧区间:
text
[100,140]
被拆成:
text
[100,110]
[130,140]
可以理解成:
text
delete [100,140]
insert [100,110]
insert [130,140]
这两个新区间不一定连续。上面的例子里,中间的 [111,129] 就不再被任何新区间覆盖。
不要把它想成"随便原地改一下 start/end"。因为 start 是树的排序 key,end 又会影响祖先节点的 maxEnd。一旦区间变化,节点在树里的位置和一路上的增强字段都可能要更新。
对应到后面的 Linux 场景,VMA 被拆分时也有类似味道,但这里要区分"抽象理解"和"内核实际执行顺序"。
抽象地看,最终效果是:
text
原来索引里有一个大区间;
拆分后,索引里不应该再保留这个旧的大区间;
索引里应该出现拆分后的两个小区间。
如果先不管 Linux 内核处理 VMA 的具体细节,只站在 interval tree 这个索引的角度看,可以这么理解:
text
旧范围不能继续留在树里;
新范围必须重新反映到树里;
所以最终效果类似 remove old interval + insert new interval(s)。
比如:
text
before:
[100,140]
after split:
[100,110]
[130,140]
gap:
[111,129]
至于内核内部是复用旧 VMA、创建新 VMA,还是通过 pre_update/post_update、clone/link 这些路径完成维护,是后面讲反向映射实现时再展开的细节。对这篇数据结构文章来说,只需要先记住:区间变化后,索引里的旧范围要失效,新范围要进入索引,这个效果可以类比成 remove/insert。
这仍然没有改变区间树要解决的核心问题。它只是说明这棵树不是一次性建好的静态表,而是一个随 VMA 生命周期变化的在线索引。
七、用 Java 跑一个最小模型
它演示区间树核心:
text
初始化一棵空树;
插入区间;
删除区间;
把一个旧区间更新成一个新区间;
把一个旧区间替换成多个新区间;
维护 maxEnd;
查询和给定区间重叠的所有区间;
展示 left.maxEnd < query.start 时整棵左子树被跳过。
这个 demo 用 AVL 树作为底层平衡树。AVL 层负责维护 height 和旋转,区间树增强层负责维护 maxEnd。为了让代码聚焦,示例把 [start,end] 当作区间身份,重复插入同一个区间会抛异常;真实工程里通常还会用对象地址、id 或连接节点身份做 tie-break。
完整代码:
java
import java.util.ArrayList;
import java.util.List;
public class IntervalTreeCoreDemo {
static final class Interval {
private final long start;
private final long end;
Interval(long start, long end) {
if (start > end) {
throw new IllegalArgumentException("interval start must be <= end");
}
this.start = start;
this.end = end;
}
long start() {
return start;
}
long end() {
return end;
}
String label() {
return "[" + start + "," + end + "]";
}
}
static final class IntervalTree {
private static final class Node {
Interval interval;
Node left;
Node right;
int height; // AVL metadata: used to detect imbalance.
long maxEnd; // Interval-tree metadata: used to skip subtrees.
Node(Interval interval) {
this.interval = interval;
this.height = 1;
this.maxEnd = interval.end();
}
}
private static final class DeleteState {
boolean deleted;
}
private Node root;
IntervalTree() {
this.root = null;
}
boolean isEmpty() {
return root == null;
}
void insert(Interval interval) {
root = insert(root, interval);
}
boolean delete(Interval interval) {
DeleteState state = new DeleteState();
root = delete(root, interval, state);
return state.deleted;
}
boolean update(Interval oldInterval, Interval newInterval) {
return replace(oldInterval, newInterval);
}
boolean replace(Interval oldInterval, Interval... newIntervals) {
if (!delete(oldInterval)) {
return false;
}
for (Interval interval : newIntervals) {
insert(interval);
}
return true;
}
List<Interval> query(long start, long end) {
Interval query = new Interval(start, end);
List<Interval> hits = new ArrayList<>();
query(root, query, hits);
return hits;
}
void dump() {
if (root == null) {
System.out.println("(empty)");
return;
}
int levels = root.height;
int cellWidth = maxNodeTextWidth(root) + 4;
int totalWidth = cellWidth * (1 << (levels - 1));
System.out.println("node format: [start,end](height;maxEnd)");
List<Node> level = new ArrayList<Node>();
level.add(root);
for (int depth = 0; depth < levels; depth++) {
int slots = 1 << depth;
char[] line = blankLine(totalWidth);
for (int i = 0; i < level.size(); i++) {
Node node = level.get(i);
if (node != null) {
putCentered(line, nodeText(node), centerOf(totalWidth, slots, i));
}
}
System.out.println(trimRight(line));
if (depth == levels - 1) {
break;
}
char[] links = blankLine(totalWidth);
List<Node> nextLevel = new ArrayList<Node>();
for (int i = 0; i < level.size(); i++) {
Node node = level.get(i);
int parentCenter = centerOf(totalWidth, slots, i);
int leftCenter = centerOf(totalWidth, slots * 2, i * 2);
int rightCenter = centerOf(totalWidth, slots * 2, i * 2 + 1);
if (node == null) {
nextLevel.add(null);
nextLevel.add(null);
continue;
}
if (node.left != null) {
links[(parentCenter + leftCenter) / 2] = '/';
}
if (node.right != null) {
links[(parentCenter + rightCenter) / 2] = '\\';
}
nextLevel.add(node.left);
nextLevel.add(node.right);
}
System.out.println(trimRight(links));
level = nextLevel;
}
}
private Node insert(Node node, Interval interval) {
if (node == null) {
return new Node(interval);
}
int cmp = compare(interval, node.interval);
if (cmp < 0) {
node.left = insert(node.left, interval);
} else if (cmp > 0) {
node.right = insert(node.right, interval);
} else {
throw new IllegalArgumentException("duplicate interval " + interval.label());
}
return rebalance(refresh(node));
}
private Node delete(Node node, Interval interval, DeleteState state) {
if (node == null) {
return null;
}
int cmp = compare(interval, node.interval);
if (cmp < 0) {
node.left = delete(node.left, interval, state);
} else if (cmp > 0) {
node.right = delete(node.right, interval, state);
} else {
state.deleted = true;
if (node.left == null) {
return node.right;
}
if (node.right == null) {
return node.left;
}
Node successor = min(node.right);
node.interval = successor.interval;
node.right = deleteMin(node.right);
}
return rebalance(refresh(node));
}
private Node deleteMin(Node node) {
if (node.left == null) {
return node.right;
}
node.left = deleteMin(node.left);
return rebalance(refresh(node));
}
private Node min(Node node) {
while (node.left != null) {
node = node.left;
}
return node;
}
private void query(Node node, Interval query, List<Interval> hits) {
if (node == null) {
return;
}
if (node.left != null && node.left.maxEnd >= query.start()) {
query(node.left, query, hits);
} else if (node.left != null) {
System.out.printf("skip left subtree of %s: left.maxEnd=%d < query.start=%d%n",
node.interval.label(), node.left.maxEnd, query.start());
}
if (overlaps(node.interval, query)) {
hits.add(node.interval);
System.out.println("hit " + node.interval.label());
}
if (node.interval.start() <= query.end()) {
query(node.right, query, hits);
}
}
private static String nodeText(Node node) {
return node.interval.label() + "(" + node.height + ";" + node.maxEnd + ")";
}
private static int maxNodeTextWidth(Node node) {
if (node == null) {
return 0;
}
int current = nodeText(node).length();
return Math.max(current, Math.max(maxNodeTextWidth(node.left), maxNodeTextWidth(node.right)));
}
private static int centerOf(int totalWidth, int slots, int index) {
return ((2 * index + 1) * totalWidth) / (2 * slots);
}
private static char[] blankLine(int width) {
char[] line = new char[width];
for (int i = 0; i < width; i++) {
line[i] = ' ';
}
return line;
}
private static void putCentered(char[] line, String text, int center) {
int start = center - text.length() / 2;
for (int i = 0; i < text.length(); i++) {
int pos = start + i;
if (pos >= 0 && pos < line.length) {
line[pos] = text.charAt(i);
}
}
}
private static String trimRight(char[] line) {
int end = line.length;
while (end > 0 && line[end - 1] == ' ') {
end--;
}
return new String(line, 0, end);
}
private static Node refresh(Node node) {
node.height = 1 + Math.max(height(node.left), height(node.right));
long maxEnd = node.interval.end();
if (node.left != null) {
maxEnd = Math.max(maxEnd, node.left.maxEnd);
}
if (node.right != null) {
maxEnd = Math.max(maxEnd, node.right.maxEnd);
}
node.maxEnd = maxEnd;
return node;
}
private static Node rebalance(Node node) {
int balance = height(node.left) - height(node.right);
if (balance > 1) {
if (height(node.left.left) < height(node.left.right)) {
node.left = rotateLeft(node.left);
}
return rotateRight(node);
}
if (balance < -1) {
if (height(node.right.right) < height(node.right.left)) {
node.right = rotateRight(node.right);
}
return rotateLeft(node);
}
return node;
}
private static Node rotateRight(Node oldRoot) {
Node newRoot = oldRoot.left;
Node movedSubtree = newRoot.right;
newRoot.right = oldRoot;
oldRoot.left = movedSubtree;
refresh(oldRoot);
refresh(newRoot);
return newRoot;
}
private static Node rotateLeft(Node oldRoot) {
Node newRoot = oldRoot.right;
Node movedSubtree = newRoot.left;
newRoot.left = oldRoot;
oldRoot.right = movedSubtree;
refresh(oldRoot);
refresh(newRoot);
return newRoot;
}
private static int height(Node node) {
return node == null ? 0 : node.height;
}
private static boolean overlaps(Interval a, Interval b) {
return a.start() <= b.end() && b.start() <= a.end();
}
private static int compare(Interval a, Interval b) {
int start = Long.compare(a.start(), b.start());
if (start != 0) {
return start;
}
return Long.compare(a.end(), b.end());
}
}
public static void main(String[] args) {
IntervalTree tree = new IntervalTree();
System.out.println("new tree is empty: " + tree.isEmpty());
tree.insert(new Interval(100, 140));
tree.insert(new Interval(40, 60));
tree.insert(new Interval(10, 20));
tree.insert(new Interval(70, 90));
tree.insert(new Interval(300, 360));
System.out.println("\nafter insert:");
tree.dump();
long queryStart = 80;
long queryEnd = 120;
System.out.printf("%nquery [%d,%d]%n", queryStart, queryEnd);
List<Interval> hits = tree.query(queryStart, queryEnd);
System.out.println("\nmatched intervals:");
for (Interval hit : hits) {
System.out.println(" " + hit.label());
}
System.out.println("\nsplit [100,140] into [100,110] and [130,140], gap [111,129]");
tree.replace(new Interval(100, 140),
new Interval(100, 110),
new Interval(130, 140));
tree.dump();
System.out.println("\nupdate [300,360] to [320,380]");
tree.update(new Interval(300, 360), new Interval(320, 380));
tree.dump();
System.out.println("\ndelete [320,380], the current maxEnd contributor");
tree.delete(new Interval(320, 380));
tree.dump();
}
}
把上面的代码保存为 IntervalTreeCoreDemo.java 后运行:
bash
javac IntervalTreeCoreDemo.java
java IntervalTreeCoreDemo
这份 demo 没有使用 record 等较新的 Java 语法,普通类定义即可。本文用 OpenJDK 21.0.6 验证,并用 javac --release 8 编译通过。
实际输出:
text
new tree is empty: true
after insert:
node format: [start,end](height;maxEnd)
[40,60](3;360)
/ \
[10,20](1;20) [100,140](2;360)
/ \
[70,90](1;90) [300,360](1;360)
query [80,120]
skip left subtree of [40,60]: left.maxEnd=20 < query.start=80
hit [70,90]
hit [100,140]
matched intervals:
[70,90]
[100,140]
split [100,140] into [100,110] and [130,140], gap [111,129]
node format: [start,end](height;maxEnd)
[100,110](3;360)
/ \
[40,60](2;90) [300,360](2;360)
/ \ /
[10,20](1;20) [70,90](1;90) [130,140](1;140)
update [300,360] to [320,380]
node format: [start,end](height;maxEnd)
[100,110](3;380)
/ \
[40,60](2;90) [130,140](2;380)
/ \ \
[10,20](1;20) [70,90](1;90) [320,380](1;380)
delete [320,380], the current maxEnd contributor
node format: [start,end](height;maxEnd)
[100,110](3;140)
/ \
[40,60](2;90) [130,140](1;140)
/ \
[10,20](1;20) [70,90](1;90)
这个输出里,先看查询部分:
text
query [80,120]
skip left subtree of [40,60]: left.maxEnd=20 < query.start=80
hit [70,90]
hit [100,140]
它说明两件事:
text
1. 查询 [80,120] 可以返回多个命中区间:
[70,90]
[100,140]
2. 算法不是从头扫描所有区间。
[40,60] 的左子树 maxEnd = 20,小于 query.start = 80,
所以 [10,20] 这棵左子树被直接跳过。
后面的拆分、更新和删除输出说明另一件事:只要树结构或区间范围变化,沿途节点的 height 和 maxEnd 都要重新维护。这里的拆分把 [100,140] 变成 [100,110] 和 [130,140],中间故意留下 [111,129] 这个空洞,说明替换后的新区间不一定连续。最后删除 [320,380] 时,它原本贡献了全树最大的 end = 380,删除后根节点从 [100,110](3;380) 变成 [100,110](3;140),这就是删除路径上重新维护 maxEnd 的效果。
八、语言标准库里有没有现成结构
如果精确定义为:
text
支持插入区间 [start,end],
并能快速查询所有重叠区间的增强平衡树。
主流语言标准库通常没有直接内置完整 interval tree。
常见内置结构只覆盖其中一部分能力:
| 语言 / 标准库 | 内置结构 | 和 interval tree 的差距 |
|---|---|---|
Java TreeMap |
红黑树实现的 NavigableMap,按单个 key 有序。 |
只能按 key 排序,不维护子树最大结束点。 |
Rust BTreeMap |
B-tree 有序 map。 | 能做 range scan,但没有内置 interval overlap index。 |
Python bisect |
对有序 list 做二分定位。 | 没有树上的 maxEnd;list 插入也是 O(n)。 |
| Go 标准库 | container/heap、container/list、container/ring。 |
没有标准库级 ordered map,更没有 interval tree。 |
C++ std::map / std::set |
有序关联容器,常见实现是红黑树。 | 标准接口没有暴露"每棵子树最大 end"这种增强字段。 |
所以这个结构的关键不是"用了红黑树"这么简单,而是:
text
balanced ordered tree
+ interval [start,end]
+ augmented subtree maxEnd
+ query all overlaps
工程里如果需要这个结构,通常有几种选择:
text
1. 使用第三方 interval tree / segment tree / range tree 库。
2. 在语言内置 ordered map 上做范围扫描,接受最坏情况退化。
3. 自己实现增强平衡树,在节点里维护 maxEnd。
九、慢慢引到反向映射
现在只做类比,不展开内核字段。
匿名页回收时,回收器手里拿到的是一个底层页。它想知道:
text
哪些上层虚拟地址区间可能映射了这个页?
这和前面的区间树问题很像:
text
查询区间:
底层页在某套逻辑坐标里的范围。
树上的区间:
上层虚拟地址区间覆盖的逻辑坐标范围。
查询结果:
可能映射这个页的候选地址区间。
所以可以先把匿名页反向映射理解成:
text
底层页
│
├─ 从页自己的元数据里取出关系域入口
├─ 拿出自己在这套坐标里的查询范围
│
▼
在关系域的 interval tree 里找候选地址区间
│
▼
后续再做精确确认
在 Linux 里,下一篇会把这些抽象名换成具体结构:
text
底层页 -> folio
关系域 -> anon_vma
树上的区间节点 -> anon_vma_chain
候选地址区间 -> vm_area_struct
精确确认 -> 页表 PTE/PMD walk
这里的"取出关系域入口"不是再做一次搜索。对普通匿名 folio 来说,后续会看到它的元数据里能直接解出对应的 anon_vma,也就是直接知道该去哪个关系域的树上查。关键点是:folio 自己不是这棵树上的节点;树上挂的是覆盖某段坐标范围的地址区间节点。folio 提供的是"去哪棵树查"和"用哪个范围查"。
但这篇先不展开这些结构。只要记住:
text
反向映射里需要一种结构,
能根据一个页的逻辑坐标范围,
快速找出所有可能覆盖它的地址区间。
区间树解决的就是这个前置问题。
十、其他工程里的类似场景
把内核名词拿掉,这个结构适合的问题是:
text
有很多区间;
给定一个点或区间;
要快速找所有重叠区间。
类似场景:
text
日历系统
给定一个时间段,找所有重叠会议。
数据库
给定一个 key range,找覆盖它的 range lock 或分片。
编辑器 / IDE
给定源文件 byte range,找覆盖它的诊断、断点、语义 token。
存储系统
给定一个 extent,找重叠的文件范围或快照范围。
网络策略
给定 IP 段或端口段,找覆盖它的规则。
可选结构也不只 interval tree:
text
interval tree
动态插入删除和查询重叠区间都自然。
segment tree
适合坐标范围可控、需要区间聚合的场景。
range tree
适合多维范围查询。
B-tree / LSM / 数据库索引
适合数据量大、要落盘或分布式存储的场景。
R-tree / GiST
常见于空间索引、多维范围。
十一、收束
这篇只建立一个前置模型:
text
1. 核心问题:
给定查询点或查询区间,找所有重叠区间。
2. 普通 start 搜索树:
能提供一部分有序剪枝,但不知道子树里最大的 end。
3. 区间树:
在普通搜索树节点上维护 maxEnd。
4. 查询:
用 maxEnd 判断整棵子树是否可能命中。
5. 动态更新:
插入、删除、区间拆分时,沿受影响路径重新维护 maxEnd。
6. 下一篇再映射:
folio = 查询对象
anon_vma = 关系域
anon_vma_chain = 树上的区间节点
vm_area_struct = 候选地址区间
page table walk = 精确确认
一句话总结:
区间树解决的第一性问题,是给定一个点或区间,快速找出所有与它重叠的区间;反向映射只是把这个模型套到了"从底层页反查上层地址区间"这个场景里。
参考
后续映射到 Linux 反向映射时,主要会用到这些源码:
include/linux/interval_tree_generic.h:INTERVAL_TREE_DEFINE和区间重叠查询逻辑include/linux/rmap.h:struct anon_vma/struct anon_vma_chainmm/rmap.c:rmap_walk_anon()
语言标准库对照:
- Java
TreeMap: Oracle JDK 21 API - Rust
BTreeMap: Rust standard library - Python
bisect: Python 3 standard library - Go standard library
container: Go Packages - C++
std::map: cppreference