|
AquilaOS documentation
How it works, and its code
|
A machine with several CPUs that share the same memory, and are all equal, is a symmetric multiprocessor (SMP). Each CPU runs its own thread at the same time as the others, so two programs really run at once. For the kernel this raises two problems:
This document describes how the kernel solves both, and then how x86 starts the other CPUs and lets them talk to each other; aarch64 runs on one CPU so far (see The kernel and the machine for the split).
Every CPU has a struct cpu (kernel/include/sys/cpu.h), in the array cpus[]; cpus[0] is the one that booted:
curcpu() returns the running CPU's, however the architecture finds it. curthread and curproc, used all over the kernel, are its running thread and that thread's process: per CPU, not global. Each CPU also has its own idle stack, where it waits when there's nothing to run.
There are at most 32 CPUs (CPUS_MAX): sets of CPUs are 32-bit masks.
The classic answers to two CPUs in the kernel are:
AquilaOS takes the second: the kernel lock (sys/lock.c). A CPU takes it when it enters the kernel from user mode or from its idle loop (kernel_enter()), and lets it go when it goes back to either (kernel_leave()). An exception in the kernel finds the lock already held by its CPU and does nothing.
The lock belongs to the CPU, not to a thread. A thread that sleeps in the kernel doesn't let it go: its CPU, still in the kernel, runs the next thread; and if there's none, its idle loop lets the lock go. A thread woken later may run on another CPU, which has the lock because it entered the kernel to run it. "Holding the lock" means "this CPU is in the kernel".
It's a ticket lock: a CPU takes a number and waits until it's served, so the CPUs get it in the order they asked. While a CPU waits, it spins with interrupts off; it still does what the CPU holding the lock asks of the others (arch_lock_spin(): flush its TLB, below), because that CPU waits for it.
Why one lock: the kernel was written for one CPU, and its central rule, that what runs in the kernel isn't interrupted, carries over whole. No structure needs its own lock, and none of the code had to change for SMP. It costs scalability: programs that compute run on all CPUs at once, but programs that make many system calls queue for the kernel. UnixBench's index on x86_64 under KVM went from 2249 on one CPU to 2973 on four, not four times as much. For a kernel meant to be read, that's the right trade.
There is one ready queue for all the CPUs (see Processes and threads). A CPU whose thread sleeps or whose time slice is over takes the next ready thread, whichever CPU it last ran on: no CPU has its own queue, and a thread has no preferred CPU.
When a process exits or execs, its other threads must end (proc_threads_end(), in sys/proc.c). A thread that is sleeping or ready is freed at once. One that is running on another CPU can't be: that CPU is in user mode, on the thread's stack. So the thread is marked thread->killed, and its CPU is kicked (sched_thread_kick()). When it enters the kernel, which it does at once, it finds the mark and the thread frees itself (sched_thread_killed()), from the CPU's idle stack. If what made it enter was a device interrupt, the interrupt is handled first.
A CPU caches address translations in its TLB, and doesn't notice when the page tables change. When the kernel unmaps a page, or makes it read-only, the CPU that did it flushes its own TLB, but another CPU running a thread of the same process could keep using the old translation, and write to a page that was freed and given to someone else.
So when a mapping changes, the CPUs that may have it must flush it before the change is done: a TLB shootdown. The pmap does it for every change (tlb_shootdown()), and knows which CPUs to ask: each address space keeps the set of CPUs it's loaded on (pmap->cpus, kept by pmap_switch()). The CPU sends them a message and waits until each has flushed. The others do it without the kernel lock, which is held by the CPU waiting for them: from the message's interrupt, or, if they're spinning for the lock with interrupts off, from the spin.
The kernel's own mappings are in every address space, so every CPU may have them. But a CPU without the lock only touches memory that was mapped before it took the lock last and stays mapped (its stacks, its struct cpu, the lock): so they don't need a message. Each change to the kernel's mappings counts one more generation (kernel_gen), and a CPU that takes the lock flushes its TLB if the generation moved since it last did (arch_lock_taken()).
The shootdown itself is the architecture's (mm/tlb.c): what a TLB is and how to flush it are x86's. But the rules above are what any architecture would follow.
Anything a CPU keeps in its registers for a thread has the same problem. On x86, the FPU's state is switched lazily (a thread's first FPU instruction after a switch traps, see Interrupts); each CPU remembers whose state its FPU has (arch.fpu_thread). A thread switched out has its state saved first (x86_fpu_switch_out()), so it can go on on any CPU. Any architecture with lazily switched state has to do the same; aarch64 switches its FP/SIMD registers lazily too, and each CPU remembers whose they are (arch.fp_thread).
On x86, curcpu() reads the struct cpu pointer at gs:0 (kernel/arch/i386/include/cpu/cpu.h):
Each CPU has its own GDT and TSS (a TSS is busy on the CPU that loaded it, and holds the kernel stack the CPU switches to on an interrupt from user mode), in struct arch_cpu, with its local APIC's ID and its loaded page tables.
At reset only one CPU runs, the bootstrap processor; the others, the application processors (APs), wait for a message. x86_smp_init() (cpu/smp.c) starts them at boot, unless smp=off, as the Intel manual describes:
The APs wait idle until the scheduler starts: arch_sched_init() kicks each one, and the first wake IPI starts its timer (x86_lapic_timer_cpu_start()). The PIT times the waits, which is why platform_init() starts the CPUs after it (see Booting).
Without an APIC (apic=off), there's no way to start them: AquilaOS runs on one CPU.
aarch64 runs on the boot CPU only: the other CPUs of the virt machine are never started. Its curcpu() reads tpidr_el1, which aarch64_init() sets to cpus[0] (kernel/arch/aarch64/include/cpu/cpu.h). The kernel lock is taken and let go as on x86, but nothing ever waits for it, so the architecture's side has nothing to do: arch_cpu_kick(), arch_lock_spin() and arch_lock_taken() are empty (sys/smp.c), and tlb_shootdown() flushes this CPU's TLB only (mm/pmap.c).
The local APIC sends the inter-processor interrupts (IPIs), to a CPU by its APIC ID (x86_lapic_ipi()):
| Vector | Sent by | What the CPU does |
|---|---|---|
| 0xF1 | tlb_shootdown() | flush the range asked (invlpg, or reload CR3 past 32 pages), or switch to the kernel's tables if the address space is going away; without the kernel lock |
| 0xF2 | arch_cpu_kick() | enter the kernel: run a ready thread, take a signal, end a killed thread; with the kernel lock |
The TLB message is handled in __x86_irq_handler() before the lock is taken: the sender holds it. A request is one at a time (only the CPU with the lock sends them): its pages, and a mask of the CPUs that haven't done it yet, which each clears.
| File | Layer | What |
|---|---|---|
| kernel/include/sys/cpu.h | kernel | struct cpu, cpus[] |
| kernel/sys/lock.c, kernel/include/sys/lock.h | kernel | the kernel lock |
| kernel/sys/sched.c | kernel | ticks, waking idle CPUs, killed threads |
| kernel/sys/proc.c | kernel | ending a process's threads (proc_threads_end()) |
| kernel/arch/i386/include/cpu/cpu.h | x86 | struct arch_cpu, curcpu() |
| kernel/arch/i386/cpu/gdt.c | x86 | each CPU's GDT, TSS and gs |
| kernel/arch/i386/cpu/smp.c | x86 | starting the APs, arch_cpu_kick() |
| kernel/arch/i386/cpu/trampoline.S | x86 | from real mode to the kernel |
| kernel/arch/i386/mm/tlb.c | x86 | TLB shootdown, arch_lock_spin(), arch_lock_taken() |
| kernel/arch/i386/cpu/isr.c, platform/misc/irq.c | x86 | taking the lock on the way in |
| kernel/arch/i386/platform/misc/apic.c | PC | the local APIC: IPIs, each CPU's timer |
| kernel/arch/aarch64/include/cpu/cpu.h | aarch64 | struct arch_cpu, curcpu() |
| kernel/arch/aarch64/sys/smp.c | aarch64 | the lock's and the kick's hooks, empty |