How Does the Linux CFS Use a Red-Black Tree for vruntime?
The Linux Completely Fair Scheduler uses a self-balancing red-black tree to organize runnable tasks by their virtual runtime, ensuring that every process receives a fair share of processor time. By keeping tasks sorted in this tree structure, the scheduler can quickly identify and dispatch the task that has fallen furthest behind in execution time.
Understanding Virtual Runtime
In classical computing, a single processor core can only execute one
thread at a time, so the operating system must rapidly switch between
tasks to create the illusion of simultaneous execution. The Completely
Fair Scheduler, or CFS, tracks how much CPU time each task has received
using a metric called virtual runtime, commonly referred to as
vruntime.
Rather than measuring raw wall-clock time alone,
vruntime scales actual execution time according to a task's
priority, which is determined by its nice value. A task with a higher
priority (lower nice value) accumulates vruntime more
slowly than a task with a lower priority. The relationship is expressed
as:
\[\Delta vruntime = \Delta t \times \frac{W_0}{W_i}\]
Here, \(\Delta t\) is the actual
physical time the task spent running on the CPU, \(W_0\) is the default weight for a task with
a nice value of 0, and \(W_i\) is the
weight of the current task. Because higher-priority tasks have a larger
\(W_i\), their vruntime
increases at a slower rate, allowing them to run more frequently.
Why CFS Uses a Red-Black Tree
To decide which task should run next, CFS needs a data structure that
can efficiently maintain a sorted order of all runnable tasks as their
vruntime values constantly change. CFS uses a red-black
tree, which is a self-balancing binary search tree.
In this tree, each node represents a runnable task
(sched_entity), and the tree is keyed by each task's
vruntime. A red-black tree guarantees that the longest path
from the root to any leaf is no more than twice as long as the shortest
path. This self-balancing property ensures that inserting, deleting, and
updating tasks all occur in \(O(\log
n)\) time, where \(n\) is the
number of runnable tasks.
Selecting and Updating Tasks
Because the red-black tree is ordered by vruntime, the
task with the smallest vruntime—and therefore the greatest
need for CPU time—is always located at the leftmost node of the
tree.
To avoid traversing the tree from the root every time a context switch occurs, CFS caches a pointer to this leftmost node. This optimization allows the scheduler to pick the next task to execute in \(O(1)\) constant time.
The scheduling cycle follows a straightforward sequence:
- CFS selects the leftmost node from the red-black tree and dispatches that task to the CPU.
- As the task executes, its
vruntimeincreases based on the weighted execution time. - When a timer tick occurs or the task is preempted, CFS updates the
task's
vruntime. - If the task is still runnable, it is reinserted into the red-black tree at its new position, and the cached leftmost pointer is updated if necessary.
Through this combination of weighted vruntime
calculations and a self-balancing red-black tree, CFS provides
predictable, low-overhead scheduling that scales smoothly as the number
of active processes grows.