How does Linux's fair-share CPU scheduler (CFS, and EEVDF since kernel 6.6) turn a task's nice value into an actual share of CPU time?
answer
- shares, not strict priority levels
- every nice value maps to a number
- about 1.25 per step
- runtime scaled by weight, then ordered
- 6.6 changed the pick, not the share
basics
~20 sLinux maps each nice value to a weight, roughly 1.25x per step with nice 0 at 1024, and hands every runnable task CPU time in proportion to its weight. Each nice step therefore shifts about 10% of the contested CPU.
solid answer
~60 sLinux does not implement nice as a strict priority queue; it implements proportional share. Each nice value maps to a fixed weight from a kernel table — nice 0 is 1024, and each step changes the weight by a factor of about 1.25 — and a runnable task is entitled to `its weight / sum of all runnable weights` of the CPU. The bookkeeping is virtual runtime: real execution time scaled inversely by weight, so a low-weight task's virtual clock advances faster and it gets picked less often. CFS simply ran whichever task had the smallest virtual runtime. EEVDF, which replaced that pick logic in Linux 6.6, keeps the same weights and shares but adds an eligibility test and a virtual deadline per task, so a task that has taken less than its fair share can be served ahead of others — better latency behaviour, same proportional share. One consequence: there is no fixed time slice to quote; the slice falls out of the weights and the number of runnable tasks.
go deeper
Know that Linux shares CPU proportionally rather than running the top priority first, and that a nice step is worth roughly ten percent of contested CPU.
Explain the weight table with nice 0 at 1024, the ~1.25 ratio per step, and virtual runtime as real time scaled inversely by weight so lighter tasks fall behind in the ordering.
Demonstrate that shares are computed only over currently runnable tasks and dilute as load changes, and that nice therefore gives no protection guarantee for a latency-sensitive service.
Own the choice between weight-based hints and hard controls: when proportional share is adequate for mixed workloads, and when a platform must enforce ceilings instead of ratios.
## Proportional share, not priority levels A classic priority scheduler runs the highest-priority runnable task and only descends when nothing above is runnable — which starves the bottom. Linux's normal policy, `SCHED_OTHER`, is deliberately not that. It is a **weighted fair-share** scheduler: every runnable task gets CPU, and the nice value determines only what fraction. ## From nice to weight The kernel holds a fixed lookup table mapping each of the 40 nice values to a weight. Nice 0 has weight **1024**; each step towards -20 multiplies the weight by roughly 1.25, and each step towards 19 divides it by roughly 1.25. So nice -1 is about 1277 and nice 1 about 820. The entitlement of a runnable task is then: ``` share = weight(task) / sum of weight(t) for all runnable t on that CPU ``` The 1.25 ratio was chosen for a memorable property: with two competing CPU-bound tasks, a one-step nice difference moves about **10%** of the CPU from one to the other. Two tasks at nice 0 and nice 1 split roughly 55/45. Two tasks five steps apart split roughly 75/25, because 1.25^5 is about 3. ## Virtual runtime: the bookkeeping trick The scheduler cannot afford to recompute fractions constantly, so it keeps a per-task **virtual runtime** (vruntime). When a task runs for some real nanoseconds, its vruntime advances by that time scaled by `1024 / weight`: - A nice 0 task's virtual clock runs at real speed. - A favoured (low-nice, high-weight) task's virtual clock runs *slower* than real time, so it looks like it has consumed less and gets picked again sooner. - A de-prioritised task's virtual clock races ahead, so it falls behind in the ordering. CFS ("Completely Fair Scheduler", the default from 2.6.23 to 6.5) then had one rule: run the runnable task with the smallest vruntime, kept in a red-black tree keyed by vruntime. Fairness fell out of the arithmetic. ## What EEVDF changed in Linux 6.6 CFS controlled *how much* CPU each task got but had only indirect, heuristic control over *when* a waking task ran — the source of long-standing latency complaints and a pile of tunables. Linux 6.6 replaced the pick logic with **EEVDF** (Earliest Eligible Virtual Deadline First): - Each task accumulates **lag**: how far its actual service is from its fair-share entitlement. A task that has received less than its share has positive lag and is *eligible*. - Each task is assigned a virtual **deadline** derived from its allotted slice and its weight. - The scheduler runs the eligible task with the earliest virtual deadline. The weights and the resulting long-run shares are unchanged — nice still means exactly what it meant. What changed is that short, latency-sensitive tasks get served promptly without needing to be renice'd, and several CFS-era tuning knobs became irrelevant. The remaining scheduler tunables live under `/sys/kernel/debug/sched/` on kernels built with scheduler debugging, not in the general sysctl namespace. ## Why there is no fixed time slice Interviewers often ask "what is the Linux time slice?" expecting a number. There isn't one. The slice a task receives is computed from a target period, its weight, and the number of runnable tasks on that CPU, floored at a minimum granularity so that a hundred runnable tasks do not produce microsecond slices and a context-switch storm. Add more runnable tasks and every slice shrinks; give a task more weight and its slice grows. ## The practical consequences - **Nice never starves anything.** Even a nice 19 CPU hog keeps getting a small share, which is why nice is unsuitable for "this must never touch the CPU while the important thing runs". For that you need real-time policies or hard resource controls. - **Shares are relative to the competitors present.** A nice -5 task competing with one other task looks powerful; the same task competing with fifty gets its slice diluted like everyone else. - **Blocked tasks are not counted.** A task sleeping on I/O contributes no weight, so shares are computed over the *runnable* set, which changes constantly. - **Weights compose hierarchically.** When tasks are grouped by the scheduler, nice redistributes CPU among the tasks inside a group first, and the group's own weight decides how much the group gets overall — which is why a nice value can look ineffective across a boundary. ## What an interviewer is checking That you can say "proportional share by weight, tracked as virtual runtime" rather than "higher priority runs first", that you know a nice step is worth roughly 10% of contested CPU rather than an absolute guarantee, and ideally that you know the pick algorithm became EEVDF in 6.6 without the nice semantics changing.
- Why can a nice 19 CPU-bound task never be starved completely on Linux?Because SCHED_OTHER is proportional-share, not strict priority. A nice 19 task still carries a non-zero weight, so its entitlement is small but positive, and its virtual runtime keeps advancing until it becomes the best candidate. Starvation is only possible under the real-time policies, where a higher static priority genuinely excludes everything below it.
- An engineer asks what Linux's default time slice is. What is the honest answer?There is no fixed slice. The scheduler derives one from a target period, the task's weight, and how many tasks are runnable on that CPU, subject to a minimum granularity so slices do not degenerate into context-switch thrash. More runnable tasks means shorter slices for everyone; more weight means a longer slice.
- Two CPU-bound tasks share one CPU at nice 0 and nice 5. Roughly how does the CPU split, and why?About 75/25 in favour of the nice 0 task. Each nice step scales the weight by roughly 1.25, so five steps is a factor of about three: weights of roughly 1024 against 335, and the share is each weight over their sum. It is a ratio, not a guarantee, and it only holds while both remain runnable.
saying these in an interview costs you the question
- Says the highest-priority runnable task always runs first
- Quotes a fixed millisecond time slice for SCHED_OTHER
- Thinks nice 19 tasks can be starved to zero CPU
- Believes EEVDF changed what nice values mean
- Treats a nice share as a guaranteed percentage of the machine