Operating Systems Module 1 – Foundations: what an operating system does
What an operating system does
The three jobs of every operating system, abstraction, sharing and protection, each shown by a program that ran, and where the kernel sits among the rest.
What you will learn
- Explain the three jobs of an operating system, abstraction, resource allocation and protection, with an example of each
- Show with a running program that two processes have separate address spaces
- Distinguish the kernel from the C library, the shell, system services and applications
- Name the operating-system families in use today and the devices each one runs on
What you’ll learn
What you will learn
- Explain the three jobs of an operating system, abstraction, resource allocation and protection, with an example of each
- Show with a running program that two processes have separate address spaces
- Distinguish the kernel from the C library, the shell, system services and applications
- Name the operating-system families in use today and the devices each one runs on
On this page
Two processes in the first program below store a value at the same memory address, 1 in one and 101 in the other, and neither ever sees the other’s value. The software that makes this possible, while a handful of processor cores and one memory serve many programs at once, is the operating system.
Every operating system does three jobs: it hides shared hardware behind abstractions that each program gets for itself, it shares the hardware out between programs, and it protects programs from one another and the machine from all of them. Each job comes with a program that ran here. Then the lesson separates the kernel from the rest of the software, and names the families of operating systems that run computers, phones and microcontrollers today.
Every program gets a machine of its own
A program behaves as if it had the computer to itself. It counts on its variables keeping their values, on its files holding what it wrote, and on running from its first line to its last, while dozens of other programs do the same on the same hardware. The part of the operating system that keeps this promise is the kernel: The part of an operating system that runs in the processor's privileged mode. It owns the hardware, shares it between programs and answers their system calls.. It builds one abstraction for each kind of shared hardware:
What the kernel turns the shared hardware into
Text description of the diagram
The figure has two columns. On the left is hardware that every program on the computer shares; on the right is what each program sees instead. An arrow leads from each piece of hardware to its abstraction, and each abstraction's box names, in brackets, the part of the kernel that builds it.
- A handful of CPU cores become processes: each program runs as if it had a CPU of its own, because the kernel gives the cores out in time slices.
- One physical memory becomes address spaces: each program sees memory of its own, because page tables map its addresses onto parts of the physical memory.
- A disk of numbered blocks becomes files: named streams of bytes, built by the file system.
- A network card that sends and receives raw frames becomes sockets: streams of bytes to programs on this or another computer, built by the network stack.
Read the figure across. The cores become process: A running program together with what the kernel keeps for it: an address space, saved registers, open files and an owner. Each process runs as if it had the computer to itself., each a CPU of its own, because the kernel gives the cores out in turns. One physical memory becomes many address space: The range of memory addresses a process can use. The kernel maps it onto physical memory with page tables, so the same address in two processes can hold different values., each a private range of addresses mapped onto a part of that memory. The disk’s numbered blocks become files, named streams of bytes, and the network card’s frames become sockets, streams of bytes to other programs.
The address space is the easiest of the four to see. The program below calls fork, which makes a second process, a
child, with a copy of the parent’s memory. The fork(2) manual promises that the child’s memory starts as an exact
copy of the parent’s and that a write by one of them never reaches the other. Each changes counter and prints it:
#include <stdint.h>
#include <stdio.h>
#include <sys/wait.h>
#include <unistd.h>
static int counter = 100;
int main(void) {
int fds[2];
if (pipe(fds) != 0) {
perror("pipe");
return 1;
}
printf("before fork: counter = %d\n", counter);
fflush(stdout); // or the child would print a second copy of this line
pid_t child = fork();
if (child < 0) {
perror("fork");
return 1;
}
if (child == 0) {
counter = 1;
printf("child: set counter to 1, reads %d\n", counter);
uintptr_t where = (uintptr_t)&counter;
if (write(fds[1], &where, sizeof where) != (ssize_t)sizeof where) {
return 1;
}
return 0;
}
uintptr_t child_where = 0;
if (read(fds[0], &child_where, sizeof child_where) !=
(ssize_t)sizeof child_where) {
return 1;
}
waitpid(child, NULL, 0);
counter += 1;
printf("parent: added 1, reads %d\n", counter);
printf("same address in both processes: %s\n",
child_where == (uintptr_t)&counter ? "yes" : "no");
return 0;
} Output
before fork: counter = 100 child: set counter to 1, reads 1 parent: added 1, reads 101 same address in both processes: yes
Recorded with Apple clang version 21.0.0 (clang-2100.3.34.2) on macOS 26 arm64. To run it yourself: clang -std=c23 -O2 -Wall -Wextra -Werror -pedantic -fsanitize=address,undefined own_memory.c -o own_memory && ./own_memory
The child set counter to 1, and the parent, which still had 100, added 1 and read 101. Both used the same address:
the child sent its address through a pipe and the parent compared it with its own. An address is a name inside one
address space, and the kernel maps the same name to different physical memory for each process. The program flushes
its output before fork, because the child gets a copy of everything in memory, unwritten output included, and would
print the first line a second time.
Key idea
The kernel turns shared hardware into private abstractions: processes, address spaces, files and sockets, so that every program can be written as if the machine were its own.
Checkpoint
Check: Abstractions that give each program its own machine
Sharing one CPU in time and one memory in space
A core runs one instruction stream at a time, so the kernel shares it in time: it lets one program run for a time slice: The stretch of CPU time a program may run before the kernel takes the processor back and gives another program a turn., takes the CPU back, and gives the next program a turn. Memory can hold many programs side by side, so it is shared in space: each program gets its own pieces of it, and the disk is divided the same way into blocks for each file.
Sharing in time needs a place to keep a program’s state while it waits. A CPU has one set of registers. When a slice
ends, the kernel copies them into the program’s record; before the next slice of that program, it copies them back.
Here is that run for three tiny programs, each made of add and mul steps on a running total, acc, with slices
of 2 steps:
The drawing shows the whole scene; its 11 steps are listed below.
Every step as text
- Three programs, one CPU. Each program has a record that keeps its registers while it waits, pc (its next step) and acc (its running total). All three start at 0. The kernel lets each program run 2 steps, then takes the CPU back.
- Load A. The kernel takes A from the front of the ready queue and copies A's saved registers into the CPU.
- A runs two steps. Ticks 0 and 1 each add 1, so the CPU's accumulator goes from 0 to 2 and its program counter from 0 to 2.
- Save A. The slice is over. The kernel copies the CPU's registers into A's record and puts A at the back of the queue.
- Load B. B's record says pc 0 and acc 0, so B starts at its first step. The CPU now holds B's registers, and A's values wait safely in A's record.
- B runs two steps. Tick 2 adds 2 and tick 3 multiplies by 5, which leaves 10 in the accumulator.
- Save B. Two steps are used up, so B's registers go back into its record, and B joins the queue behind A.
- C runs its first slice. The kernel loads C, and ticks 4 and 5 add 10 and then 20, which makes 30. Then it saves C's registers, pc 2 and acc 30, the same way.
- A carries on. A's record still says pc 2 and acc 2, so A goes on with its third step as if it had never stopped. Ticks 6 and 7 bring its total to 4.
- A finishes. A has no steps left, so the kernel does not queue it again. Its total, 4, is exactly what A computes when it runs alone.
- B and C finish. B's last step, tick 8, adds 1 to make 11, and C's last step, tick 9, adds 30 to make 60. Each program had only part of the CPU's time, yet each computed exactly what it computes alone.
The same run as a program follows in the 14 languages of this track. Their outputs matched byte for byte, so the page shows one copy of them, under the tabs:
Python 3.14 · main.py
from collections import deque
from dataclasses import dataclass
SLICE = 2 # steps a program may run before the kernel takes the CPU back
@dataclass
class Process:
name: str
steps: list[tuple[str, int]]
pc: int = 0 # saved registers: the next step and the accumulator
acc: int = 0
def execute(op: str, n: int, acc: int) -> int:
return acc + n if op == "add" else acc * n
def run_alone(steps: list[tuple[str, int]]) -> int:
acc = 0
for op, n in steps:
acc = execute(op, n, acc)
return acc
def main() -> None:
processes = [
Process("A", [("add", 1), ("add", 1), ("add", 1), ("add", 1)]),
Process("B", [("add", 2), ("mul", 5), ("add", 1)]),
Process("C", [("add", 10), ("add", 20), ("add", 30)]),
]
ready = deque(processes)
tick = 0
while ready:
p = ready.popleft()
pc, acc = p.pc, p.acc # load the saved registers into the CPU
print(f"load {p.name} (pc {pc}, acc {acc})")
for _ in range(SLICE):
if pc == len(p.steps):
break
op, n = p.steps[pc]
acc = execute(op, n, acc)
pc += 1
print(f"tick {tick}: {p.name} {op} {n} -> acc {acc}")
tick += 1
p.pc, p.acc = pc, acc # save them back into the process's record
if pc == len(p.steps):
print(f"{p.name} finished with acc {acc} (alone: {run_alone(p.steps)})")
else:
print(f"save {p.name} (pc {pc}, acc {acc})")
ready.append(p)
main() In Python: A dataclass is the process's record, and deque gives the ready queue a popleft that does not shift the whole list. The tuple assignment loads and saves both registers at once.
Recorded with CPython 3.14.8 on macOS 26 arm64. It runs in your browser with Pyodide 314.0.7 (CPython 3.14.2). To run it yourself: python3 main.py
JavaScript Node 24 · main.mjs
const SLICE = 2; // steps a program may run before the kernel takes the CPU back
const execute = (op, n, acc) => (op === "add" ? acc + n : acc * n);
const runAlone = (steps) => steps.reduce((acc, [op, n]) => execute(op, n, acc), 0);
const add = (n) => ["add", n];
const mul = (n) => ["mul", n];
// pc and acc are each program's saved registers: the next step and the accumulator.
const processes = [
{ name: "A", steps: [add(1), add(1), add(1), add(1)], pc: 0, acc: 0 },
{ name: "B", steps: [add(2), mul(5), add(1)], pc: 0, acc: 0 },
{ name: "C", steps: [add(10), add(20), add(30)], pc: 0, acc: 0 },
];
const ready = [...processes];
let tick = 0;
for (let p = ready.shift(); p !== undefined; p = ready.shift()) {
let { pc, acc } = p; // load the saved registers into the CPU
console.log(`load ${p.name} (pc ${pc}, acc ${acc})`);
for (let i = 0; i < SLICE; i++) {
if (pc === p.steps.length) break;
const [op, n] = p.steps[pc];
acc = execute(op, n, acc);
pc += 1;
console.log(`tick ${tick}: ${p.name} ${op} ${n} -> acc ${acc}`);
tick += 1;
}
Object.assign(p, { pc, acc }); // save them back into the process's record
if (pc === p.steps.length) {
console.log(`${p.name} finished with acc ${acc} (alone: ${runAlone(p.steps)})`);
} else {
console.log(`save ${p.name} (pc ${pc}, acc ${acc})`);
ready.push(p);
}
} In JavaScript: Each record is a plain object; destructuring loads pc and acc, and Object.assign writes both back. shift takes the next program off the front of the ready array.
Recorded with Node.js 24.21.0 LTS (V8 13.6) on macOS 26 arm64. It runs in your browser with QuickJS (quickjs-emscripten 0.32.0). To run it yourself: node main.mjs
Java 25 LTS · Main.java
import java.util.ArrayDeque;
import java.util.List;
public class Main {
static final int SLICE = 2; // steps a program may run before the kernel takes the CPU back
record Step(String op, int n) {}
static final class Process {
final String name;
final List<Step> steps;
int pc; // saved registers: the next step and the accumulator
int acc;
Process(String name, List<Step> steps) {
this.name = name;
this.steps = steps;
}
}
static int execute(String op, int n, int acc) {
return op.equals("add") ? acc + n : acc * n;
}
static int runAlone(List<Step> steps) {
int acc = 0;
for (Step step : steps) {
acc = execute(step.op(), step.n(), acc);
}
return acc;
}
public static void main(String[] args) {
List<Process> processes =
List.of(
new Process(
"A",
List.of(
new Step("add", 1),
new Step("add", 1),
new Step("add", 1),
new Step("add", 1))),
new Process("B", List.of(new Step("add", 2), new Step("mul", 5), new Step("add", 1))),
new Process(
"C", List.of(new Step("add", 10), new Step("add", 20), new Step("add", 30))));
ArrayDeque<Process> ready = new ArrayDeque<>(processes);
int tick = 0;
while (!ready.isEmpty()) {
Process p = ready.removeFirst();
int pc = p.pc; // load the saved registers into the CPU
int acc = p.acc;
System.out.println("load " + p.name + " (pc " + pc + ", acc " + acc + ")");
for (int i = 0; i < SLICE; i++) {
if (pc == p.steps.size()) {
break;
}
Step step = p.steps.get(pc);
acc = execute(step.op(), step.n(), acc);
pc += 1;
System.out.println(
"tick " + tick + ": " + p.name + " " + step.op() + " " + step.n() + " -> acc " + acc);
tick += 1;
}
p.pc = pc; // save them back into the process's record
p.acc = acc;
if (pc == p.steps.size()) {
System.out.println(
p.name + " finished with acc " + acc + " (alone: " + runAlone(p.steps) + ")");
} else {
System.out.println("save " + p.name + " (pc " + pc + ", acc " + acc + ")");
ready.addLast(p);
}
}
}
} In Java: Step is a record, but a process is a small class because the kernel changes its saved registers. ArrayDeque is the ready queue.
Recorded with OpenJDK 25.0.4.1 LTS (Eclipse Temurin) on macOS 26 arm64. It runs in your browser with a TeaVM 0.16.0 build in QuickJS (quickjs-emscripten 0.32.0). To run it yourself: java Main.java
C++23 · main.cpp
#include <cstddef>
#include <deque>
#include <print>
#include <string>
#include <vector>
// steps a program may run before the kernel takes the CPU back
constexpr int SLICE = 2;
struct Step {
std::string op;
int n;
};
struct Process {
char name;
std::vector<Step> steps;
std::size_t pc = 0; // saved registers: the next step and the accumulator
int acc = 0;
};
int execute(const std::string &op, int n, int acc) {
return op == "add" ? acc + n : acc * n;
}
int run_alone(const std::vector<Step> &steps) {
int acc = 0;
for (const auto &[op, n] : steps) {
acc = execute(op, n, acc);
}
return acc;
}
int main() {
std::vector<Process> processes = {
{'A', {{"add", 1}, {"add", 1}, {"add", 1}, {"add", 1}}},
{'B', {{"add", 2}, {"mul", 5}, {"add", 1}}},
{'C', {{"add", 10}, {"add", 20}, {"add", 30}}},
};
std::deque<Process *> ready;
for (auto &p : processes) {
ready.push_back(&p);
}
int tick = 0;
while (!ready.empty()) {
Process *p = ready.front();
ready.pop_front();
auto pc = p->pc; // load the saved registers into the CPU
auto acc = p->acc;
std::println("load {} (pc {}, acc {})", p->name, pc, acc);
for (int i = 0; i < SLICE; i++) {
if (pc == p->steps.size()) {
break;
}
const auto &[op, n] = p->steps[pc];
acc = execute(op, n, acc);
pc += 1;
std::println("tick {}: {} {} {} -> acc {}", tick, p->name, op, n, acc);
tick += 1;
}
p->pc = pc; // save them back into the process's record
p->acc = acc;
if (pc == p->steps.size()) {
std::println("{} finished with acc {} (alone: {})", p->name, acc,
run_alone(p->steps));
} else {
std::println("save {} (pc {}, acc {})", p->name, pc, acc);
ready.push_back(p);
}
}
} In C++: The ready queue holds pointers into the vector, so saving the registers changes the record itself rather than a copy.
Recorded with Apple clang 21.0.0 (C++23) and GCC 16.2.0 on macOS 26 arm64. It runs in your browser with Clang in WebAssembly (YoWASP) 22.0.0-git20542-10. To run it yourself: clang++ -std=c++23 -O2 -Wall -Wextra -Werror -pedantic -fsanitize=address,undefined main.cpp -o main && ./main
C# 14 · .NET 10 · app.cs
using System.Globalization;
CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;
const int Slice = 2; // steps a program may run before the kernel takes the CPU back
List<Process> processes =
[
new("A", [new("add", 1), new("add", 1), new("add", 1), new("add", 1)]),
new("B", [new("add", 2), new("mul", 5), new("add", 1)]),
new("C", [new("add", 10), new("add", 20), new("add", 30)]),
];
var ready = new Queue<Process>(processes);
int tick = 0;
while (ready.Count > 0)
{
var p = ready.Dequeue();
var (pc, acc) = (p.Pc, p.Acc); // load the saved registers into the CPU
Console.WriteLine($"load {p.Name} (pc {pc}, acc {acc})");
for (int i = 0; i < Slice; i++)
{
if (pc == p.Steps.Count)
{
break;
}
var (op, n) = p.Steps[pc];
acc = Execute(op, n, acc);
pc += 1;
Console.WriteLine($"tick {tick}: {p.Name} {op} {n} -> acc {acc}");
tick += 1;
}
(p.Pc, p.Acc) = (pc, acc); // save them back into the process's record
if (pc == p.Steps.Count)
{
Console.WriteLine($"{p.Name} finished with acc {acc} (alone: {RunAlone(p.Steps)})");
}
else
{
Console.WriteLine($"save {p.Name} (pc {pc}, acc {acc})");
ready.Enqueue(p);
}
}
static int Execute(string op, int n, int acc) => op == "add" ? acc + n : acc * n;
static int RunAlone(List<Step> steps) =>
steps.Aggregate(0, (acc, step) => Execute(step.Op, step.N, acc));
record Step(string Op, int N);
sealed class Process(string name, List<Step> steps)
{
public string Name { get; } = name;
public List<Step> Steps { get; } = steps;
public int Pc { get; set; } // saved registers: the next step and the accumulator
public int Acc { get; set; }
} In C#: Step is a positional record, so var (op, n) takes it apart. Process uses a primary constructor, and its registers are settable properties.
Recorded with C# 14 on .NET 10.0.12 (SDK 10.0.401) on macOS 26 arm64. It runs in your browser with the .NET runtime for WebAssembly 10.0.12. To run it yourself: dotnet run app.cs
C23 · main.c
#include <stddef.h>
#include <stdio.h>
// steps a program may run before the kernel takes the CPU back
enum { SLICE = 2 };
typedef enum { ADD, MUL } Op;
static const char *const OP_NAMES[] = {"add", "mul"};
typedef struct {
Op op;
int n;
} Step;
typedef struct {
char name;
Step steps[4];
size_t count;
size_t pc; // saved registers: the next step and the accumulator
int acc;
} Process;
static int execute(Op op, int n, int acc) {
return op == ADD ? acc + n : acc * n;
}
static int run_alone(const Process *p) {
int acc = 0;
for (size_t i = 0; i < p->count; i++) {
acc = execute(p->steps[i].op, p->steps[i].n, acc);
}
return acc;
}
int main(void) {
Process processes[] = {
{'A', {{ADD, 1}, {ADD, 1}, {ADD, 1}, {ADD, 1}}, 4, 0, 0},
{'B', {{ADD, 2}, {MUL, 5}, {ADD, 1}}, 3, 0, 0},
{'C', {{ADD, 10}, {ADD, 20}, {ADD, 30}}, 3, 0, 0},
};
const size_t total = sizeof processes / sizeof processes[0];
Process *ready[16];
size_t head = 0;
size_t tail = 0;
for (size_t i = 0; i < total; i++) {
ready[tail++] = &processes[i];
}
int tick = 0;
while (head < tail) {
Process *p = ready[head++];
size_t pc = p->pc; // load the saved registers into the CPU
int acc = p->acc;
printf("load %c (pc %zu, acc %d)\n", p->name, pc, acc);
for (int i = 0; i < SLICE; i++) {
if (pc == p->count) {
break;
}
const Step step = p->steps[pc];
acc = execute(step.op, step.n, acc);
pc += 1;
printf("tick %d: %c %s %d -> acc %d\n", tick, p->name, OP_NAMES[step.op],
step.n, acc);
tick += 1;
}
p->pc = pc; // save them back into the process's record
p->acc = acc;
if (pc == p->count) {
printf("%c finished with acc %d (alone: %d)\n", p->name, acc,
run_alone(p));
} else {
printf("save %c (pc %zu, acc %d)\n", p->name, pc, acc);
ready[tail++] = p;
}
}
return 0;
} In C: The ready queue is an array of pointers with a head and a tail index; 16 places are more than this run ever fills.
Recorded with Apple clang 21.0.0 (C23) and GCC 16.2.0 on macOS 26 arm64. It runs in your browser with Clang in WebAssembly (YoWASP) 22.0.0-git20542-10. To run it yourself: clang -std=c23 -O2 -Wall -Wextra -Werror -pedantic -fsanitize=address,undefined main.c -o main && ./main
PHP 8.5 · main.php
<?php
declare(strict_types=1);
const SLICE = 2; // steps a program may run before the kernel takes the CPU back
final class Process
{
public int $pc = 0; // saved registers: the next step and the accumulator
public int $acc = 0;
/** @param list<array{string, int}> $steps */
public function __construct(
public readonly string $name,
public readonly array $steps,
) {}
}
function execute(string $op, int $n, int $acc): int
{
return $op === 'add' ? $acc + $n : $acc * $n;
}
/** @param list<array{string, int}> $steps */
function runAlone(array $steps): int
{
$acc = 0;
foreach ($steps as [$op, $n]) {
$acc = execute($op, $n, $acc);
}
return $acc;
}
$processes = [
new Process('A', [['add', 1], ['add', 1], ['add', 1], ['add', 1]]),
new Process('B', [['add', 2], ['mul', 5], ['add', 1]]),
new Process('C', [['add', 10], ['add', 20], ['add', 30]]),
];
$ready = new SplQueue();
foreach ($processes as $p) {
$ready->enqueue($p);
}
$tick = 0;
while (!$ready->isEmpty()) {
$p = $ready->dequeue();
[$pc, $acc] = [$p->pc, $p->acc]; // load the saved registers into the CPU
echo "load {$p->name} (pc {$pc}, acc {$acc})\n";
for ($i = 0; $i < SLICE; $i++) {
if ($pc === count($p->steps)) {
break;
}
[$op, $n] = $p->steps[$pc];
$acc = execute($op, $n, $acc);
$pc += 1;
echo "tick {$tick}: {$p->name} {$op} {$n} -> acc {$acc}\n";
$tick += 1;
}
[$p->pc, $p->acc] = [$pc, $acc]; // save them back into the process's record
if ($pc === count($p->steps)) {
echo "{$p->name} finished with acc {$acc} (alone: " . runAlone($p->steps) . ")\n";
} else {
echo "save {$p->name} (pc {$pc}, acc {$acc})\n";
$ready->enqueue($p);
}
} In PHP: The record is a final class with promoted constructor properties, and SplQueue is the ready queue. A list assignment saves both registers at once.
Recorded with PHP 8.5.11 (CLI) on macOS 26 arm64. It runs in your browser with php-wasm 0.2.0 (PHP 8.5.2). To run it yourself: php main.php
Rust 1.99 · main.rs
use std::collections::VecDeque;
const SLICE: usize = 2; // steps a program may run before the kernel takes the CPU back
struct Process {
name: char,
steps: Vec<(&'static str, i32)>,
pc: usize, // saved registers: the next step and the accumulator
acc: i32,
}
impl Process {
fn new(name: char, steps: Vec<(&'static str, i32)>) -> Self {
Self {
name,
steps,
pc: 0,
acc: 0,
}
}
}
fn execute(op: &str, n: i32, acc: i32) -> i32 {
if op == "add" { acc + n } else { acc * n }
}
fn run_alone(steps: &[(&str, i32)]) -> i32 {
steps.iter().fold(0, |acc, &(op, n)| execute(op, n, acc))
}
fn main() {
let mut processes = vec![
Process::new('A', vec![("add", 1), ("add", 1), ("add", 1), ("add", 1)]),
Process::new('B', vec![("add", 2), ("mul", 5), ("add", 1)]),
Process::new('C', vec![("add", 10), ("add", 20), ("add", 30)]),
];
let mut ready: VecDeque<usize> = (0..processes.len()).collect();
let mut tick = 0;
while let Some(i) = ready.pop_front() {
let p = &mut processes[i];
let (mut pc, mut acc) = (p.pc, p.acc); // load the saved registers into the CPU
println!("load {} (pc {pc}, acc {acc})", p.name);
for _ in 0..SLICE {
if pc == p.steps.len() {
break;
}
let (op, n) = p.steps[pc];
acc = execute(op, n, acc);
pc += 1;
println!("tick {tick}: {} {op} {n} -> acc {acc}", p.name);
tick += 1;
}
(p.pc, p.acc) = (pc, acc); // save them back into the process's record
if pc == p.steps.len() {
println!(
"{} finished with acc {acc} (alone: {})",
p.name,
run_alone(&p.steps)
);
} else {
println!("save {} (pc {pc}, acc {acc})", p.name);
ready.push_back(i);
}
}
} In Rust: The ready queue holds indexes into the vector, not references, so the borrow checker lets the loop change one record at a time.
Recorded with rustc 1.99.0 (edition 2024) on macOS 26 arm64. It runs in your browser with a WebAssembly build on the WASI runner (browser_wasi_shim 0.4.2). To run it yourself: rustc --edition 2024 -O main.rs && ./main
TypeScript 6.0 · main.ts
const SLICE = 2; // steps a program may run before the kernel takes the CPU back
type Step = [op: "add" | "mul", n: number];
interface Process {
name: string;
steps: Step[];
pc: number; // saved registers: the next step and the accumulator
acc: number;
}
const execute = (op: Step[0], n: number, acc: number): number => (op === "add" ? acc + n : acc * n);
const runAlone = (steps: Step[]): number => steps.reduce((acc, [op, n]) => execute(op, n, acc), 0);
const add = (n: number): Step => ["add", n];
const mul = (n: number): Step => ["mul", n];
const processes: Process[] = [
{ name: "A", steps: [add(1), add(1), add(1), add(1)], pc: 0, acc: 0 },
{ name: "B", steps: [add(2), mul(5), add(1)], pc: 0, acc: 0 },
{ name: "C", steps: [add(10), add(20), add(30)], pc: 0, acc: 0 },
];
const ready = [...processes];
let tick = 0;
for (let p = ready.shift(); p !== undefined; p = ready.shift()) {
let { pc, acc } = p; // load the saved registers into the CPU
console.log(`load ${p.name} (pc ${pc}, acc ${acc})`);
for (let i = 0; i < SLICE; i++) {
if (pc === p.steps.length) break;
const [op, n] = p.steps[pc];
acc = execute(op, n, acc);
pc += 1;
console.log(`tick ${tick}: ${p.name} ${op} ${n} -> acc ${acc}`);
tick += 1;
}
Object.assign(p, { pc, acc }); // save them back into the process's record
if (pc === p.steps.length) {
console.log(`${p.name} finished with acc ${acc} (alone: ${runAlone(p.steps)})`);
} else {
console.log(`save ${p.name} (pc ${pc}, acc ${acc})`);
ready.push(p);
}
} In TypeScript: A labelled tuple type names the two parts of a step; add and mul build the steps, so each program fits on one line.
Recorded with TypeScript 6.0.3 (strict), run by Node.js 24.21.0 on macOS 26 arm64. It runs in your browser with QuickJS (quickjs-emscripten 0.32.0) after Sucrase 3.35.1. To run it yourself: npx -p typescript@6.0.3 tsc --noEmit --strict main.ts && node main.ts
Go 1.27 · main.go
package main
import "fmt"
const slice = 2 // steps a program may run before the kernel takes the CPU back
type step struct {
op string
n int
}
type process struct {
name string
steps []step
pc int // saved registers: the next step and the accumulator
acc int
}
func execute(op string, n, acc int) int {
if op == "add" {
return acc + n
}
return acc * n
}
func runAlone(steps []step) int {
acc := 0
for _, s := range steps {
acc = execute(s.op, s.n, acc)
}
return acc
}
func main() {
processes := []*process{
{name: "A", steps: []step{{"add", 1}, {"add", 1}, {"add", 1}, {"add", 1}}},
{name: "B", steps: []step{{"add", 2}, {"mul", 5}, {"add", 1}}},
{name: "C", steps: []step{{"add", 10}, {"add", 20}, {"add", 30}}},
}
ready := append([]*process(nil), processes...)
tick := 0
for len(ready) > 0 {
p := ready[0]
ready = ready[1:]
pc, acc := p.pc, p.acc // load the saved registers into the CPU
fmt.Printf("load %s (pc %d, acc %d)\n", p.name, pc, acc)
for range slice {
if pc == len(p.steps) {
break
}
s := p.steps[pc]
acc = execute(s.op, s.n, acc)
pc++
fmt.Printf("tick %d: %s %s %d -> acc %d\n", tick, p.name, s.op, s.n, acc)
tick++
}
p.pc, p.acc = pc, acc // save them back into the process's record
if pc == len(p.steps) {
fmt.Printf("%s finished with acc %d (alone: %d)\n", p.name, acc, runAlone(p.steps))
} else {
fmt.Printf("save %s (pc %d, acc %d)\n", p.name, pc, acc)
ready = append(ready, p)
}
}
} In Go: The ready queue is a slice of pointers, and ready[1:] drops the front entry. for range slice runs the slice's two steps.
Recorded with Go 1.27.1 on macOS 26 arm64. It runs in your browser with the Go compiler in WebAssembly 1.27.1. To run it yourself: go run main.go
Swift 6.4 · main.swift
let slice = 2 // steps a program may run before the kernel takes the CPU back
struct Step {
let op: String
let n: Int
init(_ op: String, _ n: Int) {
self.op = op
self.n = n
}
}
struct Process {
let name: Character
let steps: [Step]
var pc = 0 // saved registers: the next step and the accumulator
var acc = 0
}
func execute(_ op: String, _ n: Int, _ acc: Int) -> Int {
op == "add" ? acc + n : acc * n
}
func runAlone(_ steps: [Step]) -> Int {
steps.reduce(0) { acc, step in execute(step.op, step.n, acc) }
}
var processes = [
Process(name: "A", steps: [Step("add", 1), Step("add", 1), Step("add", 1), Step("add", 1)]),
Process(name: "B", steps: [Step("add", 2), Step("mul", 5), Step("add", 1)]),
Process(name: "C", steps: [Step("add", 10), Step("add", 20), Step("add", 30)]),
]
var ready = Array(processes.indices)
var tick = 0
while !ready.isEmpty {
let i = ready.removeFirst()
let p = processes[i]
var (pc, acc) = (p.pc, p.acc) // load the saved registers into the CPU
print("load \(p.name) (pc \(pc), acc \(acc))")
for _ in 0..<slice {
if pc == p.steps.count {
break
}
let step = p.steps[pc]
acc = execute(step.op, step.n, acc)
pc += 1
print("tick \(tick): \(p.name) \(step.op) \(step.n) -> acc \(acc)")
tick += 1
}
(processes[i].pc, processes[i].acc) = (pc, acc) // save them back into the process's record
if pc == p.steps.count {
print("\(p.name) finished with acc \(acc) (alone: \(runAlone(p.steps)))")
} else {
print("save \(p.name) (pc \(pc), acc \(acc))")
ready.append(i)
}
} In Swift: Process is a struct, a value type, so the loop works on a copy and writes the registers back through processes[i].
Recorded with Apple Swift 6.4 on macOS 26 arm64. It runs in your browser with a WebAssembly build on the WASI runner (browser_wasi_shim 0.4.2). To run it yourself: swiftc -O main.swift -o main && ./main
Ruby 4.0 · main.rb
# frozen_string_literal: true
SLICE = 2 # steps a program may run before the kernel takes the CPU back
# pc and acc are the saved registers: the next step and the accumulator.
Task = Struct.new(:name, :steps, :pc, :acc)
def execute(operation, number, acc)
operation == 'add' ? acc + number : acc * number
end
def run_alone(steps)
steps.reduce(0) { |acc, (operation, number)| execute(operation, number, acc) }
end
tasks = [
Task.new('A', [['add', 1], ['add', 1], ['add', 1], ['add', 1]], 0, 0),
Task.new('B', [['add', 2], ['mul', 5], ['add', 1]], 0, 0),
Task.new('C', [['add', 10], ['add', 20], ['add', 30]], 0, 0)
]
ready = tasks.dup
tick = 0
until ready.empty?
task = ready.shift
pc = task.pc # load the saved registers into the CPU
acc = task.acc
puts "load #{task.name} (pc #{pc}, acc #{acc})"
SLICE.times do
break if pc == task.steps.size
operation, number = task.steps[pc]
acc = execute(operation, number, acc)
pc += 1
puts "tick #{tick}: #{task.name} #{operation} #{number} -> acc #{acc}"
tick += 1
end
task.pc = pc # save them back into the process's record
task.acc = acc
if pc == task.steps.size
puts "#{task.name} finished with acc #{acc} (alone: #{run_alone(task.steps)})"
else
puts "save #{task.name} (pc #{pc}, acc #{acc})"
ready.push(task)
end
end In Ruby: Ruby's own Process module is taken, so a program's record is a Struct named Task. times runs the slice, and break leaves it early.
Recorded with CRuby 4.0.7 on macOS 26 arm64. It runs in your browser with ruby.wasm 2.10.1 (Ruby 4.0.0). To run it yourself: ruby main.rb
Kotlin 2.4 · Main.kt
const val SLICE = 2 // steps a program may run before the kernel takes the CPU back
data class Step(
val op: String,
val n: Int,
)
class Process(
val name: Char,
val steps: List<Step>,
) {
var pc = 0 // saved registers: the next step and the accumulator
var acc = 0
}
fun execute(
op: String,
n: Int,
acc: Int,
): Int = if (op == "add") acc + n else acc * n
fun runAlone(steps: List<Step>): Int = steps.fold(0) { acc, (op, n) -> execute(op, n, acc) }
fun main() {
val processes =
listOf(
Process('A', listOf(Step("add", 1), Step("add", 1), Step("add", 1), Step("add", 1))),
Process('B', listOf(Step("add", 2), Step("mul", 5), Step("add", 1))),
Process('C', listOf(Step("add", 10), Step("add", 20), Step("add", 30))),
)
val ready = ArrayDeque(processes)
var tick = 0
while (ready.isNotEmpty()) {
val p = ready.removeFirst()
var pc = p.pc // load the saved registers into the CPU
var acc = p.acc
println("load ${p.name} (pc $pc, acc $acc)")
for (turn in 0 until SLICE) {
if (pc == p.steps.size) break
val (op, n) = p.steps[pc]
acc = execute(op, n, acc)
pc += 1
println("tick $tick: ${p.name} $op $n -> acc $acc")
tick += 1
}
p.pc = pc // save them back into the process's record
p.acc = acc
if (pc == p.steps.size) {
println("${p.name} finished with acc $acc (alone: ${runAlone(p.steps)})")
} else {
println("save ${p.name} (pc $pc, acc $acc)")
ready.addLast(p)
}
}
} In Kotlin: Kotlin's standard library has its own ArrayDeque, which the Wasm build can use too; Step is a data class, so (op, n) takes it apart.
Recorded with Kotlin 2.4.20 on Java 25.0.4.1 on macOS 26 arm64. It runs in your browser with a WebAssembly build on the WASI runner (browser_wasi_shim 0.4.2). To run it yourself: kotlinc Main.kt -include-runtime -d main.jar && java -jar main.jar
Dart 3.13 · main.dart
import 'dart:collection';
const slice = 2; // steps a program may run before the kernel takes the CPU back
class Process {
Process(this.name, this.steps);
final String name;
final List<(String, int)> steps;
int pc = 0; // saved registers: the next step and the accumulator
int acc = 0;
}
int execute(String op, int n, int acc) => op == 'add' ? acc + n : acc * n;
int runAlone(List<(String, int)> steps) =>
steps.fold(0, (acc, step) => execute(step.$1, step.$2, acc));
void main() {
final processes = [
Process('A', [('add', 1), ('add', 1), ('add', 1), ('add', 1)]),
Process('B', [('add', 2), ('mul', 5), ('add', 1)]),
Process('C', [('add', 10), ('add', 20), ('add', 30)]),
];
final ready = Queue.of(processes);
var tick = 0;
while (ready.isNotEmpty) {
final p = ready.removeFirst();
var (pc, acc) = (p.pc, p.acc); // load the saved registers into the CPU
print('load ${p.name} (pc $pc, acc $acc)');
for (var i = 0; i < slice; i++) {
if (pc == p.steps.length) break;
final (op, n) = p.steps[pc];
acc = execute(op, n, acc);
pc += 1;
print('tick $tick: ${p.name} $op $n -> acc $acc');
tick += 1;
}
p.pc = pc; // save them back into the process's record
p.acc = acc;
if (pc == p.steps.length) {
print('${p.name} finished with acc $acc (alone: ${runAlone(p.steps)})');
} else {
print('save ${p.name} (pc $pc, acc $acc)');
ready.addLast(p);
}
}
} In Dart: Steps are records such as ('add', 1), taken apart by a pattern, and Queue from dart:collection is the ready queue.
Recorded with Dart 3.13.5 on macOS 26 arm64. It runs in your browser with a WebAssembly build by dart2wasm 3.13.5. To run it yourself: dart run main.dart
What every language printed
load A (pc 0, acc 0) tick 0: A add 1 -> acc 1 tick 1: A add 1 -> acc 2 save A (pc 2, acc 2) load B (pc 0, acc 0) tick 2: B add 2 -> acc 2 tick 3: B mul 5 -> acc 10 save B (pc 2, acc 10) load C (pc 0, acc 0) tick 4: C add 10 -> acc 10 tick 5: C add 20 -> acc 30 save C (pc 2, acc 30) load A (pc 2, acc 2) tick 6: A add 1 -> acc 3 tick 7: A add 1 -> acc 4 A finished with acc 4 (alone: 4) load B (pc 2, acc 10) tick 8: B add 1 -> acc 11 B finished with acc 11 (alone: 11) load C (pc 2, acc 30) tick 9: C add 30 -> acc 60 C finished with acc 60 (alone: 60)
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs. Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs. Runs on this device: a JavaScript build of this program (about 11 KB), made when the page was built. The first run also downloads the JavaScript runner (about 0.6 MB), which is kept for the next runs. To change the code, copy it and run it with the command below. Runs on this device: a WebAssembly build of this program (about 153 KB), made when the page was built. Edit runs your changes in the browser: the first edited run downloads the C and C++ compiler (about 29.1 MB), which is kept for the next runs. It is best on a computer. Runs on this device, in your browser. The first run downloads the C and C++ compiler (about 29.1 MB), which is kept for the next runs. It is best on a computer. Runs on this device: a .NET build of this program (about 4 KB), made when the page was built. The first run also downloads .NET (about 3.6 MB), which is kept for the next runs. Edit runs your changes in the browser: the first edited run downloads .NET (about 8.9 MB), which is kept for the next runs. Runs on this device, in your browser. The first run downloads .NET (about 8.9 MB), which is kept for the next runs. Runs on this device: a WebAssembly build of this program (about 8 KB), made when the page was built. Edit runs your changes in the browser: the first edited run downloads the C and C++ compiler (about 29.1 MB), which is kept for the next runs. It is best on a computer. Runs on this device, in your browser. The first run downloads the C and C++ compiler (about 29.1 MB), which is kept for the next runs. It is best on a computer. Runs on this device, in your browser. The first run downloads PHP (about 4.3 MB), which is kept for the next runs. Runs on this device: a WebAssembly build of this program (about 20 KB), made when the page was built. To change the code, copy it and run it with the command below. Runs on this device, in your browser. The first run downloads the JavaScript runner (about 0.9 MB), which is kept for the next runs. Runs on this device: a WebAssembly build of this program (about 718 KB), made when the page was built. Edit runs your changes in the browser: the first edited run downloads the Go compiler (about 18.7 MB), which is kept for the next runs. Runs on this device, in your browser. The first run downloads the Go compiler (about 18.7 MB), which is kept for the next runs. Runs on this device: a WebAssembly build of this program (about 1.7 MB), made when the page was built. To change the code, copy it and run it with the command below. Runs on this device, in your browser. The first run downloads Ruby (about 10.2 MB), which is kept for the next runs. Runs on this device: a WebAssembly build of this program (about 185 KB), made when the page was built. To change the code, copy it and run it with the command below. Runs on this device: a WebAssembly build of this program (about 11 KB), made when the page was built. To change the code, copy it and run it with the command below.Your run, in this browser
The 10 ticks interleave the three programs, yet each finish line shows the same total twice: A ends at 4, B at 11 and C at 60, exactly what each reaches when it runs alone. Saving and restoring the registers is what makes the sharing invisible to the programs. A real kernel adds two things the model leaves out: a timer that interrupts the CPU when a slice is over, so a program cannot keep the CPU by never stopping (the next lesson), and the cost of every switch, which the context-switch lesson measures.
Checkpoint
Check: Sharing the CPU in time and memory in space
The kernel checks every request
Abstractions alone do not stop a program from reaching past them. Protection is the third job: every request that
touches something outside the program goes to the kernel, which decides whether this program, run by this user, may
do it. The program below asks for four things and prints each answer. The first two use kill with signal 0, which
POSIX calls the null signal: the kernel performs every check of a real signal and sends nothing.
#include <errno.h>
#include <fcntl.h>
#include <signal.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#if defined(__APPLE__)
static const char *const DISK = "/dev/disk0";
#else
static const char *const DISK = "/dev/sda";
#endif
static void report(const char *request, int result) {
if (result == 0) {
printf("%-38s allowed\n", request);
} else {
printf("%-38s refused: %s\n", request, strerror(errno));
}
}
int main(void) {
report("may I signal my own process?", kill(getpid(), 0));
report("may I signal process 1?", kill(1, 0));
report("make me the superuser (user ID 0)", setuid(0));
int fd = open(DISK, O_RDONLY);
report("let me read the disk, past every file", fd >= 0 ? 0 : -1);
if (fd >= 0) {
close(fd);
}
return 0;
} Output
may I signal my own process? allowed may I signal process 1? refused: Operation not permitted make me the superuser (user ID 0) refused: Operation not permitted let me read the disk, past every file refused: Permission denied
Recorded with Apple clang version 21.0.0 (clang-2100.3.34.2) on macOS 26 arm64. To run it yourself: clang -std=c23 -O2 -Wall -Wextra -Werror -pedantic -fsanitize=address,undefined kernel_says_no.c -o kernel_says_no && ./kernel_says_no
The kernel allowed the first request, because a process may always signal itself. It refused the other three. Process
1 belongs to the system, so an ordinary user may not signal it. Only a privileged process may make itself the
superuser. And the device file of the whole disk, /dev/disk0 on the recorded Mac (the program opens /dev/sda on
Linux), holds every user’s files and is readable only by privileged users, so a program cannot read raw blocks to get
round each file’s permissions.
Two layers make these refusals stick. The processor runs programs in an unprivileged mode, in which instructions that control the hardware do not work, so the only way to touch a device or another process is to ask the kernel (the lesson on user mode: The processor's unprivileged mode, in which applications run. Instructions that control the hardware fail there, so a program has to ask the kernel for such work. More in “User mode, kernel mode and traps” shows the processor refusing). And every address a program uses falls inside its own address space. Because those address spaces are private, Windows’ documentation sums up the result in one line: “one application can’t modify another application’s data.”
Treating a refusal as a crash
A refused request is an answer, not a failure of the program: kill returned −1 and set errno to EPERM, and
the program went on to its next request. Code that ignores the return value reads garbage later. Check every call
and handle the refusal, as kernel_says_no.c does.
Checkpoint
Check: The kernel checks every request
Where the kernel ends and the rest begins
Not everything called the operating system runs in the kernel. Most of it is ordinary programs and libraries, and the boundary between the two is the system call: A request from a program to the kernel, made with a trap instruction that switches the processor into kernel mode at an entry point the kernel chose. More in “System calls: the kernel's front door” interface:
Where the kernel sits among the software of a computer
Text description of the diagram
The figure is a single column of five boxes, read from top to bottom, with an arrow from each box to the one below it.
- Applications, shells and services run in user mode. They call functions of the layer below.
- The C library and the language runtimes also run in user mode, inside each program. When a request needs the kernel, they execute a trap instruction.
- The system-call interface is the only way a program can ask the kernel for a service. It passes a request on only after checking it.
- The kernel runs in kernel mode and holds the scheduler, memory management, file systems, the network stack and the device drivers.
- The kernel drives the hardware, the cores, memory, disks and network card, through the devices' registers.
Code above the interface runs in user mode, inside processes the kernel protects from each other. Code below it runs in kernel mode: The processor's privileged mode, in which the kernel runs. Code in kernel mode may execute every instruction and reach all memory and devices. More in “User mode, kernel mode and traps”, privileged, in one address space shared by the whole kernel. Here is where some everyday pieces live:
| Piece of software | Where it runs | Why |
|---|---|---|
printf |
C library, user mode | It formats text in the program’s memory, then calls write |
write |
The kernel does the work | The C library’s write only passes the request through the system-call interface |
malloc |
C library, user mode | It hands out memory from a pool and asks the kernel for more only when the pool runs out |
| The page-fault handler | Kernel | A fault traps into the kernel, which finds or loads the missing page |
| Most device drivers | Kernel, on Linux and Windows | They program the hardware directly; a few drivers run as user processes |
The shell (bash, zsh) |
User mode | An ordinary program that starts other programs |
PID 1 (systemd, launchd) |
User mode | The first process; the kernel starts it, then it starts the services |
| The scheduler | Kernel | It decides which program runs next on each core |
The names add to the confusion. Strictly, Linux is a kernel. Debian’s documentation puts it in one sentence: “In short, Linux is the kernel of a Unix-like operating system.” A distribution: A complete operating system built around a kernel: the kernel with a C library, a shell, an init system, tools and packages, as Debian and Fedora are built around Linux. such as Debian or Fedora builds a complete operating system around it, with a C library, a shell, an init system, tools and thousands of packages.
Checkpoint
Check: Kernel, libraries and applications
The operating-system families in use today
These are the families a developer meets most often. Each one is a kernel and the system built around it:
| Family | Kernel | Where it runs |
|---|---|---|
| Linux | Linux | Servers and cloud machines, Android phones (Android’s kernel is based on a long-term-support Linux release), embedded boards, and the containers on a Linux host, which share its one kernel |
| Windows | Windows NT | PCs and Windows servers |
| macOS, iOS and their relatives | XNU, which its README calls a hybrid of the Mach kernel and parts of FreeBSD | Apple’s computers, phones and tablets |
| The BSDs | FreeBSD, OpenBSD, NetBSD | Servers, network appliances and the base of other systems |
| Real-time kernels | FreeRTOS, Zephyr and others | Microcontrollers in sensors, wearables and controllers, where timing must be predictable and memory is small |
Linux, NT and XNU are full kernels built for machines with a memory management unit (Linux keeps only limited support for running without one); the real-time kernels are small enough for chips with very little memory. Containers are a common source of confusion here: Docker’s documentation says a virtual machine brings a whole operating system with its own kernel, while containers all share the same kernel, so ten containers on one server still have one kernel under them.
Checkpoint
Check: Operating-system families and where they run
What the rest of this track covers
The three jobs organise everything that follows. Abstraction comes back as processes, threads, address spaces and files; allocation as scheduling, memory management and disk scheduling; protection as user and kernel mode, permissions, sandboxes and containers. The computer-science curriculum of the ACM, the IEEE Computer Society and AAAI, CS2023, lists the same ground in its operating-systems knowledge area, from the purpose and principles of operating systems to virtualization and fault tolerance, and the modules of this track follow it.
GATE CS
The GATE 2027 syllabus for Computer Science lists, under Operating System: system calls, processes, threads, inter-process communication, concurrency and synchronisation, deadlock, CPU and I/O scheduling, memory management and virtual memory, and file systems. Modules 1 to 10 of this track cover each of them; interrupts, DMA and caches belong to the syllabus’s computer organisation section, and the next lesson covers what an operating system needs from them.
Practise: write the kernel’s time-slice loop
Write the loop that the model above runs, for any programs and any length of slice, and run the sample tests. They include the lesson’s own run and the runs with slices of 1, 3 and 10 steps:
Exercise · Easy · Python
Write the kernel's time-slice loop
Write run_round_robin(programs, steps_per_slice) in timeslice.py. It runs programs the way the lesson's kernel does and returns what happened at every tick.
programsis a list of(name, steps)pairs. Each step is("add", n)or("mul", n), and every program's accumulator starts at 0.- Take the first program from the ready queue and run up to
steps_per_sliceof its steps, starting where it last stopped. If it still has steps left, put it at the back of the queue; otherwise it is finished. - Return a list with one
(name, acc)pair per tick: the program that ran and its accumulator after the step.
For example, run_round_robin([("A", [("add", 1), ("add", 1)]), ("B", [("mul", 3)])], 1) is [("A", 1), ("B", 0), ("A", 2)]. A program with no steps produces no ticks.
Starter code · timeslice.py
def run_round_robin(programs, steps_per_slice):
"""One (name, acc) pair per tick, as the kernel's time slices run the programs."""
# Replace this line with your code.
return [] The sample tests · test_timeslice.py
from timeslice import run_round_robin
LESSON = [
("A", [("add", 1), ("add", 1), ("add", 1), ("add", 1)]),
("B", [("add", 2), ("mul", 5), ("add", 1)]),
("C", [("add", 10), ("add", 20), ("add", 30)]),
]
def test_lesson_run():
"""gives the ten ticks of the lesson's run with slices of two steps"""
assert run_round_robin(LESSON, 2) == [
("A", 1), ("A", 2), ("B", 2), ("B", 10), ("C", 10),
("C", 30), ("A", 3), ("A", 4), ("B", 11), ("C", 60),
]
def test_slices_of_one_step():
"""takes turns after every step when a slice is one step"""
assert run_round_robin(LESSON, 1) == [
("A", 1), ("B", 2), ("C", 10), ("A", 2), ("B", 10),
("C", 30), ("A", 3), ("B", 11), ("C", 60), ("A", 4),
]
def test_long_slices():
"""runs each program to the end, in order, when a slice is longer than every program"""
assert run_round_robin(LESSON, 10) == [
("A", 1), ("A", 2), ("A", 3), ("A", 4), ("B", 2),
("B", 10), ("B", 11), ("C", 10), ("C", 30), ("C", 60),
]
def test_slice_of_three():
"""finishes B in one slice of three steps and still gives A its last step"""
assert run_round_robin(LESSON, 3) == [
("A", 1), ("A", 2), ("A", 3), ("B", 2), ("B", 10),
("B", 11), ("C", 10), ("C", 30), ("C", 60), ("A", 4),
]
def test_programs_without_steps():
"""produces no ticks for a program with no steps, and none at all for no programs"""
assert run_round_robin([], 2) == []
assert run_round_robin([("E", [])], 2) == []
assert run_round_robin([("E", []), ("F", [("mul", 4), ("add", 3)])], 1) == [("F", 0), ("F", 3)]
def test_same_result_as_alone():
"""ends every program with the total it reaches when it runs alone"""
last = {}
for name, acc in run_round_robin(LESSON, 2):
last[name] = acc
assert last == {"A": 4, "B": 11, "C": 60} A hint
Keep each program's record as a list [name, steps, pc, acc] in a collections.deque, so that popleft() takes the front one. Inside a slice, run for _ in range(steps_per_slice) and break as soon as pc == len(steps); after the slice, append the record again only if pc < len(steps).
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.
The questions below cover the whole lesson and start with the idea you are least sure of:
Practice that adapts to you
Practise this lesson
11 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
The first question an interviewer is likely to ask on this topic comes with a full answer below. Harder ones on the same ideas, why one address can hold two values and which library calls need the kernel, are in this module’s question bank.
-
Fresher · Warm-up
What is the difference between an operating system and a kernel?
The kernel is the part of the operating system that runs in the processor's privileged mode. It schedules processes, manages memory, runs the file systems, the network stack and most drivers, and answers system calls. The operating system is the kernel plus everything around it that runs in user mode: the C library and other system libraries, the first process (systemd or launchd) and the services it starts, a shell and the basic tools. Strictly, Linux is only a kernel; Debian, Fedora and Android each build a complete operating system around it. A strong answer names what decides which side a piece belongs to: kernel code is privileged and shares one address space, so a bug there can bring down the whole machine, while everything else runs as processes that the kernel protects from one another.
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Fresher to mid
After fork(), a parent and its child print the same address for a variable but different values at it. Explain how both can be true, and what happens in the hardware when the child writes to the variable.
-
Fresher to mid
Which of these can a library do without entering the kernel, and which need a system call: strlen, malloc, printf, reading the current time, opening a file? Defend each answer.
Key takeaways
- An operating system does three jobs: it hides shared hardware behind abstractions, shares the hardware between programs, and protects programs from each other and the machine from all of them.
- The kernel builds processes from the cores, address spaces from memory, files from disk blocks and sockets from the network card, so the same address in two processes held 1 in one and 101 in the other.
- The CPU is shared in time and memory in space; saving and restoring each program’s registers made three interleaved programs end with exactly the totals they reach alone.
- Every request that reaches beyond a program goes to the kernel, which refused to let an ordinary process signal process 1, become the superuser or read the raw disk.
- The kernel is only the privileged part: the C library, the shell and even PID 1 run in user mode, and a Linux distribution builds a whole operating system around the Linux kernel.
Terms in this lesson
- address space
- The range of memory addresses a process can use. The kernel maps it onto physical memory with page tables, so the same address in two processes can hold different values.
- distribution
- A complete operating system built around a kernel: the kernel with a C library, a shell, an init system, tools and packages, as Debian and Fedora are built around Linux.
- kernel
- The part of an operating system that runs in the processor's privileged mode. It owns the hardware, shares it between programs and answers their system calls.
- process
- A running program together with what the kernel keeps for it: an address space, saved registers, open files and an owner. Each process runs as if it had the computer to itself.
- time slice
- The stretch of CPU time a program may run before the kernel takes the processor back and gives another program a turn.
References
- intro(2): introduction to system calls (Linux man-pages project (man7.org))
- fork(2): create a child process (Linux man-pages project (man7.org))
- kill: send a signal to a process or a group of processes (POSIX.1-2024) (The Open Group and IEEE)
- malloc(3): allocate and free dynamic memory (Linux man-pages project (man7.org))
- User mode and kernel mode (Windows drivers) (Microsoft)
- The Linux kernel: what is Linux? (admin guide README) (The Linux Kernel Organization)
- The Debian FAQ: definitions and overview (The Debian Project)
- No-MMU memory mapping support (The Linux Kernel Organization)
- Android kernel overview (Android Open Source Project)
- XNU kernel source and README (Apple (open-source distributions))
- FreeRTOS, a real-time operating system for microcontrollers (FreeRTOS project)
- Introduction to the Zephyr project (Zephyr Project (Linux Foundation))
- What is a container? (Docker)
- Computer Science Curricula 2023: Operating Systems knowledge area (Version Gamma) (ACM, IEEE Computer Society and AAAI (CS2023 Task Force))
- GATE 2027 syllabus for Computer Science and Information Technology (CS) (Indian Institute of Technology Madras (GATE 2027))
Report a problem with this lesson
Kept only in this browser. Your Learn progress