ยง9.4โ€“9.8An Example โ€ฆ Summary

Part I OSTEP pp. 93โ€“100 ยท ~9 min read

  • stride scheduling
  • cfs
  • vruntime
  • nice
  • red-black tree

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 U=TfirstTsecondU = \frac{T_{first}}{T_{second}} โ€” the first finisherโ€™s completion time over the secondโ€™s. Perfect fairness is U=1U = 1.

1.00.80.60.40.20.01101001000Job Length (log scale)Unfairnessshort jobs: U โ‰ˆ 0.5 โ€” quite unfairlong jobs: U โ†’ 1 โ€” fairness emerges

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 , 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:

Figure 9.3: the stride trace โ€” click any row for what the scheduler was thinking
Pass(A) (stride 100)Pass(B) (stride 200)Pass(C) (stride 40)Who runs?
1000A
210000B
31002000C
410020040C
510020080C
6100200120A
7200200120C
8200200160C
9200200200โ€ฆ
Dotted-underlined cells have explanations โ€” click one.
Figure 9.3, running: stride scheduling with A:100, B:50, C:250 tickets (quantum 10)
AarrruntixBarrruntixCarrruntix
CPUA: 0โ€“10AB: 10โ€“20BC: 20โ€“50CA: 50โ€“60AC: 60โ€“80C020406080
jobarrivalruntimeturnaroundresponse
A020600
B0102010
C0508020
average53.3310.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 โ€” 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 ; 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.)

Figure 9.4, running: CFS with four equal jobs (sched_latency 48, min_granularity 6)
AarrrunBarrrunCarrrunDarrrun
CPUA: 0โ€“12AB: 12โ€“24BC: 24โ€“36CD: 36โ€“48DA: 48โ€“60AB: 60โ€“72BC: 72โ€“84CD: 84โ€“96DA: 96โ€“120AB: 120โ€“144BA: 144โ€“168AB: 168โ€“192B0306090120150180
jobarrivalruntimeturnaroundresponse
A0721680
B07219212
C0248424
D0249636
average135.0018.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 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

time_slicek=weightkโˆ‘i=0nโˆ’1weightiโ‹…sched_latency(9.1)\text{time\_slice}_k = \frac{weight_k}{\sum_{i=0}^{n-1} weight_i} \cdot \text{sched\_latency} \tag{9.1}

and vruntime accrues inversely to weight:

vruntimei=vruntimei+weight0weightiโ‹…runtimei(9.2)vruntime_i = vruntime_i + \frac{weight_0}{weight_i} \cdot runtime_i \tag{9.2}
CFS weighting: A at nice โˆ’5 vs B at nice 0
CPUA: 0โ€“36AB: 36โ€“48BA: 48โ€“84AB: 84โ€“96B020406080
jobarrivalruntimeturnaroundresponse
A072840
B0249636
average90.0018.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 ordered by vruntime โ€” insert, delete, and find-min in O(log n):

14918110172252124next to run =leftmost node

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?

5 questions