|
AquilaOS documentation
How it works, and its code
|
Most of what a kernel does has nothing to do with the CPU it runs on. Processes, the scheduler's choices, files, path lookup, pipes, sockets, TCP: these are the same on a PC, a phone or a mainframe. A smaller part can't be written without knowing the machine: how to switch from one thread to another, how page tables look, how an interrupt arrives, how the machine boots. When you're learning, the two are easy to mix up, because the books and the code show them side by side.
AquilaOS keeps them apart, and so do these documents. This one explains where the line is and how it's drawn in the code; the others describe each subsystem's design first, the same on every machine, then how the machine-specific part does its share, in sections whose titles say which machine it is.
| Layer | What it is | Where |
|---|---|---|
| The kernel | the design: processes, memory, files, devices, networking | kernel/core, mm, sys, fs, dev, net |
| The architecture | the CPU: its modes, registers, page tables, traps, context switch | kernel/arch/i386/ (boot, cpu, earlycon, mm, sys), kernel/arch/aarch64/ |
| The platform | the machine around the CPU: how it boots, its interrupt controllers, timers, firmware tables | kernel/arch/i386/platform/, kernel/arch/aarch64/platform/ |
aarch64 is a second implementation of both: the architecture in kernel/arch/aarch64/, and QEMU's virt machine as its platform (see "What a new architecture needs" below).
Drivers sit in between. A driver in kernel/dev/ is generic code for a particular device (an e1000 network card, an ATA disk, an 8250 serial port), which could be on any machine that has the device; it reaches the hardware through the architecture's I/O functions (io_in8() and the others, cpu/io.h) at the addresses the platform gives it, and through interrupts.
A driver hardcodes neither where its device is nor which interrupt it raises; it's told:
This is Linux's platform devices and the BSDs' hints, cut down to a name lookup: no matching, no probing order, no hotplug. Linux can also read the table from the devicetree; AquilaOS's tables are static.
The kernel and the architecture meet in three ways.
Every function the kernel needs from the machine is declared in kernel/include/, mostly in core/arch.h, and called arch_*, or is one of the pmap_* functions of mm/pmap.h. A few are declared next to what uses them: arch_copy_user() in sys/uaccess.h, arch_cpu_kick() in sys/cpu.h, arch_lock_spin() and arch_lock_taken() in sys/lock.h, and the interrupts' irq_install() and irq_uninstall() in dev/irq.h. An architecture implements all of them except arch_msi_alloc(), which is optional (a weak symbol: aarch64 has none):
| What | Functions |
|---|---|
| processes and threads | arch_proc_init(), arch_proc_fork(), arch_thread_create(), arch_kthread_create(), arch_thread_kill(), arch_exec_start() |
| running threads | arch_thread_spawn(), arch_thread_switch(), arch_idle(), arch_sleep(), arch_yield(), arch_cur_thread_kill(), arch_sched_init() |
| system calls | arch_syscall_return(): a call's result, into the thread's registers |
| signals | arch_sigcode_map(), arch_sigreturn(): the handler's frame, and back |
| page tables | pmap_create(), pmap_switch(), pmap_add(), pmap_remove(), pmap_protect(), pmap_page_copy(), ...; arch_mm_setup(), arch_page_get_mapping() |
| physical memory and devices | arch_phys_map(), arch_mmio_map(), arch_msi_alloc() |
| user memory | arch_copy_user(), arch_copy_user_str(): copies that stop at a fault |
| several CPUs | arch_cpu_kick(), arch_lock_spin(), arch_lock_taken() |
| time | arch_rtime_ns() (since boot), arch_time_get() (the date), arch_cycles() |
| the machine | arch_reboot(), arch_halt(), arch_poweroff(), arch_stack_trace() |
The names say what the kernel wants, not how: arch_thread_switch() runs another thread, and the kernel doesn't care that on x86 it means saving registers on the kernel stack, loading CR3 and setting the TSS's stack pointer.
Some things the kernel needs are types and constants, not functions: how big a physical address is, where user space ends, what a CPU's machine-specific state is. These come from headers the architecture overrides. The include path lists the ABI headers first, then the platform's headers, then the architecture's, then the kernel's:
(That's aarch64's. The PC has no directory of platform headers: its headers are in the architecture's, core/platform.h and platform/misc.h.)
So #include <mm/mm.h> finds kernel/arch/<arch>/include/mm/mm.h first, which defines what is the architecture's and then includes the kernel's own with #include_next <mm/mm.h>. The headers completed this way:
| Header | What the architecture adds |
|---|---|
| config.h | ARCH_BITS (32 or 64), the machine's name for uname |
| core/system.h | PAGE_SIZE, and how much a page table maps |
| core/arch.h | struct x86_thread (a thread's registers), USER_STACK, KERN_STACK_SIZE, interrupts on and off |
| cpu/cpu.h | struct arch_cpu (a CPU's GDT, TSS, page tables), curcpu() |
| cpu/io.h | io_in8(), io_out8() and the others: a device's registers, ports or memory |
| mm/mm.h | paddr_t, vaddr_t, the kernel's address space layout, PHYS2VIRT() |
| mm/pmap.h | struct pmap, an address space's page tables |
| boot/boot.h | the boot loader's information to struct boot |
| core/panic.h | panic() (replaced whole) |
The generic structures carry a pointer or a member for the architecture's part and leave it opaque: a thread's arch (struct x86_thread), a CPU's arch (struct arch_cpu), an address space's pmap.
The other way round, the architecture calls into the kernel when the machine does something:
| When | The kernel's function |
|---|---|
| boot is done | kmain(struct boot *) |
| a system call | syscall_dispatch(): the handler in syscall_table[], by number |
| a page fault | mm_page_fault(), with the address and what was tried |
| the timer's tick | sched_timer_tick() (the boot CPU), sched_cpu_tick() (the others) |
| a switch is due | schedule() |
| on the way back to user mode | signal_take(): a signal to deliver |
Everything in between is the architecture's: which vector a page fault is, where the faulting address is found (CR2), what the error code means. The kernel gets an address and "write, from user mode".
To run AquilaOS on another CPU, one would write a kernel/arch/<name>/ with:
kernel/arch/aarch64/ is the second one: the CPU's part, and QEMU's virt machine as its platform (platform/virt/: the GIC, the generic timer, the PL031 clock, PSCI, the devicetree, and its devices' table, the PL011 UART).
The separation is the design, but it isn't finished; these are the places where x86 still shows through in generic code:
When reading the code, a name tells you the layer: arch_* is the interface, x86_* and aarch64_* are the architecture's or platform's own, anything else in kernel/arch/ is too, and everything outside kernel/arch/ is the kernel.
| File | What |
|---|---|
| kernel/include/core/arch.h | the arch_* interface |
| kernel/include/mm/pmap.h | the page table interface |
| kernel/include/sys/uaccess.h | user memory, over arch_copy_user() |
| kernel/include/sys/cpu.h | struct cpu, with the architecture's part |
| kernel/include/boot/boot.h | struct boot: what the kernel gets at boot |
| kernel/arch/i386/include/ | x86's headers |
| kernel/arch/i386/{boot,cpu,mm,sys}/ | x86: boot, CPU, paging, threads and traps |
| kernel/arch/i386/earlycon/ | x86's early console |
| kernel/arch/i386/platform/ | the PC (pc/, misc/) |
| kernel/arch/aarch64/ | aarch64, and QEMU's virt machine (platform/virt/) |
| kernel/include/dev/irq.h | devices' interrupts |
| kernel/include/dev/platform.h | the platform's devices, its table |
| kernel/Makefile | the include path |
| configs/ | the build configurations |