From Transistor to Browser

The Software · Chapter 9
Who is in charge?

Updated Oct 12, 2026
Summarize
Transcript

Who is in charge? Our CPU runs one program. Your phone runs dozens at once, on the same few cores.

I figured the processor really runs them side by side. But it's only got one counter and one accumulator. That's room for one program at a time.

So something takes turns: A program called the operating system runs each one for a few steps: a quantum. Then it saves that program's context: its counter, its accumulator, its memory. It loads the next program's context, and lets that one run. The adder runs three steps, then the pair runs three, then back again. Neither can tell it was ever paused. The adder still gets forty-two, and the pair gets nine.

The operating system's in charge, taking turns really fast! Saving and restoring each program's context lets one machine pretend to be many.

Slide the quantum below, and run both programs. The turns change, but do the answers?

The hosts' voices are synthetic, made with Breeze TTS 2 by BreezeBlue.

  1. The browser
  2. The network
  3. Software and languages
  4. The CPU
  5. Memory
  6. Arithmetic
  7. Logic gates
  8. The switch
Figure 9.1: Where we are in the climb from transistor to browser: on the software rung. This chapter builds the operating system that shares one processor among many programs.

The expression compiler used the CPU of Part I; the loop compiler then used a larger, named-memory model to focus on jumps. This chapter returns to the original sixteen-word CPU and programs assembled for it. Until now, each run has had its processor to itself. Real machines run many programs at once, and one of them is special: the operating system, the program in charge. It is a program like any other, but a privileged one. It decides which program runs, and it shares the single processor among all of them. This chapter builds a tiny operating system: a scheduler that runs several programs on one CPU by letting each take a turn.

Two ideas make that possible. Time-sharing gives each program the processor for a brief slice and then moves on, so all of them make progress. The context switch is how the slices are kept separate: there is only one set of registers, so before running the next program the operating system saves the current one’s state and restores the next one’s.

9.1 A process is a saved context

A running program is called a process. What makes it a process, rather than just a program text, is its state: where it is in its execution, what its registers hold, what its memory contains. That state is its context. For the machine of Chapter 5 the context is exactly four things: the program counter, the accumulator, the memory, and whether the program has halted. Save those four and you can stop a process cold; restore them and it resumes as if nothing happened.

Example 9-1. A process: a program plus its saved context

class Process:
    """A program together with its saved processor state.

    That state, the program counter, the accumulator, the memory, and a halted
    flag, is the process's context: everything the operating system must
    remember to pause the program and later resume it exactly where it left off.
    """

    def __init__(self, name: str, program: list[int]) -> None:
        self.name = name
        start = CPU(program)  # a CPU lays out the initial memory and registers1
        self.pc = start.pc
        self.acc = start.acc
        self.memory = start.memory
        self.halted = start.halted2

Because each process owns its context, switching between processes is a matter of swapping contexts in and out of the one processor. Figure 9.2 shows the arrangement: the processor has a single set of registers, and the scheduler loads one process’s context into it at a time, saving the previous one first.

context-switchcontext-switch
Figure 9.2: One processor, many contexts. The processor has a single set of registers. Each process keeps its own saved context. To run a process the scheduler restores its context into the processor; to pause it, the scheduler saves the context back. Only one context is loaded at a time.

9.2 Taking turns

The scheduler is the heart of the operating system. This one uses the simplest fair policy: round-robin. It keeps a list of processes and a single shared CPU. It gives the first process a fixed positive number of steps, called a quantum, then saves its context, restores the next process, and gives that one a quantum, and so on around the list, skipping any process that has halted, until all have finished.

Example 9-2. The round-robin scheduler

class Scheduler:
    """A round-robin scheduler over one shared processor.

    It runs each unhalted process for a quantum of steps, then switches to the
    next, saving and restoring each process's context across the switch, until
    every process has halted.
    """

    def __init__(self, processes: list[Process], quantum: int = 3) -> None:
        if quantum <= 0:
            raise ValueError("quantum must be positive")
        self.processes = processes
        self.quantum = quantum
        self.cpu = CPU([0])  # the one shared processor
        self.trace = []      # (process name, steps run) for each time slice

    def _restore(self, proc: Process) -> None:
        """Load a process's saved context into the shared processor."""
        self.cpu.pc = proc.pc
        self.cpu.acc = proc.acc
        self.cpu.memory = proc.memory
        self.cpu.halted = proc.halted

    def _save(self, proc: Process) -> None:
        """Save the shared processor's state back into the process's context."""
        proc.pc = self.cpu.pc
        proc.acc = self.cpu.acc
        # memory is already this process's list, restored by reference above;
        # reassigning it keeps save and restore symmetric.
        proc.memory = self.cpu.memory
        proc.halted = self.cpu.halted

    def run(self) -> None:
        """Run every process to completion, a quantum of steps at a time.

        This scheduler has no runaway guard: a process whose
        program never halts would loop here forever.
        """
        while not all(proc.halted for proc in self.processes):
            for proc in self.processes:
                if proc.halted:
                    continue
                self._restore(proc)1
                steps = 0
                while steps < self.quantum and not self.cpu.halted:2
                    self.cpu.step()
                    steps += 1
                self._save(proc)3
                self.trace.append((proc.name, steps))

The quantum is the whole of the policy. A small quantum switches often and feels responsive; a large one switches rarely and wastes less time on the bookkeeping of saving and restoring. Choosing it well is a real concern for real schedulers. The mechanism, though, is just save, restore, and a loop.

The quantum does force a switch partway through: a process that has not halted is stopped mid-run the moment its steps are spent. That preemption comes free here only because the CPU is simulated one step at a time, so the scheduler counts steps from outside the loop. A real operating system needs a timer interrupt to regain control from a core that is actually running. This toy swaps a reference to a process’s whole memory list; a real system switches its address-space mapping. Neither copies the whole memory on each switch. What this scheduler lacks is any guard against a program that never halts: such a process would spin the scheduler forever.

9.3 Time-sharing, traced

Running ftb.scheduler multitasks two programs, both assembled by the Chapter 6 assembler. One sums three numbers to 42; the shorter one sums two numbers to 9. With a quantum of three steps they interleave on the single processor.

output · ftb.scheduler
One processor, shared in turns (quantum = 3 steps):
slice  process  steps
---------------------
  0    adder    3
  1    pair     3
  2    adder    3
  3    pair     2

adder: halted, accumulator holds 42
pair: halted, accumulator holds 9

Read the trace as a timeline, which Figure 9.3 draws. The adder runs three steps, then the pair runs three, then the adder runs three more and finishes, and finally the pair runs its last two and finishes too. The processor was never doing two things at once. It was doing one thing at a time, in turns, fast enough that both programs ran to completion.

timelinetimeline
Figure 9.3: The processor’s time, shared. Each slice is one quantum given to one process; the width is the number of steps it ran. The ticks are context switches. The adder finishes in its second slice, after which the pair gets the processor to itself for its final slice.

The interactive version needs the in-browser runtime.

The crucial fact is the result. The adder’s accumulator holds 42 and the pair’s holds 9, exactly what each computes when run alone. Interleaving changed nothing about either answer. Because the context switch saves and restores every register faithfully, neither process can tell it was ever paused, and neither can disturb the other. That transparency is what lets one machine pretend to be many.

Sharing the machine is half the job. The next chapter models the request path: syscalls into kernel services. Later, virtual memory and page tables explain the mapping needed for private memory protection. These are separate models of operating-system mechanisms; they are not wired into this scheduler.

Exercises