Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

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.

  • Beginner
  • 15 minutes
  • Examples run with Python 3.14.8, Pyodide 314.0.7, Clang 21.0.0 and GCC 16.2.0
  • By MySmartCoPilot
Free lesson, Pro answers: 15 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

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:

Four pieces of shared hardware on the left, each turned by the kernel into an abstraction that every program gets for itself.CPU cores:a handful, sharedProcess:a CPU of its own(time slices)Physical memory:one, sharedAddress space:memory of its own(page tables)Disk:numbered blocksFiles:named byte streams(file system)Network card:raw framesSockets: bytestreams to others(network stack)

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.

  1. 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.
  2. 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.
  3. A disk of numbered blocks becomes files: named streams of bytes, built by the file system.
  4. 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:

Same address, two values: fork gives the child a memory of its own C · own_memory.c
#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

  1. Question 1 of 2 In own_memory.c, counter is 100 before the fork. The child sets it to 1, then the parent adds 1 to its own counter. What does the parent read?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: 101

    fork gives the child a separate memory space with the same content as the parent's. The child's counter becomes 1, the parent's goes from 100 to 101, and both still use the same address for it, because the address is only a name inside each process's own address space.

  2. Question 2 of 2 Which abstraction lets a program use memory address 4096 without knowing where in the physical memory its data really is?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: The address space

    The kernel builds four abstractions out of shared hardware: processes from the cores, address spaces from the physical memory, files from the disk's blocks and sockets from the network card. The address space is the one that makes addresses private.

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:

Interactive · step by step Three programs take turns on one CPU Step through the run of the program below. Each record on the left keeps a program's registers while another program has the CPU.
Three programs take turns on one CPU, step 11 of 11. Showing Program A (finished: acc 4), Program B (finished: acc 11), Program C (finished: acc 60), Kernel (ready: empty), CPU (idle).Program Afinished: acc 4Program Bfinished: acc 11Program Cfinished: acc 60Kernelready: emptyCPUidle

The drawing shows the whole scene; its 11 steps are listed below.

Every step as text
  1. 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.
  2. Load A. The kernel takes A from the front of the ready queue and copies A's saved registers into the CPU.
  3. 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.
  4. 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.
  5. 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.
  6. B runs two steps. Tick 2 adds 2 and tick 3 multiplies by 5, which leaves 10 in the accumulator.
  7. Save B. Two steps are used up, so B's registers go back into its record, and B joins the queue behind A.
  8. 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.
  9. 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.
  10. 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.
  11. 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:

One program · 14 languages Three programs sharing one CPU, two steps at a time

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)

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

  1. Question 1 of 2 The model runs A (4 steps), B (3 steps) and C (3 steps) with slices of 2 steps and takes 10 ticks. How many ticks does it take with slices of 1 step?

    Type a number.

    A hint

    Shorter slices change the order in which steps run, not how many steps there are.

    Show the answer: Question 1 of 2

    Answer: 10 ticks

    Every tick runs one step of one program, and the three programs have 4 + 3 + 3 = 10 steps between them. The slice decides how often the kernel switches between them, so the order changes but the count stays 10. In a real kernel each switch also costs time of its own, which the context-switch lesson measures.

  2. Question 2 of 2 After B and C have used the CPU, A continues at its third step with acc 2. Why?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: The kernel saved A's pc and acc in A's record and copied them back into the CPU before A's next slice

    There is one set of registers in the CPU. The kernel copies a program's registers into its record when its slice ends and copies them back when its next slice starts, so each program sees an unbroken run of its own.

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.

Four requests to the kernel, and its answers C · kernel_says_no.c
#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

  1. Question 1 of 2 kernel_says_no.c asked to open /dev/disk0, the whole disk, and was refused. What would a program that could read the whole disk be able to skip?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: The file system's permission checks on every file, because it would read the blocks directly

    The device file of a whole disk is readable only by privileged users (its owner is root on the recorded Mac), so the kernel refused the open with "Permission denied". Every program has to go through the file system, which checks each file's permissions.

  2. Question 2 of 2 The program called kill(1, 0) and got "Operation not permitted". What does signal 0 do?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: It sends nothing; the kernel only checks that the process exists and that the caller may signal it

    With sig 0, kill performs every check a real signal would get and sends nothing. The kernel allowed the check on the program's own process and refused it on process 1, which belongs to the system, not to this user.

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:

A stack from applications and the C library in user mode, through the system-call interface, to the kernel and the hardware.Applications, shellsand services(user mode)C library andlanguage runtimes(user mode)System-call interface:the only way to askKernel: scheduler, memory,file systems, network,drivers (kernel mode)Hardware: cores, memory,disks, network cardfunction callsa trap instructionchecked requestdevice registers

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.

  1. Applications, shells and services run in user mode. They call functions of the layer below.
  2. 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.
  3. 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.
  4. The kernel runs in kernel mode and holds the scheduler, memory management, file systems, the network stack and the device drivers.
  5. 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

  1. One question Which of these run inside the kernel, in the processor's privileged mode, on Linux or macOS?

    Choose every answer that is right.

    Show the answer: One question

    Answer:

    • The handler that runs on a page fault
    • The scheduler that picks the next program to run

    The kernel holds the scheduler, memory management (page faults included), file systems, the network stack and the drivers. Everything else, from the C library to the shell and the first process, runs in user mode and enters the kernel only through system calls.

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

  1. One question Which kernel runs on an Android phone?

    Choose one answer.

    Show the answer: One question

    Answer: Linux, in a version based on one of its long-term-support releases

    Android builds a different operating system around the Linux kernel: its own libraries, runtime and app model on top, which is why Android and a Linux server feel nothing alike while sharing a kernel.

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.

  • programs is 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_slice of 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).

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.

  1. Question 1 of 11 Which of these are jobs of every operating system?

    Choose every answer that is right.

    Show the answer: Question 1 of 11

    Answer:

    • Stopping programs from touching each other's memory or the hardware directly
    • Hiding hardware behind abstractions such as processes, address spaces and files
    • Sharing the CPU, memory and devices between programs

    Abstraction, resource allocation and protection are the three jobs. A compiler, an editor or a browser is an application that relies on them.

  2. Question 2 of 11 Two processes print the same address for a variable. A colleague concludes that they share the variable. What is the flaw in that conclusion?

    Choose one answer.

    Show the answer: Question 2 of 11

    Answer: An address names a place in one process's own address space, so equal addresses in two processes can refer to different memory

    Memory can be shared between processes, but only when the kernel maps the same physical memory into both. The numbers a program sees are virtual addresses, and the same one means different memory in different processes.

  3. Question 3 of 11 The kernel gives the CPU to programs in turns and gives memory to them in separate pieces. What are these two ways of sharing called?

    Choose one answer.

    Show the answer: Question 3 of 11

    Answer: Sharing in time for the CPU, and sharing in space for memory

    The CPU is multiplexed in time (time slices), memory and disk in space (separate frames and blocks for each program). Both together let a few cores and one memory serve many programs.

  4. Question 4 of 11 The same three programs run with slices of 3 steps instead of 2. Which program finishes first?

    Choose one answer.

    Show the answer: Question 4 of 11

    Answer: B, at the end of its first slice

    With slices of 3: A runs ticks 0 to 2 and is saved with one step left; B runs ticks 3 to 5 and finishes; C runs ticks 6 to 8 and finishes; A finishes at tick 9. The exercise's tests contain this run.

  5. Question 5 of 11 kill(getpid(), 0) returned 0 and kill(1, 0) returned −1 with "Operation not permitted". What did the kernel decide?

    Choose one answer.

    Show the answer: Question 5 of 11

    Answer: The program may signal itself but not process 1

    POSIX's kill reports EPERM when the caller has no permission to signal any receiving process, and the null signal 0 asks exactly that question without sending anything.

  6. Question 6 of 11 A cheat program wants to read the memory of a game running beside it. What stops it on Linux, macOS or Windows?

    Choose one answer.

    Show the answer: Question 6 of 11

    Answer: Its addresses reach only its own address space, and asking the kernel to read another process needs a permission it lacks

    Each process sees only its own address space, and every route into another process (debugging interfaces, shared memory) passes through the kernel, which checks the caller's rights first. Windows' documentation states the same rule: one application cannot modify another application's data.

  7. Question 7 of 11 printf("hi\n") ends with the kernel writing two bytes. Which part does the work of formatting the text?

    Choose one answer.

    Show the answer: Question 7 of 11

    Answer: The C library, in user mode, inside the program

    Libraries do as much as they can in user mode, where calls are cheap, and enter the kernel only for what needs it: here, writing bytes to an open file.

  8. Question 8 of 11 Strictly speaking, what does the name Linux refer to?

    Choose one answer.

    Show the answer: Question 8 of 11

    Answer: The kernel; a distribution such as Debian adds the C library, the shell, the services and the packages

    The kernel is what Linus Torvalds started and what kernel.org publishes. Debian, Fedora, Ubuntu and Android each build a complete operating system around it.

  9. Question 9 of 11 A program calls malloc(16) a million times. Roughly how often does that enter the kernel?

    Choose one answer.

    Show the answer: Question 9 of 11

    Answer: Rarely; malloc hands out pieces of memory it already holds and asks the kernel for more only when its pool runs out

    Library code does the cheap, frequent work in user mode and calls the kernel for the rare, privileged part: here, mapping more memory into the process. The memory-allocators lesson builds such a pool.

  10. Question 10 of 11 Five containers run on one Linux server. How many kernels are running?

    Choose one answer.

    Show the answer: Question 10 of 11

    Answer: One; containers share the host's kernel

    Containers isolate processes with kernel features (namespaces and cgroups, in the containers module) but share one kernel, which is why a Linux container needs a Linux kernel underneath.

  11. Question 11 of 11 A battery-powered sensor uses a small microcontroller with no memory management unit. Which family of operating system fits it?

    Choose one answer.

    Show the answer: Question 11 of 11

    Answer: A small real-time kernel such as FreeRTOS or Zephyr

    Operating systems come in sizes. Real-time kernels such as FreeRTOS and Zephyr schedule tasks on microcontrollers with very little memory, while Linux, NT and XNU expect a memory management unit and far more memory.

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.

  1. 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.

  1. 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.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. 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.

    Model answer, Pro. First step and follow-ups

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

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.