区间树:反向映射的前置数据结构

区间树:反向映射的前置数据结构

这篇只讲一个前置数据结构:区间树

先不展开 anon_vmaanon_vma_chainvmafolio 这些内核结构。真正讲匿名页反向映射时,再把这些结构套进来。

先给一个简短定义:

区间树可以理解成一棵被增强过的有序平衡树。 原本平衡树的节点通常保存一个具体的 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_updateclone/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] 这棵左子树被直接跳过。

后面的拆分、更新和删除输出说明另一件事:只要树结构或区间范围变化,沿途节点的 heightmaxEnd 都要重新维护。这里的拆分把 [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/heapcontainer/listcontainer/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 反向映射时,主要会用到这些源码:

语言标准库对照:

相关推荐
半条-咸鱼7 小时前
【FreeRTOS】核心原理与实战速查手册
c语言·操作系统·rtos
花花无缺1 天前
Windows 定时执行 Python 脚本方案
python·操作系统·命令行
小宇子2B1 天前
LRU 不只是最近最少使用:active/inactive、anon/file、folio 和分代 LRU
操作系统
凉、介1 天前
Linux 设备驱动匹配机制
linux·笔记·单片机·学习·操作系统·嵌入式
沅姬2 天前
如何优雅地关闭找不到终端的 roslaunch 进程
操作系统
小宇子2B3 天前
从 malloc 到物理页:把 Linux 内存管理链路串起来
操作系统
无忧.芙桃3 天前
线程同步与线程互斥(上)
linux·运维·操作系统·运维开发
zandy10115 天前
从可用到好用:2026年信创改造建设之邮件系统经验分享
经验分享·中间件·操作系统·信创·国产cpu
岑梓铭6 天前
考研408《操作系统》复习笔记,第四章《4.2.2 文件操作、文件共享、文件保护》
linux·考研·操作系统·文件系统·408