DSA DSA In Depth
0%
case linux-kernelrbtreecfs-scheduler

Production Case Study: Intrusive Augmented Red-Black Trees in the Linux Kernel CFS & VMA Manager

7 min read Module 3.2: Self-Balancing, Augmented, Disk-Backed & Persistent Trees Updated Oct 23, 2026
📄 Cheat Sheet
Linux Completely Fair Scheduler (CFS) & Red-Black Tree Runqueue Internals
Introduction to Operating Systems Linux Completely Fair Scheduler (CFS) & Red-Black Tree Runqueue Internals Watch on YouTube ↗

Explains how the Linux kernel Completely Fair Scheduler indexes runnable tasks by virtual runtime (vruntime) inside an augmented Red-Black Tree with an O(1) leftmost cache.

0:00 / --:--

Three Kernel Engineering Breakthroughs in Linux rbtree.h

1. Intrusive Embedding & container_of(ptr, type, member)

In C++ std::map or Java TreeMap, inserting an object allocates a heap wrapper containing left, right, parent, color, and a pointer/copy of the payload. In Linux, struct rb_node contains only three unsigned long words (24 bytes): __rb_parent_color, rb_right, and rb_left. It is embedded directly inside the domain struct (and a single domain struct can embed multiple rb_node fields to participate in multiple trees simultaneously with zero heap allocations!). Given a struct rb_node *node, the macro rb_entry(node, struct task_struct, run_node) subtracts the compile-time offsetof(struct task_struct, run_node) in zero CPU instructions (folded into the base-displacement addressing mode mov rax, [rdi - 0x48])!

2. rb_root_cached: $O(1)$ rb_first() for the CFS vruntime Scheduler

The Linux Completely Fair Scheduler (CFS, introduced in 2.6.23 by Ingo Molnar) orders runnable tasks by their virtual runtime (vruntime in nanoseconds). On every context switch (thousands of times per second per core), CFS must pick the task with the smallest vruntime. Walking down the left spine in $O(\log N)$ on every context switch would waste precious cycles. Instead, Linux uses struct rb_root_cached, which stores rb_root alongside a cached pointer rb_leftmost. Whenever a new node is inserted and never branches right on its way down, rb_leftmost is updated in $O(1)$—making pick_next_task_fair() a single $O(1)$ pointer read!

3. Augmented Interval Gap Tree (rb_subtree_gap) in vm_area_struct

When a process calls mmap() to allocate an unmapped virtual address region of length $L$, the kernel used to scan the linked list of Virtual Memory Areas (VMAs) in $O(N)$ time. Since Linux 3.6, struct vm_area_struct embeds an Augmented Red-Black Tree (rbtree_augmented.h) where each VMA node maintains rb_subtree_gap: the maximum contiguous free virtual address gap anywhere in its subtree, computed via custom rotation callbacks (RB_DECLARE_CALLBACKS_MAX) to find a suitable address hole in $O(\log N)$ time! *(Note: In Linux 6.1+, VMA tracking has also introduced the RCU-safe B-tree-like Maple Tree alongside rbtree_augmented used in DRM/interval-tree subsystems.)*

Feature / Metric Standard Non-Intrusive RB-Tree (std::map) Linux Kernel Intrusive rb_root_cached
Dynamic Heap Allocations on Insert 1 operator new call per insert 0 Allocations (Pre-embedded in struct!)
Color Storage Overhead 4–8 bytes (due to struct alignment padding) 0 Bytes (Bit 0 of __rb_parent_color)
Minimum Element Lookup (rb_first) O(log N) left-spine walk O(1) via rb_root_cached.rb_leftmost
Cache Lines Touched per Comparison 2 (Tree node wrapper $\to$ payload pointer) 1 (rb_node sits in same cache line as key!)

Bit-Level Anatomy of __rb_parent_color & container_of

Because struct rb_node contains unsigned long fields, the C ABI aligns every struct rb_node to at least a 4-byte or 8-byte boundary. Therefore, the least significant 2 bits of any struct rb_node * address are guaranteed to be 00:

$$\text{rb\_parent}(r) = (\text{struct rb\_node }*)\big(r\to\text{\_\_rb\_parent\_color} \;\&\; \sim 3\text{UL}\big)$$

$$\text{rb\_color}(r) = r\to\text{\_\_rb\_parent\_color} \;\&\; 1\text{UL} \quad (0 = \text{RB\_RED}, \; 1 = \text{RB\_BLACK})$$

And given a pointer ptr to member member inside type, container_of recovers the outer struct pointer via standard byte-pointer arithmetic:

$$\text{container\_of}(\text{ptr}, \text{type}, \text{member}) = (\text{type }*)\big((\text{char }*)\text{ptr} - \text{offsetof}(\text{type}, \text{member})\big)$$

Augmented Order-Statistic (rank/select) & Interval Overlap Tree
O(log N) Augmented Descent · O(1) Rotation Fixup

Explore the Augmentation Theorem: maintain subtree maxEnd = max(hi, L.maxEnd, R.maxEnd) for O(log N) interval overlap search and Linux VMA gap allocation, or subtree size sz for O(log N) select(k) / rank(x).

Step 1 / 8
Custom Input:
Timeline

Invariant & State

Deterministic Single-Path Theorem: If u.left.maxEnd ≥ q.lo (23 ≥ 14), we are guaranteed to either find an overlap in u.left OR prove no overlap can exist in u.right!
[17,19] max=30 [5,8] L.max=23 ≥ 14 [21,24] max=30 Query [14, 16]: Root [17,19] misses, but u.left.maxEnd (23) ≥ 14 ⟹ Go Left ONLY!
Search Step
searchOverlap([14, 16]) at Root
Pruning Predicate
u.left.maxEnd (23) ≥ q.lo (14)
Guarantee
Hit [15, 23] inside Left Subtree in O(log N)
[Step 1] Deterministic Single-Path Theorem: If u.left.maxEnd ≥ q.lo (23 ≥ 14), we are guaranteed to either find an overlap in u.left OR prove no overlap can exist in u.right!
struct sched_entity (Base Address = 0x1000) +0x00: struct load_weight load; +0x18: struct rb_node run_node; (24 Bytes) • __rb_parent_color = (parent_ptr | color_bit) • rb_right, rb_left +0x30: u64 vruntime; (Same 64B Cache Line!) struct rb_root_cached • rb_root.rb_node → Tree Root • rb_leftmost → Min vruntime (O(1)!) rb_entry(ptr, struct sched_entity, run_node): (char*)ptr - 0x18 = 0x1000 (Zero extra loads!) Zero malloc/free during context switch!
flowchart LR Insert["cfs_enqueue_entity(cfs_rq, se)"] --> Walk["Walk down rb_root_cached Track bool leftmost = true (Set leftmost = false if branching right)"] Walk --> Cache{"leftmost == true ?"} Cache -- "Yes" --> SetMin["cfs_rq->rb_leftmost = &se->run_node (O(1) update of min vruntime!)"] Cache -- "No" --> Rebal["rb_insert_color(&se->run_node)"] SetMin --> Rebal

Hardware Reality: Co-Locating rb_node and vruntime in the Same 64-Byte Cache Line

Look closely at struct sched_entity in include/linux/sched.h: the kernel developers deliberately placed struct rb_node run_node (24 bytes) immediately adjacent to u64 vruntime (8 bytes) inside the first 64-byte cache line of sched_entity. When the CPU traverses node = node->rb_left and loads the cache line containing run_node, the comparison key vruntime is already sitting in that exact same 64-byte L1 cache line! A non-intrusive tree would suffer two dependent cache misses per level (one to load the tree node, a second to chase the pointer to the sched_entity).

Implementation: Linux-Style Intrusive Augmented RB-Node & container_of

// C11 / C++20: Linux Kernel-Style Intrusive RB-Node with Tagged Parent-Color & container_of
#include <cstddef>
#include <cstdint>

struct alignas(8) rb_node {
    uintptr_t __rb_parent_color; // Bit 0: 0 = RED, 1 = BLACK; Bits [63:2] = parent pointer
    rb_node* rb_right = nullptr;
    rb_node* rb_left  = nullptr;

    rb_node* parent() const noexcept {
        return reinterpret_cast<rb_node*>(__rb_parent_color & ~uintptr_t(3));
    }
    bool is_black() const noexcept { return (__rb_parent_color & 1U) != 0; }
    void set_parent_and_color(rb_node* p, int color) noexcept {
        __rb_parent_color = reinterpret_cast<uintptr_t>(p) | (color & 1U);
    }
};

struct rb_root_cached {
    rb_node* rb_node_root = nullptr;
    rb_node* rb_leftmost  = nullptr; // O(1) access to minimum element
};

#define rb_entry(ptr, type, member)     reinterpret_cast<type*>(reinterpret_cast<char*>(ptr) - offsetof(type, member))

// Example Domain Object: Runnable Task Entity with Zero-Allocation Intrusive Node
struct TaskEntity {
    uint64_t pid;
    rb_node run_node;      // Co-located in the same cache line as vruntime!
    uint64_t vruntime;
};

inline TaskEntity* pickNextTaskO1(const rb_root_cached& rq) noexcept {
    if (!rq.rb_leftmost) return nullptr;
    return rb_entry(rq.rb_leftmost, TaskEntity, run_node);
}

Failure Modes & Adversarial Pitfalls

  • Inserting an Already-Linked Intrusive rb_node Into a Second Tree Instance: Because an intrusive rb_node lives inside the object itself, inserting an object into Tree B while it is still linked inside Tree A overwrites its rb_left/rb_right/__rb_parent_color pointers in place, instantly corrupting Tree A! If an object must belong to two trees simultaneously, it must embed two distinct rb_node fields (e.g., rb_node by_vruntime; and rb_node by_pid;).
  • Misaligned rb_node Corrupting the Color Bit: If a struct containing rb_node is packed with __attribute__((packed)) so that &node sits at an odd byte address, bit 0 of the pointer is 1, colliding with RB_BLACK! Always enforce alignas(sizeof(long)) on struct rb_node.
Check yourself Question 1 of 4
Concept Check Intrusive `struct rb_node` vs Standard Pointer-Wrapper Nodes recall

In the Linux kernel (`<linux/rbtree.h>`), `struct rb_node` contains no `key` or `value` fields—only `unsigned long __rb_parent_color; struct rb_node *rb_right; struct rb_node *rb_left;`.

⚡ Interactive Challenge · Staff Level

Algorithmic Design & Complexity Check

Analyze the invariant, asymptotic bound, and hardware behavior before selecting.

How does EEVDF find an eligible task with the minimum virtual deadline in $O(\log N)$ time using a single intrusive Red-Black tree?

Key Takeaways

Intrusive trees embed struct rb_node directly inside the domain struct and recover the enclosing object via container_of / rb_entry pointer subtraction with zero dynamic heap allocations.
Co-locating rb_node (24B) and the comparison key (vruntime, 8B) in the same 64B cache line eliminates the second pointer-indirection cache miss at every tree level.
Packing the Red/Black color bit into bit 0 of the 8-byte aligned parent pointer (__rb_parent_color) eliminates 8 bytes of struct alignment padding per node.
Linux's rb_root_cached maintains a direct pointer to rb_leftmost, turning CFS scheduler minimum-vruntime selection into an O(1) pointer read.
Was this lesson helpful? Spot an issue or have a suggestion? Let us know on GitHub.
Report an issue