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
- Why scheduling is necessary: the burst cycle
- CPU-bound and I/O-bound processes
- The three schedulers and the dispatcher
- Evaluation criteria and their trade-offs
- Preemptive versus cooperative scheduling
- FCFS: first come, first served
- SJF and SRTF: shortest job first
- Priority scheduling, starvation and aging
- Round Robin and the effect of the quantum
- Multilevel queues and feedback queues
- The real Linux scheduler: CFS, EEVDF and
nice - Real-time policies:
SCHED_FIFO,SCHED_RR,SCHED_DEADLINE - 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 msEach 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: 44The 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:
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:
- The process goes from Running to Blocked (it requests I/O).
- The process goes from Running to Ready (the quantum expires, an interrupt arrives).
- The process goes from Blocked to Ready (its I/O completes).
- 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:
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.
| Process | Waiting |
|---|---|
| P2 | 0 |
| P3 | 2 |
| P1 | 4 |
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:
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.
| 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 |
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:
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 |
| Process | Waiting |
|---|---|
| P2 | 0 |
| P5 | 1 |
| P1 | 6 |
| P3 | 16 |
| P4 | 18 |
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:
| Process | End | Turnaround | Burst | Waiting |
|---|---|---|---|---|
| P1 | 30 | 30 | 24 | 6 |
| P2 | 7 | 6 | 3 | 3 |
| P3 | 10 | 8 | 3 | 5 |
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.
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
nicechanges 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-01What each command does:
renice -n 10 -p 1877lowers theaggregatortonice10. Now, competing withmeteo-api(nice 0), it will receive roughly 9.7% of the CPU when both want to run. Whenmeteo-apiis blocked waiting for requests — which is nearly always — theaggregatorwill use 100%. This is the key point many people miss:nicedoes 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:
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.
- Calculate the percentage overhead and the maximum response time with quanta of 500 µs, 5 ms and 50 ms.
- Which would you choose for a server where
meteo-apimust respond in under 200 ms? - 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.
| 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 |
(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:
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.
| 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 |
(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.
| 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 |
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
aggregatorto starvation whenevermeteo-apireceived 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
ingestorand theaggregatorhave 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:
-
ingestoratnice −10, notSCHED_FIFO. Withnice −10it 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_FIFOwould give a stronger guarantee but would introduce the risk of locking up an entire core ifingestorhad a bug. The rule is not to use real time ifnicesuffices, and here it suffices with room to spare. -
meteo-apiatnice −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 lowestvruntime. A moderate nudge is enough and avoids putting it in competition withingestor. -
aggregatoratnice 10and confined to 2 cores. It is the only CPU-bound one and the one that can hurt the others. Atnice 10it 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. -
daily-backupatnice 19andionice -c 3. Here is the point that always gets forgotten: the backup compresses and transfers, so it competes for CPU and for disk.reniceonly 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 theaggregator's reads from/var/lib/meteora/readings/.
Risks this configuration introduces:
- Hard affinity wastes capacity. If
ingestorandmeteo-apiare idle at 3 in the morning, cores 0 and 1 go unused while theaggregatorlimits itself to two. It would be better to apply the affinity only during the peak window, with a timer, or to remove it if theaggregatorstarts running up against its deadline. ionice -c 3can 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
aggregatorconsumes all the RAM, the OOM killer could killmeteo-apiregardless 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.
Almost five times the 200 ms target. To meet it again:
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 msIn 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
- Basic Concepts of Operating Systems
- History and Evolution of Operating Systems
- Types of Operating Systems
- Main Functions of an Operating System
- Kernel Architecture: Monolithic, Microkernel and Hybrid
- User Mode, Kernel Mode and System Calls
Module 2: Resource Management
- Process Management
- CPU Scheduling
- Memory Management
- Virtual Memory and Paging
- Storage Management
- Device Management
- Drivers, Interrupts and I/O Operations
Module 3: Concurrency
- Concurrency Concepts
- Threads and Processes
- Inter-Process Communication (IPC)
- Synchronization and Mutual Exclusion
- Classic Concurrency Problems
- Deadlocks: Prevention, Detection and Recovery
Module 4: File Structures
- File Systems
- Directory Structures
- Partitions, Mounting and the Virtual File System
- File Management
- Space Allocation, Journaling and Integrity
- File Security and Permissions
Module 5: System Protection and Security
- Protection Principles and Access Control
- Users, Authentication and Privilege Escalation
- Common Threats and System Hardening
- Auditing, Logging and Incident Response
Module 6: Virtualization and Containers
- Virtualization: Hypervisors and Virtual Machines
- Containers: Namespaces and cgroups
- The Operating System in the Cloud
- Mobile and Real-Time Operating Systems
