Time Slices and Request Sizes in EEVDF

Under EEVDF (Earliest Eligible Virtual Deadline First, the default fair scheduler since Linux 6.6), a task’s time slice is not a fixed quantum the way it was in classic round-robin schedulers — it is a request size r_i, the chunk of CPU the task asks to run for in one go. EEVDF combines that request with the task’s weight w_i to compute a virtual deadline vd_i = ve_i + r_i / w_i, and then always runs the eligible task with the earliest virtual deadline (sched-eevdf, fair.c v6.12). The default request size for every fair task is a single tunable, sysctl_sched_base_slice750 µs in 6.12 LTS, retuned to 700 µs in 6.18 LTS. Crucially, EEVDF lets a task choose its own request size through sched_setattr(2)’s sched_runtime field: a smaller slice yields an earlier deadline and thus lower scheduling latency at the cost of more context switches, while a larger slice trades latency for throughput. This single mechanism replaced CFS’s two-knob sched_latency / sched_min_granularity scheme.

This note is pinned to 6.12 LTS (released 2024-11-17) and 6.18 LTS (2025-11-30), with the base-slice value difference between them called out explicitly below. For the lag/eligibility half of EEVDF see Eligibility Lag and Virtual Deadlines in EEVDF; for the weight that divides the slice see Nice Values Weights and Priority Scaling.

Mental Model

Picture each runnable task standing in line, holding a ticket that says “I would like to run for r microseconds.” EEVDF converts that request into a deadline in virtual time and serves whoever’s deadline comes soonest — but only among tasks that are eligible (have not already run ahead of their fair share; see the lag note). A task that asks for a short run gets a near deadline, so it cuts toward the front of the line and is dispatched quickly — ideal for a latency-sensitive thread (audio, an interactive UI loop) that wakes often and only needs the CPU briefly. A task that asks for a long run gets a far deadline and waits longer to start, but once running it holds the CPU for the full chunk before the next preemption check — ideal for a throughput job that wants to amortize cache warm-up and avoid switch overhead. Both tasks get the same long-run share of the CPU (that is set by weight, not slice); they differ only in how the share is delivered in time.

flowchart TB
  REQ["request size r_i<br/>(base 700us in 6.18,<br/>or custom via sched_runtime)"] --> CALC["calc_delta_fair(r_i, se)<br/>= r_i / w_i  (weight-divide)"]
  VE["virtual eligible time ve_i<br/>(= se.vruntime)"] --> ADD["vd_i = ve_i + r_i / w_i"]
  CALC --> ADD
  ADD --> TREE["augmented rbtree<br/>keyed by virtual deadline"]
  TREE --> PICK["pick: earliest deadline<br/>among eligible (lag >= 0)"]
  PICK -->|small r_i| LAT["near deadline -> picked sooner<br/>LOW LATENCY, more switches"]
  PICK -->|large r_i| THR["far deadline -> picked later<br/>HIGH THROUGHPUT, fewer switches"]

How a request size becomes a scheduling decision. What it shows: the request r_i is divided by the task’s weight to produce a virtual-time offset, added to the task’s eligible time to form the virtual deadline, which keys the run-queue tree; the scheduler picks the earliest-deadline eligible task. The insight to take: the slice is a latency knob, not a share knob — shrinking r_i pulls the deadline earlier (faster dispatch, more frequent switching) while the weight w_i alone determines the long-run CPU fraction. A short slice on a heavy task and a long slice on a light task can coexist coherently.

From CFS’s Two Knobs to One

To appreciate sched_base_slice, you have to know what it replaced. CFS (the Completely Fair Scheduler, default 2.6.23 through 6.5) sized timeslices indirectly through two tunables: sched_latency (the target period in which every runnable task should run at least once — historically ~6 ms scaled by CPU count) and sched_min_granularity (a floor on how short any one slice could get, ~0.75 ms, to cap switching overhead). CFS computed a per-task slice by partitioning sched_latency proportionally to weight, clamped below by min_granularity. This was awkward: the period stretched as more tasks arrived, the two knobs interacted nonlinearly, and there was no clean way for a task to ask for a particular latency (sched-design-CFS).

EEVDF discards that scheme. There is now a single base quantum, sysctl_sched_base_slice, which is the request size every fair task uses by default, and the per-task slice is no longer derived from a stretching global period — it is just this base (or a task-chosen custom value). From kernel/sched/fair.c:

/* v6.12 LTS */
unsigned int sysctl_sched_base_slice = 750000ULL;   /* 750 microseconds */
 
/* v6.18 LTS */
unsigned int sysctl_sched_base_slice = 700000ULL;   /* 700 microseconds */

Uncertain

Verify: the reason the base slice was retuned from 750 µs (6.12) to 700 µs (6.18), and whether any intermediate stable release used a different value. Reason: both values are directly confirmed from the pinned kernel/sched/fair.c blobs at tags v6.12 and v6.18, but the changelog/commit motivating the 700 µs retune was not consulted in this pass. To resolve: git log -L the sysctl_sched_base_slice line between v6.12 and v6.18, or read the merge-window LWN coverage for the release that changed it. uncertain

The value is exposed (read-write, with CONFIG_SCHED_DEBUG) at /sys/kernel/debug/sched/base_slice_ns, in nanoseconds. It is a system-wide default, not per-task; a task overrides it only by requesting a custom slice (next section).

The Deadline Formula, Symbol by Symbol

EEVDF’s defining equation, written in the kernel’s own comments, is vd_i = ve_i + r_i / w_i (fair.c v6.12). Expanding each symbol:

  • vd_i — the virtual deadline of task i: the point in virtual time by which the task should have finished its current request. The scheduler sorts eligible tasks by this and picks the smallest.
  • ve_i — the virtual eligible time of task i, which in the implementation is the task’s se.vruntime (its accumulated virtual runtime). It is the virtual-time instant at which the task becomes eligible to run.
  • r_i — the request size (the slice), in real nanoseconds: sysctl_sched_base_slice by default, or the task’s custom value.
  • w_i — the task’s weight from sched_prio_to_weight[] (1024 at nice 0). Dividing by weight converts the real request into a virtual-time span.

The division r_i / w_i is the same weight-normalization used for vruntime: it is performed by calc_delta_fair(), which for a nice-0 task (weight == NICE_0_LOAD) returns the request unchanged, and otherwise scales it. The function that maintains the deadline as a task runs is update_deadline() (fair.c v6.12):

static bool update_deadline(struct cfs_rq *cfs_rq, struct sched_entity *se)
{
    if ((s64)(se->vruntime - se->deadline) < 0)
        return false;                       /* not yet reached deadline */
 
    /* the virtual time slope is w_i (nice); the request r_i is the base slice */
    if (!se->custom_slice)
        se->slice = sysctl_sched_base_slice;
 
    /* EEVDF: vd_i = ve_i + r_i / w_i */
    se->deadline = se->vruntime + calc_delta_fair(se->slice, se);
 
    return true;                            /* request consumed -> reschedule */
}

Trace it: the function is called from update_curr() as the running task accumulates runtime. While se->vruntime is still behind se->deadline, it returns false — the task keeps running. The instant vruntime catches up to the deadline, the task has consumed its request r_i; the function resets the slice to the base (unless the task pinned a custom one), recomputes the next deadline vruntime + r_i/w_i, and returns true, which signals the caller to set need_resched and re-pick. So the deadline both triggers preemption and re-arms for the next request. Note that r_i/w_i means a heavier task (larger w_i) gets a nearer virtual deadline for the same real request — which, combined with its slower-advancing vruntime, is how weight delivers more CPU.

A freshly-placed or newly-woken task gets its initial deadline in place_entity(), with one nicety: a task joining mid-stream is started with half a slice (vslice /= 2 under PLACE_DEADLINE_INITIAL) because existing tasks are, on average, halfway through their own slices — easing the newcomer into the competition rather than letting it jump the queue with a full fresh request (fair.c v6.12).

Custom Per-Task Slices via sched_setattr

The headline new capability EEVDF exposes is that a task can request its own slice. The EEVDF documentation states it plainly: “tasks can request specific time slices using the new sched_setattr() system call, which further facilitates the job of latency-sensitive applications” (sched-eevdf). The mechanism is not a dedicated SCHED_FLAG_* bit — it reuses the existing sched_runtime field of struct sched_attr for SCHED_OTHER/SCHED_NORMAL tasks. The plumbing lives in __setscheduler_params() in kernel/sched/syscalls.c (syscalls.c v6.12):

} else if (fair_policy(policy)) {
    p->static_prio = NICE_TO_PRIO(attr->sched_nice);
    if (attr->sched_runtime) {
        p->se.custom_slice = 1;
        p->se.slice = clamp_t(u64, attr->sched_runtime,
                              NSEC_PER_MSEC / 10,    /* 0.1 ms  = HZ 1000 * 10 */
                              NSEC_PER_MSEC * 100);  /* 100 ms  = HZ 100  / 10 */
    } else {
        p->se.custom_slice = 0;
        p->se.slice = sysctl_sched_base_slice;       /* fall back to base */
    }
}

Reading this: for a fair policy, a non-zero sched_runtime sets custom_slice = 1 and stores the requested slice, clamped to [0.1 ms, 100 ms]. A zero sched_runtime clears the custom flag and reverts to the system base slice. The custom_slice flag is what update_deadline() and place_entity() check before overwriting the slice with the base — so a custom slice persists across requests instead of being reset every time the task consumes its quantum. The clamp bounds are deliberate: 0.1 ms is roughly one tick at HZ=1000 (below which switching overhead dominates), and 100 ms is a sanity ceiling so a single fair task cannot monopolize a CPU for an unbounded burst.

A realistic invocation in C:

#define _GNU_SOURCE
#include <sched.h>
#include <linux/sched/types.h>   /* struct sched_attr */
#include <unistd.h>
#include <sys/syscall.h>
 
int main(void) {
    struct sched_attr attr = {0};
    attr.size          = sizeof(attr);
    attr.sched_policy  = SCHED_OTHER;   /* stay a normal fair task */
    attr.sched_nice    = 0;             /* keep default weight/share */
    attr.sched_runtime = 200000;        /* request a 200 us slice -> low latency */
 
    /* glibc has no wrapper; call the syscall directly */
    if (syscall(SYS_sched_setattr, 0 /*self*/, &attr, 0 /*flags*/) == -1)
        return 1;
    /* now this thread wakes, runs ~200us, and yields the deadline early */
    return 0;
}

Line notes: attr.size must be set so the kernel can version-check the struct; sched_policy = SCHED_OTHER keeps the task in the fair class (a custom slice is meaningless for SCHED_FIFO/SCHED_RR, which do not use virtual deadlines); sched_nice = 0 leaves the long-run share at the default — only the latency changes. sched_runtime = 200000 (200 µs, under the default base) gives a nearer virtual deadline so the thread is dispatched faster after each wakeup. There is no glibc wrapper for sched_setattr, so the syscall() form is required (sched_setattr(2)). Reading the value back via sched_getattr() returns the stored slice in sched_runtime for a fair task whose custom_slice is set (syscalls.c v6.12).

Uncertain

Verify: that no SCHED_FLAG_* bit is involved in selecting a custom fair-task slice in 6.12/6.18 (i.e. the slice is taken purely from sched_runtime with sched_policy == SCHED_OTHER and no extra flag). Reason: the 6.12 __setscheduler_params() code path keys solely on attr->sched_runtime != 0 for a fair policy, with no slice-specific flag, but the sched_setattr(2) man page’s SCHED_FLAG_* list should be cross-checked to confirm none is documented as a slice hint. To resolve: read the current sched_setattr(2) man page flag table and grep kernel/sched/ for any slice-gating flag. uncertain

Latency vs Throughput — Walking the Trade-off

Suppose two equal-weight (nice 0) fair tasks, A and B, alternate on one CPU, and the base slice is 700 µs (6.18). Each runs ~700 µs, then the other gets ~700 µs — so a wakeup of A, if B is currently running, waits up to ~700 µs before A is dispatched. Now set A’s slice to 100 µs via sched_runtime. A’s virtual deadline becomes much nearer, so when A wakes it is picked almost immediately, and it preempts after only ~100 µs of its own run — A’s latency drops by ~7×. But A now incurs a context switch every 100 µs instead of every 700 µs: roughly 7× more switches, each costing the switch_mm overhead plus cold-cache refills. A’s throughput per CPU-second falls because more of its time is spent switching rather than computing. The long-run share of the CPU between A and B, however, is unchanged at 50/50 — that is governed by their equal weights, not their slices. This decoupling — share from weight, latency from slice — is precisely the property EEVDF was designed to provide and CFS could only approximate (LWN 925371, sched-eevdf).

Failure Modes and Common Misunderstandings

“Shrinking the slice gives my task more CPU.” It does not — it gives it the same share, delivered in smaller, more frequent installments with lower wakeup-to-dispatch latency. More CPU comes from more weight (lower nice), a different knob entirely (Nice Values Weights and Priority Scaling).

“Setting sched_runtime makes my task real-time / SCHED_DEADLINE.” No. With sched_policy = SCHED_OTHER, sched_runtime is a fair-class slice hint. The same field name means something completely different under SCHED_DEADLINE, where sched_runtime is the guaranteed runtime per period enforced by the Constant Bandwidth Server with admission control. Confusing the two is a classic error: a fair-task slice is a best-effort latency preference, not a reservation.

“Any tiny slice is honored.” The kernel clamps sched_runtime to [0.1 ms, 100 ms] for fair tasks. Requesting 10 µs silently becomes 100 µs; requesting 500 ms becomes 100 ms. Below the clamp floor, switching overhead would swamp useful work anyway.

“The base slice is per-task and tunable per-task in sysfs.” base_slice_ns is a single global default. Per-task customization is only through sched_setattr. Lowering the global base to chase latency raises the switch rate for every fair task on the system and can measurably cut throughput under load — change it system-wide only with benchmarking.

Forgetting attr.size. sched_setattr rejects the call with EINVAL (or misreads fields) if attr.size is not set to sizeof(struct sched_attr); this is the most common first-attempt failure when calling the raw syscall.

Alternatives and When to Choose Them

For latency, the EEVDF custom slice is the lightest-weight option: it stays in the fair class, needs no privilege beyond what sched_setattr requires for the policy, and does not risk starving other work. Reach past it only when its best-effort nature is insufficient:

  • If you need a task to always run before lower-priority work regardless of fairness, use RR — at the cost of starvation risk and RT throttling caveats.
  • If you can characterize the work as “needs runtime ns every period ns by deadline,” use SCHED_DEADLINE for an admission-controlled guarantee — a much stronger contract than a fair slice.
  • If the goal is isolation (no other task should ever interfere), combine a fair slice with affinity and CPU isolation.
  • For tuning the system-wide latency/throughput balance rather than one task, adjust the global base_slice_ns (and consider the preemption model).

Production Notes

The move from CFS’s sched_latency/min_granularity to EEVDF’s single base slice landed as a quiet but consequential change in 6.6 and is the backdrop for ongoing tuning in the LTS series; LWN’s coverage of the EEVDF merge (LWN 925371, LWN 969062) documents the rationale — Peter Zijlstra wanted a principled, latency-aware fair scheduler rather than CFS’s accreted heuristics. Operationally, the most common production use of custom slices is in latency-sensitive userspace runtimes and media pipelines that previously resorted to SCHED_FIFO (with its starvation hazard) merely to get prompt dispatch; a small fair-class slice often achieves the responsiveness goal without leaving the fair class. The retune of the default base slice between 6.12 (750 µs) and 6.18 (700 µs) is the kind of fast-moving fact that must be re-checked per release — do not assume a fixed number across kernels. For the eligibility test that gates which tasks compete for the earliest deadline (a task with a near deadline is still skipped if it is ineligible due to negative lag), see Eligibility Lag and Virtual Deadlines in EEVDF, which completes the EEVDF picture this note begins.

See Also