How Linux CFS Bandwidth Control Works
This article provides an architectural overview of how the Linux kernel implements Completely Fair Scheduler (CFS) bandwidth control. It examines the control group (cgroup) parameters governing CPU time limits, the split-accounting model between global pools and per-CPU runqueues, the mechanics of process throttling, and how high-resolution timers replenish CPU quotas to resume suspended tasks.
The Core Concept: Period and Quota
CFS bandwidth control allows system administrators to enforce hard limits on CPU consumption for groups of processes using the cgroup hierarchy. Unlike standard CFS weights, which provide proportional shares under CPU contention, bandwidth control sets absolute ceilings.
It is defined using two primary parameters:
- Period (
cpu.cfs_period_us): The tracking window duration, typically defaulting to 100,000 microseconds (100 ms). - Quota (
cpu.cfs_quota_us): The total cumulative execution time allowed for all tasks within the cgroup across all CPUs during that period.
If a multi-threaded cgroup has a period of 100 ms and a quota of 200 ms, the tasks combined can consume up to two full CPU cores of execution time within that 100 ms window.
The Accounting Architecture: Global vs. Per-CPU
Enforcing quota globally across multiple CPU cores introduces severe lock contention if every CPU updates a single shared counter. To solve this, the kernel splits bandwidth management into two structures:
cfs_bandwidth(Global): A single structure per task group that maintains the global quota balance, tracking period timers, and throttling state.cfs_rq(Per-CPU): The runqueue on each individual core where tasks are scheduled and run.
To minimize cross-CPU locking overhead, a core does not acquire the
global cfs_bandwidth lock on every scheduler tick. Instead,
each per-CPU runqueue dynamically requests a slice of execution
time—known as a runtime pool (typically 5 ms by default)—from the global
quota.
As a task executes on a CPU, its elapsed execution time is deducted
from its local runqueue balance. When the local balance is exhausted,
the CPU attempts to acquire another slice from the global
cfs_bandwidth pool.
Throttling: Enforcing the Limit
Throttling occurs when a CPU runqueue needs more runtime, but the
global quota in cfs_bandwidth has run out:
- Exhaustion Detection: When a per-CPU runqueue checks the global pool and discovers that the quota is zero or negative, the kernel flags the runqueue as throttled.
- Removal from the Runqueue: The kernel removes the
throttled
cfs_rqfrom the active scheduling tree (the red-black tree). The tasks inside this group remain in a runnable state (TASK_RUNNING), but the scheduler bypasses them because their runqueue is marked inactive. - Throttled List: The throttled runqueue is placed
onto a linked list tracked by the global
cfs_bandwidthstructure to await quota replenishment.
While throttled, the affected tasks consume no CPU cycles, ensuring the group does not exceed its defined upper bound.
Replenishment and Unthrottling
To restore execution, the kernel relies on high-resolution timers
(hrtimer):
- Timer Expiration: At the start of a period, the
kernel arms an
hrtimerconfigured to fire at the end ofcfs_period_us. - Quota Reset: When the timer expires, the kernel
recharges the global pool by adding the full configured quota back into
cfs_bandwidth. - Distributing Runtime: The kernel walks the list of throttled per-CPU runqueues, assigns them new runtime slices, and clears their throttled status.
- Re-enqueueing: The formerly throttled
cfs_rqstructures are re-added to their respective active CPU runqueues. - Wake-up: The scheduler triggers an inter-processor interrupt (IPI) if necessary to reschedule the unthrottled tasks immediately.
The timer is then rescheduled for the next period, repeating the enforcement cycle continuously.