We closed the previous lesson with an open question: there are ten processes in state R and four cores — which one gets the CPU, and for how long? That decision is taken on meteo-01 several thousand times per second, and it determines whether a query to meteo-api answers in 8 ms or in 400 ms, whether ingestor loses readings, and whether the aggregator finishes its hourly averages before the next hour starts.

In this lesson you are going to understand why scheduling is necessary, which criteria conflict with each other (because they cannot all be optimized at once), how the classic algorithms work — computing them yourself on paper, which is the only way to really understand them — and what the Linux scheduler actually does when you run renice. By the end you will be able to decide, with sound reasoning, what priority each Meteora process deserves and why.

Contents

  1. Why scheduling is necessary: the burst cycle
  2. CPU-bound and I/O-bound processes
  3. The three schedulers and the dispatcher
  4. Evaluation criteria and their trade-offs
  5. Preemptive versus cooperative scheduling
  6. FCFS: first come, first served
  7. SJF and SRTF: shortest job first
  8. Priority scheduling, starvation and aging
  9. Round Robin and the effect of the quantum
  10. Multilevel queues and feedback queues
  11. The real Linux scheduler: CFS, EEVDF and nice
  12. Real-time policies: SCHED_FIFO, SCHED_RR, SCHED_DEADLINE
  13. Multiprocessor: affinity and load balancing

Why scheduling is necessary: the burst cycle

No process uses the CPU continuously. If you watch what ingestor does, you will see a repeating pattern:

wait for data ── compute ── write ── wait for data ── compute ── write ──
    (I/O)        (CPU)     (I/O)        (I/O)         (CPU)     (I/O)
    40 ms       0.3 ms    0.8 ms        40 ms        0.3 ms    0.8 ms

Each stretch of computation is a CPU burst and each wait is an I/O burst. Every process alternates between the two until it finishes. This observation, which looks trivial, is the basis of all scheduling:

  • If a process only used the CPU, it would be enough to run it to completion. There would be nothing to schedule.
  • Because it constantly blocks waiting for I/O, it leaves the CPU free, and it would be a waste not to give it to somebody else in the meantime.

The distribution of CPU bursts in real systems is very characteristic: a great many short bursts and very few long ones. On a typical workload, 70-80% of bursts last less than 8 ms. This has an enormous consequence that we will see when we talk about Round Robin: if you choose the quantum well, most processes finish their burst before exhausting it and block by themselves, without the CPU having to be taken away from them.

CPU-bound and I/O-bound processes

Processes are classified by which type of burst dominates:

I/O-bound CPU-bound
CPU bursts Many and very short Few and long
Dominant time Waiting for devices Computing
Example in Meteora ingestor, meteo-api aggregator
Context switches Many voluntary ones Many involuntary ones
What it needs Low response latency Long uninterrupted CPU time
If it is delayed Data is lost or the answer is late It just finishes later

This classification is not academic: it is what dictates the operational decision. Let us go back to the real data we saw in 02-01:

$ for p in 1842 1877 1901; do
    echo -n "PID $p: "
    grep -E 'ctxt_switches' /proc/$p/status | tr '\n' ' '
    echo
  done
PID 1842: voluntary_ctxt_switches: 94013 nonvoluntary_ctxt_switches: 118
PID 1877: voluntary_ctxt_switches: 18422 nonvoluntary_ctxt_switches: 291
PID 1901: voluntary_ctxt_switches: 210884 nonvoluntary_ctxt_switches: 44

The reading is direct:

  • ingestor (1842): 94,013 voluntary blocks against 118 preemptions. A ratio of 796 to 1. Strongly I/O-bound: it is nearly always waiting for packets from the stations.
  • meteo-api (1901): a ratio of 4,792 to 1. Even more extreme. It waits for HTTP requests.
  • aggregator (1877): a ratio of 63 to 1. There is still I/O (it reads files from /var/lib/meteora/readings/), but it spends far more time computing.

A good scheduler favors I/O-bound processes. It may sound counterintuitive — why prioritize the one that uses the least CPU? — but the logic is solid: if you give ingestor the CPU as soon as data arrives, it uses it for 0.3 ms and blocks again, releasing it almost immediately. The cost of serving it promptly is minimal and the benefit is that the network device goes back to being busy. If instead you give the CPU to the aggregator, it holds on to it for entire milliseconds while ingestor piles up unprocessed readings and the network buffers fill up.

The general rule: prioritizing the short keeps every resource busy in parallel.

The three schedulers and the dispatcher

A complete operating system does not take one scheduling decision but three, on very different time scales:

Scheduler What it decides Frequency Does Linux have it?
Long-term (admission) Which jobs enter the system Seconds or minutes Not as such
Medium-term (swapping) Which processes get evicted to swap Seconds Yes, tied to memory
Short-term (CPU) Which ready process gets the CPU Milliseconds Yes, it is the scheduler
  • The long-term one comes from the batch systems of the 1960s (we saw them in 01-02): with extremely scarce memory, you had to decide how many jobs to admit at once. On a modern Linux it does not exist: anyone can launch a process until the resources run out. The closest things are cgroup limits (module 6) and queueing systems like Slurm in supercomputing.
  • The medium-term one does exist: when memory runs short, the kernel evicts pages of inactive processes to swap. We will see it in Virtual Memory and Paging.
  • The short-term one is what this lesson is about. It acts every few milliseconds, and that is why its own code has to be extremely fast: if it took 1 ms to decide, it would eat 25% of a 4 ms quantum.

The dispatcher is the component that carries out the decision: it performs the context switch we studied in 02-01, switches to user mode and jumps to the instruction where the chosen process left off. Its working time is called dispatch latency, and on Linux it is on the order of 1-3 µs.

stateDiagram-v2
    direction LR
    [*] --> ReadyQueue: new process
    ReadyQueue --> CPU: short-term scheduler picks it
    CPU --> ReadyQueue: end of quantum
    CPU --> WaitQueue: I/O request
    WaitQueue --> ReadyQueue: I/O completed
    ReadyQueue --> Suspended: medium-term scheduler (swap out)
    Suspended --> ReadyQueue: swap in
    CPU --> [*]: exit()

Evaluation criteria and their trade-offs

To compare algorithms you need metrics. These are the five classic ones:

Criterion Definition Goal Who cares most
CPU utilization % of time the CPU is not idle Maximize The administrator
Throughput Processes completed per unit of time Maximize Batch systems
Turnaround time From arrival until completion Minimize The end user
Waiting time Total time in the ready queue Minimize The metric used to compare algorithms
Response time From arrival until the first reaction Minimize Interactive systems

The relationships between them:

Turnaround time = Waiting time + CPU time + I/O time
Waiting time    = Turnaround time − Service time

Waiting time is the metric used to compare algorithms because it is the only part the scheduler can influence: CPU time and I/O time are fixed by the job itself, not by the scheduling.

And here is the important bit: these criteria contradict each other. There is no algorithm that optimizes all of them.

Conflict Explanation
Throughput ↔ Response time Switching processes often improves response but burns CPU on context switches
Mean ↔ Variance An algorithm can produce a good average while being terrible for a few specific processes
Fairness ↔ Efficiency Sharing equally penalizes whoever needs it most
Priority ↔ Starvation Favoring some condemns others

At Meteora this is a business decision, not a technical one: if you optimize total throughput, the aggregator finishes sooner but meteo-api answers with latency spikes. If you optimize response time, the API runs smoothly and the aggregator takes longer to close out the hour. You have to choose, and choosing requires knowing what hurts most when it fails.

Preemptive versus cooperative scheduling

We already introduced both in 01-03. Now we can pin down when the scheduler is allowed to act. There are four moments:

  1. The process goes from Running to Blocked (it requests I/O).
  2. The process goes from Running to Ready (the quantum expires, an interrupt arrives).
  3. The process goes from Blocked to Ready (its I/O completes).
  4. The process terminates.

If the scheduler acts only in cases 1 and 4 — when the process gives up the CPU of its own accord — it is cooperative (or non-preemptive). If it also acts in 2 and 3, it is preemptive.

Cooperative Preemptive
Who releases the CPU The process itself The kernel can take it away
Hardware required Nothing special A programmable timer
An infinite loop Hangs the system Does not affect the others
Cost Minimal Frequent context switches
Shared data Safe by construction Needs synchronization
Systems Windows 3.1, Mac OS 9, coroutines Every modern OS

Preemption is what makes it possible for an aggregator with a badly written loop not to block the whole of meteo-01. The price to pay is that data shared between processes can be left in inconsistent states if they are interrupted halfway through, and that price is exactly what we will study in module 3.

FCFS: first come, first served

The simplest one: a FIFO queue. First to arrive, first to run, and nobody else gets in until it finishes its burst. It is non-preemptive.

Let us work with this set of processes, which we will reuse across all the algorithms:

Process Arrival CPU burst
P1 (aggregator) 0 24 ms
P2 (ingestor) 1 3 ms
P3 (meteo-api) 2 3 ms

Gantt chart with FCFS:

 P1                                          P2       P3
├────────────────────────────────────────────┼────────┼────────┤
0                                           24       27       30

Step-by-step calculation:

Process Arrival Start End Turnaround (End−Arrival) Waiting (Turnaround−Burst)
P1 0 0 24 24 0
P2 1 24 27 26 23
P3 2 27 30 28 25
Average waiting time = (0 + 23 + 25) / 3 = 16.00 ms
Average turnaround time = (24 + 26 + 28) / 3 = 26.00 ms

Now let us reverse the arrival order: P2 and P3 arrive first, and P1 last.

 P2      P3       P1
├───────┼────────┼──────────────────────────────────────────────┤
0       3        6                                             30
Process Waiting
P2 0
P3 2
P1 4
Average waiting time = (0 + 2 + 4) / 3 = 2.00 ms

From 16 ms to 2 ms without changing the algorithm, only the arrival order. That extreme sensitivity is the great flaw of FCFS.

The convoy effect

The phenomenon has a name: the convoy effect. A long CPU-bound process occupies the processor, and behind it a queue of short I/O-bound processes piles up, each of which would only need a few microseconds. While they wait, the I/O devices sit idle, because the processes that would use them are in the queue.

In Meteora terms: if the aggregator starts its 24 ms hourly computation and 40 requests to meteo-api of 0.5 ms each queue up behind it, the last one waits 24 ms to do half a millisecond of work. And during those 24 ms, the network card has nothing to send.

The mental image is exact: a slow truck on a single-lane road with 40 cars behind it.

SJF and SRTF: shortest job first

If the problem is that long jobs block short ones, the obvious solution is to run the shortest one first. That is SJF (Shortest Job First).

With our processes, assuming they all arrived at t=0:

 P2      P3       P1
├───────┼────────┼──────────────────────────────────────────────┤
0       3        6                                             30

Average waiting time: 2.00 ms. And this is no coincidence:

SJF is provably optimal: no algorithm can produce a lower average waiting time for a given set of processes.

The intuition behind the proof: if a long process goes before a short one, swapping them reduces the short one's wait by a lot and increases the long one's by a little. Repeating the swap leads to ascending order.

SRTF: the preemptive version

SRTF (Shortest Remaining Time First) adds preemption: if a process arrives whose burst is shorter than what remains of the current one, the CPU is taken away.

An example with staggered arrivals:

Process Arrival Burst
A 0 8
B 1 4
C 2 9
D 3 5

Instant-by-instant simulation:

  • t=0: only A. A runs (8 remaining).
  • t=1: B arrives with 4 < A's 7 remaining. Preemption: B takes over.
  • t=2: C arrives with 9 > B's 3 remaining. B continues.
  • t=3: D arrives with 5 > B's 2 remaining. B continues.
  • t=5: B finishes. Candidates: A (7), C (9), D (5). D takes over.
  • t=10: D finishes. Candidates: A (7), C (9). A takes over.
  • t=17: A finishes. C takes over.
  • t=26: C finishes.
 A   B        D        A            C
├──┼────────┼────────┼────────────┼─────────────────┤
0  1        5       10           17                26
Process Arrival End Turnaround Burst Waiting
A 0 17 17 8 9
B 1 5 4 4 0
C 2 26 24 9 15
D 3 10 7 5 2
Average waiting time = (9 + 0 + 15 + 2) / 4 = 6.50 ms

Compared with FCFS on the same set (which would give 7.75 ms) it is better, and SRTF is optimal among the preemptive algorithms.

The insurmountable problem with SJF

It requires knowing the length of the next burst, which is the future. There is no way to know it.

The practical solution is to estimate it from history, with an exponential average:

τ(n+1) = α · t(n) + (1 − α) · τ(n)

where t(n) is the real length of the last burst, τ(n) the previous estimate and α a weight between 0 and 1 (typically 0.5).

A numerical example with α = 0.5 and an initial estimate of τ = 10:

Real burst t Calculation New estimate τ
6 0.5·6 + 0.5·10 8.0
4 0.5·4 + 0.5·8 6.0
6 0.5·6 + 0.5·6 6.0
4 0.5·4 + 0.5·6 5.0
13 0.5·13 + 0.5·5 9.0

Notice the behavior: the estimate converges smoothly and reacts to the abrupt change in the last burst without overshooting completely. With α = 1 only the last burst would count (very reactive, very unstable); with α = 0 it would never change.

Second problem with SJF: starvation. A long process may never run if short processes keep arriving. On a system where meteo-api receives constant requests, the aggregator might never get started at all.

Priority scheduling, starvation and aging

Each process is assigned a priority number and the highest-priority one is chosen. SJF is really a special case: the priority is the inverse of the burst length.

With this set (lower number = higher priority, the usual convention):

Process Burst Priority
P1 10 3
P2 1 1
P3 2 4
P4 1 5
P5 5 2
 P2  P5        P1              P3     P4
├──┼─────────┼──────────────┼──────┼───┤
0  1         6             16     18  19
Process Waiting
P2 0
P5 1
P1 6
P3 16
P4 18
Average waiting time = (0 + 1 + 6 + 16 + 18) / 5 = 8.20 ms

Priorities can be assigned on internal criteria (memory size, I/O ratio, time consumed) or external ones (importance of the user, money paid, criticality of the service).

Starvation and aging

The problem is the same as with SJF: a low-priority process can wait indefinitely. The classic anecdote — perhaps apocryphal, but very illustrative — has it that when MIT shut down its IBM 7094 in 1973, they found a low-priority job submitted in 1967 that had never run.

The solution is aging: gradually increasing the priority of processes that have been waiting a long time.

aggregator's initial priority: 15 (low)
Rule: +1 priority for every second spent waiting

t=0 s:   priority 15 — does not run
t=5 s:   priority 10 — still does not run
t=10 s:  priority  5 — starts to compete
t=14 s:  priority  1 — is guaranteed to run

With this rule, no process waits more than 14 seconds however low its priority. Aging turns an impossible guarantee ("everybody runs") into a bounded one ("nobody waits more than X"), which is the kind of guarantee you can actually work with.

Round Robin and the effect of the quantum

Round Robin is FCFS with time-based preemption: each process receives a fixed quantum; if it does not finish, it goes back to the end of the queue.

With our three original processes (P1=24, P2=3, P3=3) and a quantum of 4 ms:

 P1    P2   P3    P1    P1    P1    P1    P1
├────┼───┼───┼────┼────┼────┼────┼────┤
0    4   7  10   14   18   22   26   30
Process End Turnaround Burst Waiting
P1 30 30 24 6
P2 7 6 3 3
P3 10 8 3 5
Average waiting time = (6 + 3 + 5) / 3 = 4.67 ms

Worse than SJF (2.00 ms), but far better than FCFS (16.00 ms). And, above all, with a property the others do not have: response time is bounded. With n processes and quantum q, none waits more than (n−1)·q for its first turn. With 10 processes and q=4 ms, the worst wait is 36 ms. That is a useful guarantee for an interactive system.

The effect of the quantum

Choosing q is a direct trade-off:

Quantum Behavior Switching overhead Resembles
Very large (∞) Each process runs until it blocks None FCFS
Large (100 ms) Slow response Low Smoothed FCFS
Medium (4-10 ms) Balanced Acceptable Useful Round Robin
Small (1 ms) Very reactive Noticeable
Tiny (10 µs) The CPU devotes itself to switching processes Ruinous Nothing useful

With the 5 µs context switch cost we calculated in 02-01:

Quantum Overhead Useful work
100 µs 5/105 = 4.8% 95.2%
1 ms 5/1005 = 0.50% 99.5%
4 ms 5/4005 = 0.12% 99.88%
100 ms 5/100005 = 0.005% 99.995%

The practical rule: the quantum must be large enough that 80% of CPU bursts finish within it. Since most bursts last less than 8 ms, a quantum of that magnitude makes most processes block by themselves and turns preemption into the exception, not the norm.

An important nuance: turnaround time does not improve monotonically as you reduce the quantum. It can get worse, because a process that would need 7 uninterrupted ms finishes in one go with q=7, whereas with q=2 it needs four turns spread out over time.

Multilevel queues and feedback queues

The algorithms above treat every process the same. In reality they are not: an interactive process and a batch process need different policies.

In multilevel queues, the ready queue is split into several queues, each with its own algorithm:

Priority 0 (highest) │ System processes           │ Round Robin q=1ms
Priority 1           │ Interactive processes      │ Round Robin q=4ms
Priority 2           │ Editing processes          │ Round Robin q=8ms
Priority 3 (lowest)  │ Batch processes            │ FCFS

Between queues, scheduling is normally by absolute priority: nothing in queue 2 runs if there is anything in queue 0 or 1. This is fast but causes starvation. The alternative is to hand out percentages of CPU: 60% to queue 0, 25% to queue 1, 10% to queue 2 and 5% to queue 3.

The problem with pure multilevel queues: a process is pinned to its queue forever, and its nature can change. The aggregator is CPU-bound while it computes, but I/O-bound while it reads files.

Multilevel feedback queues solve that by letting processes move up and down between queues according to their observed behavior:

flowchart TD
    N[New process] --> Q0
    Q0["Queue 0 — RR, quantum 8 ms"] -->|uses up its quantum| Q1
    Q0 -->|blocks on I/O| B0[Leaves to wait]
    Q1["Queue 1 — RR, quantum 16 ms"] -->|uses up its quantum| Q2
    Q1 -->|blocks on I/O| B1[Leaves to wait and moves up to Queue 0]
    Q2["Queue 2 — FCFS"] -->|aging| Q1
    B0 --> Q0
    B1 --> Q0

The logic is elegant:

  • Every process starts at the top, with the highest priority and the shortest quantum.
  • If it uses up its quantum without blocking, that is a sign it is CPU-bound: it moves down one queue, where it will have lower priority but a longer quantum (which is what suits it).
  • If it blocks on I/O before using the quantum up, it is interactive: it stays put or moves up.
  • Aging promotes processes that have been waiting a long time at the bottom, avoiding starvation.

This scheme deduces from behavior what it cannot know in advance: it approximates SJF without having to predict the future. It was the design of the traditional UNIX schedulers, of Windows NT, and of the Linux O(1) scheduler up to 2007.

The real Linux scheduler: CFS, EEVDF and nice

Linux abandoned the feedback-queue approach in version 2.6.23 (2007) in favor of CFS (Completely Fair Scheduler), and since 6.6 (2023) in favor of EEVDF (Earliest Eligible Virtual Deadline First), which refines the same idea.

CFS's starting point is different and very powerful: instead of fixed quanta and queues, it models how much CPU each process would be entitled to on an ideal machine and corrects the deviation.

  • If there are n runnable processes, each should get 1/n of the CPU.
  • CFS keeps track of each process's virtual runtime (vruntime): the CPU time it has consumed, weighted by its priority.
  • It always picks the process with the lowest vruntime, that is, the one furthest behind its fair share.
  • Processes are stored in a red-black tree ordered by vruntime, so picking the next one is taking the leftmost node: O(1) in practice, O(log n) on insertion.

The elegant consequence: a process that blocks on I/O does not accumulate vruntime while it waits, so when it wakes up it has the lowest vruntime of all and runs immediately. Interactive processes get good response with no special heuristic at all: it falls out of the model for free.

nice and weighting

The nice value ranges from −20 (highest priority) to +19 (lowest), with 0 as the default. The name comes from "how nice you are to the others": a high nice means you give way.

In CFS, nice does not grant extra turns: it changes the rate at which vruntime advances.

vruntime += real_time_executed × (weight_of_nice_0 / weight_of_the_process)

Each unit of nice changes the weight by a factor of roughly 1.25, which translates into a rule that is very easy to remember:

Each point of nice changes the CPU share by around 10%.

nice Internal weight Share against another process at nice 0
−20 88,761 98.8%
−10 9,548 90.3%
−5 3,121 75.3%
0 1,024 50%
+5 335 24.7%
+10 110 9.7%
+19 15 1.4%

Applied to Meteora:

$ ps -eo pid,ni,pri,comm -u meteora
    PID  NI PRI COMMAND
   1842  -5  24 ingestor
   1877   5  14 aggregator
   1901   0  19 meteo-api

$ sudo renice -n 10 -p 1877
1877 (process ID) old priority 5, new priority 10

$ nice -n 15 /opt/meteora/bin/historical-reprocess --from 2026-01-01

What each command does:

  • renice -n 10 -p 1877 lowers the aggregator to nice 10. Now, competing with meteo-api (nice 0), it will receive roughly 9.7% of the CPU when both want to run. When meteo-api is blocked waiting for requests — which is nearly always — the aggregator will use 100%. This is the key point many people miss: nice does not cap consumption, it only decides who gives way in a conflict. To really cap it you need cgroups (06-02).
  • nice -n 15 ... launches the historical reprocess at minimum priority. It is the right command for heavy jobs that must not get in the way: it will consume all the spare CPU and step aside instantly as soon as a request arrives.
  • Lowering nice (negative values) requires root privileges, because otherwise any user could monopolize the system.

An important warning about nice that almost always catches people off guard: it only affects the CPU. If the aggregator saturates the disk reading /var/lib/meteora/readings/, renice will fix nothing. That is what ionice is for, and we will see it in Storage Management.

Real-time policies: SCHED_FIFO, SCHED_RR, SCHED_DEADLINE

Linux implements several scheduling policies that coexist. nice applies only to the first one:

Policy Type Priority Behavior
SCHED_OTHER Normal nice −20..+19 CFS/EEVDF, fair sharing
SCHED_BATCH Normal nice Like OTHER but assumed non-interactive
SCHED_IDLE Normal Absolute minimum Only with a completely idle CPU
SCHED_FIFO Real time 1..99 Runs until it blocks or yields. No quantum
SCHED_RR Real time 1..99 Like FIFO but with a quantum among equals
SCHED_DEADLINE Real time Above everything You declare deadline, period and computation time

Golden rule: any real-time process displaces every normal one. A SCHED_FIFO process at priority 1 runs before a SCHED_OTHER process at nice −20.

$ chrt -p 1842
pid 1842's current scheduling policy: SCHED_OTHER
pid 1842's current scheduling priority: 0

$ sudo chrt -f -p 10 1842
$ chrt -p 1842
pid 1842's current scheduling policy: SCHED_FIFO
pid 1842's current scheduling priority: 10

With this, ingestor moves to SCHED_FIFO at priority 10: as soon as it has a packet to process, it displaces the aggregator and meteo-api immediately.

Is that a good idea? With caveats. SCHED_FIFO is appropriate here because ingestor is I/O-bound: it uses the CPU for 0.3 ms and blocks. But it carries a real danger: a SCHED_FIFO process that enters an infinite loop locks up its core completely, and you will not even be able to open a shell to kill it. Linux protects itself partially with:

$ cat /proc/sys/kernel/sched_rt_runtime_us
950000
$ cat /proc/sys/kernel/sched_rt_period_us
1000000

Real-time processes may use at most 950,000 µs out of every 1,000,000 µs, that is 95%. The remaining 5% is reserved so that the system stays manageable. It is a safety net designed for exactly the infinite-loop scenario.

SCHED_DEADLINE is more modern and safer: instead of a priority, you declare "I need 2 ms of computation every 10 ms, with a deadline at 8 ms" and the kernel rejects the request if it cannot guarantee it. The details of these guarantees belong to Mobile and Real-Time Operating Systems.

Multiprocessor: affinity and load balancing

meteo-01 has 4 cores, so what really has to be decided is which process and on which core.

Linux maintains one run queue per core, not a global one. It is a deliberate decision: a global queue would need a lock shared by every core, and that lock would become the system's bottleneck with 64 or 128 cores.

Two concepts govern the distribution:

Processor affinity. When a process has spent a while on core 2, that core's L1 and L2 caches are full of its data. Migrating it to core 3 means starting cold: hundreds of cache misses at ~100 ns each. That is why the scheduler prefers to keep each process where it was (soft affinity), and only migrates when the imbalance justifies it.

Hard affinity is set by hand:

$ taskset -cp 1877
pid 1877's current affinity list: 0-3

$ sudo taskset -cp 2,3 1877
pid 1877's new affinity list: 2,3

$ sudo taskset -c 0,1 /opt/meteora/bin/ingestor --port 9010

With this you have partitioned the machine: the aggregator can only use cores 2 and 3, and ingestor cores 0 and 1. They do not even compete. It is a valid technique when you know your workload well, and it is widely used in low-latency systems, but it has a cost: if ingestor is idle, its two cores are wasted because the aggregator cannot touch them. Hard affinity trades flexibility for predictability.

Load balancing. Periodically, the kernel checks whether some queues are much more loaded than others and migrates processes. It does so taking topology into account: migrating between two cores that share an L3 cache is cheap; migrating between two different NUMA sockets is expensive, because on top of the cold cache the process would end up accessing memory on another socket.

$ nproc
4
$ mpstat -P ALL 1 1
CPU  %usr %nice %sys %iowait %idle
all  23.1  4.2   6.3    1.8   64.6
  0  31.2  0.0   8.1    3.0   57.7
  1  28.9  0.0   7.2    2.1   61.8
  2  16.4 16.8   4.9    0.9   61.0
  3  15.9  0.0   5.0    1.2   77.9

Interpretation: the %nice column measures the time consumed by processes with a positive nice. That 16.8% on core 2 is exactly the aggregator with its nice 10, and it confirms that the affinity is doing what we asked of it. The low %iowait indicates that the disk is not today's bottleneck.

Common Mistakes and Tips

Believing that nice caps CPU consumption. It does not. A process at nice 19 will use 100% of the CPU if nobody else wants it. nice only decides who gives way in a conflict. To put a real ceiling in place you need cgroups (CPUQuota in systemd), the subject of 06-02.

Putting processes in SCHED_FIFO "just in case". It is the fastest way to make a machine unreachable. It only makes sense for processes that block frequently and demonstrably. And before doing it, try nice -20: it is almost always enough.

Confusing priority PRI with NI. NI is what you ask for; PRI is what the kernel computes from it. On top of that, ps shows PRI on a scale inverted with respect to the kernel's internal one, which is very misleading. To know what is going on, look at NI and the policy (chrt -p).

Applying the exam Gantt chart to reality. The exercises assume one burst per process and no I/O. In reality each process alternates dozens of bursts, new processes come and go and there are several cores. The calculations are there to help you understand the algorithms, not to predict real times.

Looking for the optimal algorithm. There is none. SJF minimizes the average wait but causes starvation and requires predicting the future. Round Robin bounds the response but worsens the average. The right question is never "which is better", but "what hurts me more if it goes wrong: meteo-api's latency or the aggregator's delay".

Forgetting that a blocked process does not compete. It is the most frequent reasoning error when analyzing real workloads. If meteo-api spends 99% of its time waiting for requests, giving it nice -10 changes almost nothing, because when it wants CPU the CPU is usually free.

Practical tip: before touching priorities, check that the CPU really is the problem. Look at %iowait in mpstat and the b column in vmstat: if there are blocked processes and %iowait is high, the bottleneck is the disk and no CPU priority will fix it.

Exercises

Exercise 1: comparing four algorithms numerically

These four jobs arrive on meteo-01:

Process Arrival Burst (ms)
P1 (aggregator) 0 10
P2 (meteo-api) 1 2
P3 (ingestor) 2 4
P4 (cleanup) 3 6

Compute the Gantt chart and the average waiting time for: (a) FCFS, (b) non-preemptive SJF, (c) SRTF, (d) Round Robin with a 3 ms quantum. Then state which algorithm you would choose for Meteora and why.

Exercise 2: deciding real priorities

On meteo-01 (4 cores) you have this situation during the 8:00 peak, when the stations send their morning burst:

  • ingestor: receives 800 readings per second. If its socket buffer fills up, readings are lost irrecoverably.
  • meteo-api: 40 requests per second, with a service target of 200 ms at the 99th percentile.
  • aggregator: computes the previous hour's averages. It must finish before 9:00.
  • daily-backup: compresses /var/lib/meteora/readings/ and ships it to another server. It takes 25 minutes and has no hard deadline.

Decide a policy and priority for each one, write the concrete commands and justify every decision. Also state what risk your configuration introduces.

Exercise 3: calculating the impact of the quantum

A system has 12 runnable processes. The context switch cost is 6 µs. 75% of CPU bursts last less than 5 ms.

  1. Calculate the percentage overhead and the maximum response time with quanta of 500 µs, 5 ms and 50 ms.
  2. Which would you choose for a server where meteo-api must respond in under 200 ms?
  3. If the machine came to have 200 runnable processes, would your answer change?

Solutions

Solution 1

(a) FCFS — in arrival order: P1, P2, P3, P4.

 P1                    P2      P3         P4
├──────────────────┼──────┼─────────┼───────────────┤
0                 10     12        16              22
Process Arrival Start End Waiting (Start−Arrival)
P1 0 0 10 0
P2 1 10 12 9
P3 2 12 16 10
P4 3 16 22 13
Average = (0 + 9 + 10 + 13) / 4 = 8.00 ms

(b) Non-preemptive SJF — when choosing, the shortest of those that have already arrived is taken.

  • t=0: only P1. P1 runs until t=10 (non-preemptive).
  • t=10: P2 (2), P3 (4) and P4 (6) have arrived. The shortest is P2 → until t=12.
  • t=12: P3 (4) against P4 (6). P3 takes over → until t=16.
  • t=16: P4 → until t=22.

It coincides with FCFS by chance, because P1 hogged the beginning:

Average = 8.00 ms

This result is instructive: non-preemptive SJF does not help if the long job arrives first. Its advantage only shows up when there is a real choice.

(c) SRTF (preemptive):

  • t=0: P1 (10). P1 runs.
  • t=1: P2 (2) arrives < P1's 9 remaining. Preemption → P2.
  • t=2: P3 (4) arrives > P2's 1 remaining. P2 continues.
  • t=3: P2 finishes. Candidates: P1 (9), P3 (4), P4 (6). P3 takes over.
  • t=7: P3 finishes. Candidates: P1 (9), P4 (6). P4 takes over.
  • t=13: P4 finishes. P1 takes over.
  • t=22: P1 finishes.
 P1 P2    P3        P4          P1
├─┼──┼───────┼───────────┼───────────────────┤
0 1  3      7          13                   22
Process Arrival End Turnaround Burst Waiting
P1 0 22 22 10 12
P2 1 3 2 2 0
P3 2 7 5 4 1
P4 3 13 10 6 4
Average = (12 + 0 + 1 + 4) / 4 = 4.25 ms

(d) Round Robin, q=3 ms. Initial queue: P1. The others join the end as they arrive.

  • t=0-3: P1 (7 remaining). By the end of the turn P2, P3 and P4 have arrived → queue: P2, P3, P4, P1.
  • t=3-5: P2 (needs 2, finishes before the quantum). Queue: P3, P4, P1.
  • t=5-8: P3 (1 remaining). Queue: P4, P1, P3.
  • t=8-11: P4 (3 remaining). Queue: P1, P3, P4.
  • t=11-14: P1 (4 remaining). Queue: P3, P4, P1.
  • t=14-15: P3 finishes.
  • t=15-18: P4 finishes.
  • t=18-22: P1 finishes.
 P1   P2  P3   P4   P1   P3 P4    P1
├───┼──┼───┼───┼───┼─┼───┼─────┤
0   3  5   8  11  14 15  18    22
Process Arrival End Turnaround Burst Waiting
P1 0 22 22 10 12
P2 1 5 4 2 2
P3 2 15 13 4 9
P4 3 18 15 6 9
Average = (12 + 2 + 9 + 9) / 4 = 8.00 ms

Final comparison:

Algorithm Average wait Worst individual wait
FCFS 8.00 ms 13 ms (P4)
SJF 8.00 ms 13 ms (P4)
SRTF 4.25 ms 12 ms (P1)
Round Robin q=3 8.00 ms 12 ms (P1)

What I would choose for Meteora: none of the four in pure form, but Round Robin with priorities, which is essentially what CFS does. The reasoning:

  • SRTF wins on the average, but it is inapplicable: burst lengths are not known and, above all, it would condemn the aggregator to starvation whenever meteo-api received a continuous stream of requests.
  • FCFS would produce the convoy effect: the aggregator's 10 ms would block every API request.
  • Pure Round Robin treats all four the same, when ingestor and the aggregator have opposite needs.

The practical configuration would be Round Robin with differentiated nice values: ingestor at −5, meteo-api at 0, aggregator at 10 and cleanup at 19. Note as well that these calculations assume a single core; with meteo-01's four, the four processes would run almost in parallel and the average wait would drop drastically under every algorithm.

Solution 2

Preliminary analysis. The first thing to do is classify by cost of delay, not by perceived importance:

Process Type Cost of delaying it
ingestor I/O, minimal bursts Irreversible: data lost
meteo-api I/O, short bursts Contractual: missing the 200 ms target
aggregator CPU, long bursts Recoverable: it just has to finish before 9:00
daily-backup CPU + disk None: it can run whenever there is time to spare

Proposed configuration:

# 1. ingestor: highest normal priority. Losing readings is irreversible.
$ sudo renice -n -10 -p $(pgrep -f ingestor)

# 2. meteo-api: slightly raised priority.
$ sudo renice -n -3 -p $(pgrep -f meteo-api)

# 3. aggregator: low priority, confined to 2 of the 4 cores.
$ sudo renice -n 10 -p $(pgrep -f aggregator)
$ sudo taskset -cp 2,3 $(pgrep -f aggregator)

# 4. backup: minimum CPU and disk priority.
$ sudo renice -n 19 -p $(pgrep -f daily-backup)
$ sudo ionice -c 3 -p $(pgrep -f daily-backup)

Justification for each decision:

  1. ingestor at nice −10, not SCHED_FIFO. With nice −10 it gets 90% of the CPU against a normal process in a conflict, more than enough for a process that only uses 0.3 ms per reading. SCHED_FIFO would give a stronger guarantee but would introduce the risk of locking up an entire core if ingestor had a bug. The rule is not to use real time if nice suffices, and here it suffices with room to spare.

  2. meteo-api at nice −3. It needs to respond quickly, but it is I/O-bound: it spends nearly all its time blocked, and when it wakes up CFS already gives it preference because it has the lowest vruntime. A moderate nudge is enough and avoids putting it in competition with ingestor.

  3. aggregator at nice 10 and confined to 2 cores. It is the only CPU-bound one and the one that can hurt the others. At nice 10 it gets ~10% in a conflict and 100% when the others are asleep, which is nearly always. Confining it to cores 2 and 3 guarantees that cores 0 and 1 are always available for the 8:00 peak. Since it has to finish within an hour and its actual computation takes minutes, it has ample margin.

  4. daily-backup at nice 19 and ionice -c 3. Here is the point that always gets forgotten: the backup compresses and transfers, so it competes for CPU and for disk. renice only solves the first. ionice -c 3 (the idle class) makes it use the disk only when nobody else is asking for it, and that protects the aggregator's reads from /var/lib/meteora/readings/.

Risks this configuration introduces:

  • Hard affinity wastes capacity. If ingestor and meteo-api are idle at 3 in the morning, cores 0 and 1 go unused while the aggregator limits itself to two. It would be better to apply the affinity only during the peak window, with a timer, or to remove it if the aggregator starts running up against its deadline.
  • ionice -c 3 can starve the backup on a server with continuous disk activity. If the backup stops completing, you have to move it up to -c 2 -n 7 (best effort, lowest priority), which does guarantee progress.
  • None of this limits memory. If the aggregator consumes all the RAM, the OOM killer could kill meteo-api regardless of the CPU priorities. That front is covered with cgroups, in 06-02.

Solution 3

1. Calculations. With 12 processes and a context switch cost of 6 µs, the worst wait for the first turn is (n−1)·(q + cost) = 11·(q + 6 µs).

Quantum Overhead = 6/(q+6) Worst response time 5 ms bursts completed
500 µs 6/506 = 1.19% 11 × 506 µs = 5.57 ms No (it needs 10 turns)
5 ms 6/5006 = 0.12% 11 × 5.006 ms = 55.1 ms Yes, just barely
50 ms 6/50006 = 0.012% 11 × 50.006 ms = 550 ms Yes, with room to spare

2. The choice for meteo-api with a 200 ms target: a 5 ms quantum.

  • 500 µs is ruled out even though its response time is excellent, because 75% of bursts last less than 5 ms and with q=500 µs a typical burst would need up to 10 turns. Each interleaved turn means coming back with the caches polluted by the other 11 processes: the real overhead is far higher than the nominal 1.19%, which only counts the context switch and not the cache misses.
  • 50 ms is ruled out outright: 550 ms in the worst case far exceeds the 200 ms target. A single load spike would be enough to miss it.
  • 5 ms is the right spot: negligible overhead (0.12%), 55 ms worst-case response — a 3.6× margin over the target — and it matches the threshold at which 75% of bursts finish on their own, which is precisely the practical rule from the section on the quantum.

3. With 200 runnable processes: yes, it would change, and drastically.

Worst response time with q=5 ms:  199 × 5.006 ms = 996 ms ≈ 1 second

Almost five times the 200 ms target. To meet it again:

q_max = 200 ms / 199 ≈ 1.0 ms
Resulting overhead: 6 / 1006 = 0.6%

You would have to drop the quantum to 1 ms, accepting five times more context switch overhead.

And here is the most valuable observation in the exercise: this is exactly why CFS does not use a fixed quantum. Linux defines a target latency (sched_latency_ns, 6 ms by default) which is the period within which every runnable process must get CPU at least once, and it computes the quantum by dividing that by the number of runnable processes. With a minimum-granularity floor (sched_min_granularity_ns, ~0.75 ms) so it does not degenerate:

$ cat /proc/sys/kernel/sched_latency_ns 2>/dev/null || echo "(EEVDF: replaced by sched_base_slice_ns)"
6000000

12 processes:  6 ms / 12 = 0.5 ms   → below the floor, 0.75 ms is used
200 processes: 6 ms / 200 = 0.03 ms → the 0.75 ms floor is used
               and the real latency stretches to 200 × 0.75 = 150 ms

In other words: the scheduler holds the target latency for as long as it can, and when there are too many processes it prefers to stretch it rather than ruin performance with tiny quanta. That controlled degradation is a conscious design decision, and it explains why a server with 2,000 runnable processes feels slow even though the CPU is not at 100%: it is not short of CPU, it is short of turns.

Conclusion

Scheduling is necessary because processes alternate between CPU bursts and I/O bursts, and when one blocks the CPU would otherwise be wasted. The distinction between CPU-bound and I/O-bound processes is what guides every practical decision, and it is read directly from the ratio of voluntary to involuntary context switches in /proc/<pid>/status. Favoring the short ones is not charity: it is what keeps every resource busy at the same time.

The criteria contradict each other: there is no algorithm that simultaneously optimizes throughput, turnaround time and response time. FCFS is trivial but suffers the convoy effect; SJF/SRTF are optimal on average waiting time but require predicting the future and cause starvation; priorities need aging so as not to condemn anybody; Round Robin sacrifices the average in exchange for bounding the response, with the quantum as the central dial; and feedback queues deduce from behavior what they cannot know in advance.

Linux solves all this with CFS/EEVDF, which replaces queues and quanta with a virtual time and always picks the process furthest behind its fair share. That is where nice fits in: it does not hand out turns, it changes the rate of the virtual clock, with the rule of roughly 10% of share per point. And nice acts only in a conflict and only on the CPU, two limits you must always keep in mind. Above it sit the real-time policies, powerful and dangerous in equal measure, and below it the distribution across cores with affinity and balancing.

So far we have been handing out the CPU while taking for granted that each process has its own memory and does not tread on anyone else's. It is time to ask how that is achieved: how several processes share a limited physical memory without invading each other, what turns ingestor's address 0x400000 and the aggregator's 0x400000 into two different physical places, and what exactly the hardware does to stop one from reading the other's data. That is what we will see in Memory Management.

Operating Systems Fundamentals

Module 1: Introduction to Operating Systems

Module 2: Resource Management

Module 3: Concurrency

Module 4: File Structures

Module 5: System Protection and Security

Module 6: Virtualization and Containers

Module 7: Administration and Troubleshooting in Practice

© Copyright 2026. All rights reserved