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:

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:

  1. cfs_bandwidth (Global): A single structure per task group that maintains the global quota balance, tracking period timers, and throttling state.
  2. 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:

  1. 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.
  2. Removal from the Runqueue: The kernel removes the throttled cfs_rq from 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.
  3. Throttled List: The throttled runqueue is placed onto a linked list tracked by the global cfs_bandwidth structure 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):

  1. Timer Expiration: At the start of a period, the kernel arms an hrtimer configured to fire at the end of cfs_period_us.
  2. Quota Reset: When the timer expires, the kernel recharges the global pool by adding the full configured quota back into cfs_bandwidth.
  3. Distributing Runtime: The kernel walks the list of throttled per-CPU runqueues, assigns them new runtime slices, and clears their throttled status.
  4. Re-enqueueing: The formerly throttled cfs_rq structures are re-added to their respective active CPU runqueues.
  5. 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.