Lottery is delightfully simple โ but random. This half of the chapter asks how unfair randomness gets, then meets the deterministic alternatives: stride scheduling and the fair-share scheduler youโre probably running right now.
9.4 An Example (How Unfair Is Random?)
Two jobs, identical tickets (100 each), identical run time R. Ideally they finish together; randomness says otherwise. Define unfairness โ the first finisherโs completion time over the secondโs. Perfect fairness is .
Figure 9.2 (sketch of the bookโs simulator study, 30 trials per point): with a job length of 1, one job always finishes at half the otherโs time (U = 0.5); only over many slices does the lottery approach its promised proportions.
9.5 How To Assign Tickets?
An open problem the chapter is honest about: system behavior depends strongly on allocation, and โlet the users decideโ is a non-solution โ it doesnโt tell anyone what to do. Hold that thought; it returns in the summary.
9.6 Why Not Deterministic? (Stride Scheduling)
If short-run randomness bothers you, Waldspurger has the cure: stride scheduling stride scheduling Deterministic fair share โ stride = bignum/tickets; run the job with the lowest pass value, then add its stride to its pass. defined in ch. 9 โ open in glossary , deterministic fair share. Each job gets a stride โ a big number divided by its tickets (10,000 โ A:100, B:200, C:40) โ and a pass counter. The rule:
curr = remove_min(queue); // pick client with min pass
schedule(curr); // run for quantum
curr->pass += curr->stride; // update pass using stride
insert(queue, curr); // return curr to queue
Low pass runs; running costs you your stride; big ticket-holders pay tiny strides and therefore run often. Trace it:
| Pass(A) (stride 100) | Pass(B) (stride 200) | Pass(C) (stride 40) | Who runs? | |
|---|---|---|---|---|
| 1 | 0 | 0 | 0 | A |
| 2 | 100 | 0 | 0 | B |
| 3 | 100 | 200 | 0 | C |
| 4 | 100 | 200 | 40 | C |
| 5 | 100 | 200 | 80 | C |
| 6 | 100 | 200 | 120 | A |
| 7 | 200 | 200 | 120 | C |
| 8 | 200 | 200 | 160 | C |
| 9 | 200 | 200 | 200 | โฆ |
| job | arrival | runtime | turnaround | response |
|---|---|---|---|---|
| A | 0 | 20 | 60 | 0 |
| B | 0 | 10 | 20 | 10 |
| C | 0 | 50 | 80 | 20 |
| average | 53.33 | 10.00 | ||
Strides (10000 รท tickets): A=100, B=200, C=40. Watch the exact book sequence โ A, B, C, C, C, A, C, C: per cycle C runs 5ร, A 2ร, B 1ร โ precisely 250:100:50. Flip to Lottery with the same tickets and re-run: same proportions, but only on average.
Exact proportions at the end of every cycle โ so why would anyone still use lottery? Global state. Suppose a new job arrives mid-cycle: what pass value should it get? Zero would let it monopolize the CPU while it โcatches up.โ Lottery needs no per-process bookkeeping at all โ add the job, bump the global ticket count, done. New processes fold in gracefully.
9.7 The Linux Completely Fair Scheduler (CFS)
The fair-share scheduler in actual deployment on billions of machines is Linuxโs Completely Fair Scheduler cfs Linux's Completely Fair Scheduler โ pick the lowest vruntime; dynamic slices from sched_latency/min_granularity; nice-based weights; red-black tree of runnables. defined in ch. 9 โ open in glossary โ same goals, different machinery, ruthlessly focused on efficiency (scheduling burns ~5% of datacenter CPU even after aggressive optimization; Google measured it).
Basic operation. No fixed time slice. Each running process
accumulates vruntime vruntime Virtual runtime a process accrues while running (scaled inversely by its weight); CFS always runs the minimum.
defined in ch. 9 โ open in glossary
; on a decision, CFS runs
the process with the lowest vruntime. Switching often maximizes
fairness but costs performance; the tension is managed by two
parameters: sched_latency (~48 ms) โ divide by the number of
runnables n for the per-process slice โ and min_granularity
(~6 ms), a floor so that huge n canโt shrink slices into switch-storm
territory. (A 1 ms periodic timer quantizes decisions; vruntime is
tracked precisely, so imperfect multiples come out right over time.)
| job | arrival | runtime | turnaround | response |
|---|---|---|---|---|
| A | 0 | 72 | 168 | 0 |
| B | 0 | 72 | 192 | 12 |
| C | 0 | 24 | 84 | 24 |
| D | 0 | 24 | 96 | 36 |
| average | 135.00 | 18.00 | ||
Four runnables โ slice = 48/4 = 12 ms: A B C D, A B C D. After t=96, C and D are done; two runnables โ slice grows to 24 ms, and A/B alternate in long turns. The slice is DYNAMIC โ the policy has no fixed quantum at all.
Weighting (niceness). Priority comes not from tickets but from the classic UNIX nice nice Classic UNIX priority knob, โ20โฆ+19 (default 0); CFS maps it to weights (1024 at 0) โ positive = nicer = less CPU. defined in ch. 9 โ open in glossary level, โ20โฆ+19 (default 0; positive = nicer = less CPU). CFS maps nice to weights:
static const int prio_to_weight[40] = {
/* -20 */ 88761, 71755, 56483, 46273, 36291,
/* -15 */ 29154, 23254, 18705, 14949, 11916,
/* -10 */ 9548, 7620, 6100, 4904, 3906,
/* -5 */ 3121, 2501, 1991, 1586, 1277,
/* 0 */ 1024, 820, 655, 526, 423,
/* 5 */ 335, 272, 215, 172, 137,
/* 10 */ 110, 87, 70, 56, 45,
/* 15 */ 36, 29, 23, 18, 15,
};
The slice generalizes to
and vruntime accrues inversely to weight:
| job | arrival | runtime | turnaround | response |
|---|---|---|---|---|
| A | 0 | 72 | 84 | 0 |
| B | 0 | 24 | 96 | 36 |
| average | 90.00 | 18.00 | ||
weight(โ5)=3121, weight(0)=1024 โ A's slice = 3121/4145 ยท 48 โ 36 ms, B's โ 12 ms โ and A's vruntime accrues at roughly โ B's rate, so it keeps earning those long slices. Equation 9.1, live.
One elegant table property: only nice differences matter โ A at โ5 vs B at 0 schedules identically to A at 5 vs B at 10 (run the math and see).
Red-black trees. With thousands of processes, scanning a list every few milliseconds wastes cycles. CFS keeps runnable processes in a red-black tree red-black tree A balanced binary tree keeping operations O(log n); CFS stores runnable processes in one, ordered by vruntime. defined in ch. 9 โ open in glossary ordered by vruntime โ insert, delete, and find-min in O(log n):
Figure 9.5: ten jobsโ vruntimes in a red-black (balanced) tree. Only runnable processes live here; sleepers are tracked elsewhere.
Sleepers. A wrinkle: if B sleeps 10 seconds while A runs, B wakes 10 seconds behind in vruntime โ and would monopolize the CPU while catching up, starving A. CFSโs fix: on wake, set the jobโs vruntime to the treeโs minimum. Starvation avoided โ at a cost: processes that sleep briefly but often never accumulate credit, and can fall short of their fair share.
Tip: Use efficient data structures when appropriate
Sometimes a list will do. On a modern server with thousands of active processes, searching a list on every core every few milliseconds will not. Knowing which structure fits which access pattern โ and how often itโs exercised โ is a hallmark of good engineering; CFSโs red-black tree is the canonical example in schedulerland.9.8 Summary
Three fair-share schedulers: lottery (randomness โ proportions in expectation, zero global state), stride (deterministic exactness, awkward newcomers), and CFS (a bit like weighted round-robin with dynamic time slices, built to scale โ the most widely used fair-share scheduler in existence). Caveats for the family: I/O-heavy jobs can get shorted, and ticket/nice assignment remains unsolved โ which is why general-purpose systems often lean on MLFQ-like schedulers instead. But where shares are the point โ say, dividing a virtualized datacenterโs CPU one-quarter to the Windows VM, three-quarters to Linux โ proportional share is simple and effective, and the idea extends to memory and beyond.
Homework: lottery.py
Run the lottery yourself: three jobs across seeds 1โ3; a 1-vs-100 ticket imbalance (will the poor job EVER run first?); two 100-ticket jobs of length 100 across seeds to measure unfairness U; then vary the quantum and try to reproduce Figure 9.2โs curve โ and ask how it would look under stride. Get it at ostep-homework.Check yourself
1.Figure 9.2 plots unfairness U against job length. What is its message?
2.In stride scheduling (A stride 100, B 200, C 40), why does C run five times per cycle?
3.Stride gives exact proportions; lottery only approximate. Why does lottery still win in one important scenario?
4.CFS has no fixed time slice. How does it decide how long a process runs?
5.Process B wakes after sleeping 10 seconds, its vruntime far below everyone else's. What does CFS do, and why?