Skip to main content

Kernel Data Structures

The Linux kernel uses specialized data structures optimized for performance, concurrency, and cache efficiency. Understanding these is essential for reading kernel source code and for systems programming interviews. Think of kernel data structures as the plumbing of the operating system. User-space programs get fancy high-level containers (hash maps, vectors, trees) from their standard libraries. The kernel has to build its own — and every decision matters because these structures are hit millions of times per second across every CPU. A poorly chosen data structure in the scheduler means your 64-core server acts like a single-core machine. A cache-unfriendly list in the networking stack means dropped packets at 10Gbps. This is why kernel data structures look different from textbook implementations: they are battle-hardened for real hardware constraints.
Interview Frequency: High (especially for infrastructure roles)
Key Topics: Linked lists, RB-trees, RCU, slab allocator
Time to Master: 8-10 hours

Kernel Linked Lists

The kernel’s linked list implementation is one of the most elegant pieces of C code you’ll encounter. The analogy: Imagine a chain of hotel rooms. The traditional approach puts a list of room numbers on each door (the data structure contains pointers to the next element). The kernel approach is different: instead of a list that holds data, you embed a chain link inside each room. Any room can be on any chain, and you can have multiple chains running through the same rooms. This is the list_head pattern — the link lives inside the data, not the other way around.

Traditional vs Kernel-Style Linked Lists

Why Kernel-Style is Better

  1. Type-agnostic: Same list_head works for any struct
  2. No memory allocation: Node is part of the data
  3. Multiple lists: One struct can be on multiple lists
  4. Cache friendly: Data and links together

The Magic: container_of Macro

container_of is the “find my house given my mailbox” macro. If you know where the mailbox (list_head) is, and you know how far the mailbox is from the front door (the offset), you can find the house (the containing struct). This single macro is what makes the entire kernel-style linked list pattern work, and it appears thousands of times throughout the kernel source.

List Operations


Red-Black Trees

Used throughout the kernel for fast ordered access: process scheduling, memory management, etc. The analogy: Think of an RB-tree as a self-balancing bookshelf. You could throw books onto a pile (unsorted list, O(n) lookup) or carefully arrange them by title (sorted array, O(n) insert). An RB-tree gives you O(log n) for both — like a bookshelf that automatically reorganizes itself every time you add or remove a book, guaranteeing that no one section gets too tall compared to others. Why not a plain binary search tree? Without balancing, a BST can degenerate into a linked list (O(n) operations). The kernel cannot tolerate that — imagine the CFS scheduler taking O(n) to pick the next task on a server with 10,000 runnable processes. RB-trees guarantee O(log n) worst-case, which is why they show up in almost every performance-critical kernel subsystem.
Common Misconception: Many engineers assume the kernel uses hash tables everywhere for O(1) lookups. In reality, the kernel uses RB-trees when it needs ordered access (e.g., VMAs sorted by address range, CFS tasks sorted by vruntime). Hash tables give O(1) lookup but O(n) ordered traversal, so the choice depends on the access pattern.

RB-Tree Properties

  1. Every node is red or black
  2. Root is black
  3. All leaves (NULL) are black
  4. Red node’s children are black
  5. All paths to leaves have same black count

Kernel RB-Tree API

RB-Tree Use Cases in Kernel


Radix Trees and XArray

For sparse arrays with integer keys (like page cache). The analogy: Imagine a phone book where you can look up entries by page number. A regular array would require memory for every possible page number (billions of entries for a large file). A radix tree is like a hierarchical index — you only allocate entries for page numbers that actually exist. For a 1GB file that only has 100 pages cached, you store 100 entries instead of 262,144. The XArray replaced the older radix tree API starting in kernel 4.20 with a cleaner, more consistent interface. If you’re reading older kernel code, you’ll see radix_tree_lookup() calls; modern code uses xa_load().

XArray (Modern Replacement for Radix Tree)

Use Cases

  • Page cache: Map file offset to page
  • Process ID allocation: Map PID to task_struct
  • Inode cache: Map inode number to inode

Hash Tables

For O(1) lookups when ordering isn’t needed. When to use hash tables vs RB-trees: Hash tables win when you only need “find this exact key” lookups (like finding a socket by port number). RB-trees win when you need “find the nearest key” or “iterate in order” (like finding the VMA that contains a given address). The kernel uses both extensively, and knowing which one a subsystem chose — and why — is the kind of insight that impresses in interviews.

Kernel Hash Table API


Per-CPU Variables

Avoid cache bouncing by giving each CPU its own copy. The analogy: Imagine a shared whiteboard in an office where every employee writes tally marks. With 100 employees, they form a line, each waiting their turn. Per-CPU variables are like giving each employee their own whiteboard. Everyone writes simultaneously with zero waiting. When the manager needs the total, they walk around and add up all the whiteboards. This is exactly how per-CPU counters work in the kernel — each CPU has its own copy, and you only aggregate when you need the global value. Why this matters in practice: On a 64-core server, a shared atomic counter can become a severe bottleneck. Each atomic_inc() invalidates the cache line on all 63 other CPUs (a phenomenon called “cache-line bouncing” or “false sharing”). Per-CPU variables eliminate this entirely. The kernel uses them for statistics counters, memory allocator free lists, and many other high-contention paths.

Why Per-CPU Matters


RCU (Read-Copy-Update)

The most important synchronization mechanism for read-heavy kernel data structures. The analogy: Imagine you’re editing a Wikipedia article. The traditional approach (locking) would be: lock the page, make edits, unlock. While locked, nobody can read it. RCU works differently: you make a copy of the article, edit the copy, then atomically swap the link so new readers see the new version. Old readers who started before the swap still see the old version — and that’s fine, because they’ll finish reading soon. Once all old readers are done, you delete the old copy. Readers never wait, ever. Why RCU matters: The routing table in a busy Linux server might be read millions of times per second (every packet triggers a lookup) but updated only a few times per day. Using traditional locks would serialize all those reads. RCU makes reads essentially free — just a preemption disable/enable pair, no atomic operations, no memory barriers on the read path. This is why the networking stack, filesystem layer, and module system all depend on RCU.
Common Misconception: “RCU is just a fancy lock.” It is not a lock at all. Readers never block, and there is no lock acquisition on the read path. RCU is a publication mechanism combined with a deferred reclamation scheme. The grace period concept (waiting for all pre-existing readers to finish) is fundamentally different from mutual exclusion.

RCU Concept

RCU API

Why RCU is Important

  • Zero-overhead reads: No atomics, no memory barriers on read path
  • Scalability: Readers never block each other
  • Used everywhere: Routing tables, file descriptors, module list

Memory Allocators

kmalloc - Small Object Allocation

GFP Flags

vmalloc - Large Allocations

kmalloc vs vmalloc

Slab Allocator

For frequently allocated objects of the same size:

Slab Allocator Internals


Lab Exercises

Objective: Use kernel-style linked lists in user space
Objective: Analyze kernel memory allocation
Objective: Understand RCU behavior

Interview Questions

Answer:container_of(ptr, type, member) returns a pointer to the containing structure given a pointer to a member.How it works:
  1. offsetof(type, member) - bytes from struct start to member
  2. (char *)ptr - treat ptr as byte pointer
  3. Subtract offset to get struct start
  4. Cast to struct pointer
Example:
Why important: Enables type-agnostic data structures in C.
Answer:Use RCU when:
  • Reads vastly outnumber writes
  • Read-side performance is critical
  • Can tolerate slightly stale data on reads
  • Updates can use copy-and-replace semantics
Use spinlock when:
  • Read/write ratio is balanced
  • Need to modify data in place
  • Can’t tolerate stale reads
  • Critical section is very short
Performance comparison:Real examples:
  • Routing table: RCU (millions of lookups, rare updates)
  • Memory allocator: Spinlock (frequent alloc/free)
Answer:Problems with raw page allocation:
  • Internal fragmentation (small objects waste pages)
  • Slow initialization (must init objects each time)
  • Cache unfriendly (random placement)
Slab benefits:
  1. Reduced fragmentation: Groups same-size objects
  2. Cache coloring: Staggers objects across cache lines
  3. Object caching: Keeps freed objects initialized
  4. Per-CPU caches: Reduces lock contention
  5. Debugging support: Red zones, poisoning
Example impact:
  • Creating process: task_struct allocated instantly from slab
  • Without slab: Would need page allocation + initialization
  • Measured speedup: 10-100x for hot objects
Answer:What are per-CPU variables:
  • Each CPU gets its own copy of the variable
  • No synchronization needed for CPU-local access
Benefits:
  1. No cache bouncing: Variable always in local cache
  2. No locking needed: Each CPU has exclusive access
  3. Better scalability: Performance scales with CPU count
Implementation:
Use cases:
  • Counters (stats, metrics)
  • Caches (per-CPU object pools)
  • State (current process, interrupt flags)
Trade-off: Uses more memory (N copies) for better performance.

Debugging Tips

Practical Debugging Guidance for Kernel Data StructuresThese tips will save you hours when you’re tracing bugs in kernel code or kernel modules.
Debugging RCU Stalls: If you see “rcu_sched self-detected stall on CPU” in dmesg, it means a CPU has been in an RCU read-side critical section for too long (or RCU callbacks cannot run). Common causes: a tight loop with preemption disabled, a softirq handler that never yields, or a spinlock held too long. This is a serious bug that can make the system unresponsive.

Common Misconceptions


Key Takeaways

Kernel Lists

Embedded list_head pattern enables type-agnostic, efficient linked lists

RCU

Read-Copy-Update provides scalable read-heavy synchronization

Slab Allocator

Object caching and per-CPU pools optimize frequent allocations

Per-CPU Variables

Eliminate cache bouncing for frequently accessed data

Interview Deep-Dive

Strong Answer:
  • The scheduler needs three operations to be efficient: find the minimum vruntime task (the next task to run), insert a task when it becomes runnable, and remove a task when it is selected or blocks. A red-black tree provides O(log n) for all three operations, and the leftmost node (minimum) can be cached for O(1) access.
  • A min-heap gives O(1) find-min and O(log n) insert, but removal of an arbitrary element (not the minimum) is O(n) because you need to find it first. The scheduler frequently removes tasks that block (sleep on I/O, mutex, etc.), not just the minimum, so this O(n) removal is unacceptable.
  • A sorted array gives O(1) find-min and O(log n) search, but insertion requires shifting elements, which is O(n). With hundreds or thousands of runnable tasks, this becomes expensive.
  • A skip list would work (O(log n) for all operations on average), but red-black trees have lower constant factors due to better cache locality (tree nodes are compact structs, skip list nodes have variable-sized forward pointer arrays) and deterministic worst-case guarantees (skip lists are probabilistic).
  • The kernel’s rb-tree implementation is also extremely well-optimized: it uses augmented rb-trees where the leftmost node pointer is cached in rb_root_cached, making pick_next_task_fair() effectively O(1). The rebalancing after insertion or removal touches at most 3 rotations, keeping the tree balanced without expensive restructuring.
Follow-up: How does the kernel’s container_of macro make the rb-tree implementation type-agnostic, and why is this important?Follow-up Answer:
  • The kernel’s rb-tree does not store user data in tree nodes. Instead, the rb_node structure is embedded inside the user’s data structure (for example, sched_entity contains an rb_node run_node). When traversing the tree, you get pointers to rb_node structures. container_of(ptr, struct sched_entity, run_node) uses pointer arithmetic (subtracting the field offset from the pointer) to recover the containing sched_entity pointer. This means the same rb-tree code works for any data type without templates, generics, or void pointers with casts. It also means zero memory allocation overhead for the tree structure itself — the link nodes are part of the data, so inserting a task requires no additional memory allocation.
Strong Answer:
  • The fundamental difference is that RCU readers do not acquire any lock at all. rcu_read_lock() merely disables preemption (a single per-CPU counter increment). There is no atomic operation, no memory barrier, no cache line bouncing between CPUs. This means RCU read-side critical sections scale perfectly: 1000 CPUs reading simultaneously have zero contention. A read-write lock, even an optimized one like rwlock_t, requires atomic operations on a shared cache line, creating contention that increases with CPU count.
  • RCU achieves this by shifting all synchronization cost to the write side. A writer does not modify data in place. Instead, it allocates a new copy, modifies the copy, then atomically updates the pointer to the new version using rcu_assign_pointer() (which includes a write memory barrier). Old readers continue reading the old version — they are safe because the old data is not freed until all pre-existing readers finish. The writer calls synchronize_rcu() or call_rcu() to wait for the grace period.
  • A grace period is the time during which all CPUs have gone through at least one context switch (or voluntary quiescent state). Since rcu_read_lock() disables preemption, a context switch on a CPU guarantees no reader on that CPU is in an RCU read-side critical section. Once all CPUs have passed through a quiescent state, the old data can safely be freed.
  • The trade-off: writers pay a significant cost (memory allocation for copies, waiting for grace periods), and readers see stale data during the grace period. This makes RCU ideal for read-mostly data (routing tables, configuration, module lists) and terrible for write-heavy data.
Follow-up: What is SRCU (Sleepable RCU) and when would you use it instead of regular RCU?Follow-up Answer:
  • Regular RCU readers cannot sleep because rcu_read_lock() disables preemption, and sleeping while preemption is disabled would cause scheduling problems. SRCU (Sleepable RCU) allows readers to sleep by using a per-CPU counter pair instead of preemption disable. srcu_read_lock() increments one counter, srcu_read_unlock() increments the other. The writer’s synchronize_srcu() waits until both counters match, indicating no readers are active. This is more expensive on the read side (two atomic increments instead of one preemption counter) but necessary when the read-side critical section must perform blocking operations like disk I/O or sleeping locks. Use cases include file system operations where you need RCU-like read scalability but the read path might block.
Strong Answer:
  • The options are: kernel hash table (hashtable.h), rhashtable (resizable hash table), rb-tree, and XArray.
  • For 10 million entries with fast lookup and frequent updates, I would use rhashtable (resizable hash table with RCU-protected reads). The standard kernel hashtable.h has a fixed number of buckets set at compile time. With 10M entries and 1024 buckets, each bucket chain averages 10K entries, making lookup O(10K) — unacceptable. Rhashtable dynamically resizes its bucket count to maintain short chains (target 0-1 entries per bucket), providing amortized O(1) lookup.
  • Rhashtable also supports RCU-protected reads: lookups do not take any locks, only rcu_read_lock(). Insertions and deletions take a per-bucket spinlock, providing fine-grained write concurrency. The resizing operation is done incrementally in the background using RCU, so readers are never blocked during a resize.
  • An rb-tree would give O(log n) lookup (about 23 comparisons for 10M entries), which is slower than a well-sized hash table’s O(1). XArray is designed for sparse integer-indexed data (like page cache indices), not arbitrary key types like IP addresses.
  • Implementation: I would define the rhashtable parameters with struct rhashtable_params specifying the key offset, key length (4 bytes for IPv4), hash function (jhash), and a reasonable initial size. For IPv6 support, the key length increases to 16 bytes but the approach is identical.
Follow-up: What are the memory overhead considerations for 10 million entries, and how does rhashtable handle memory pressure?Follow-up Answer:
  • Each rhashtable entry requires the user struct plus an rhash_head (a single pointer, 8 bytes on 64-bit). The bucket array itself is an array of pointers, which for 10M entries with a load factor of 0.75 would be approximately 13.3M buckets, each 8 bytes = ~106MB just for buckets. During resize, both old and new bucket arrays coexist temporarily, doubling the bucket memory. The user data (connection metadata) dominates memory usage at this scale. Under memory pressure, rhashtable allocation can fail during resize. The rhashtable_params structure allows specifying GFP_NOWAIT for non-blocking allocation and setting max_size to cap bucket growth. If resize allocation fails, the table continues operating with longer chains (degraded but functional).

Next: Process Subsystem Deep Dive →