kernel/main.c
About this file
main is where every hart arrives in supervisor mode, after start's mret.
It brings up the kernel in a fixed order and then hands each CPU to the
scheduler, which never returns.
The work is split. Hart 0 initializes everything that exists once for the whole machine:
the console, the page allocator, the kernel’s page table, the process table, the
disk, the first process. Every other hart waits until hart 0 says it is done, then
configures only its own per-CPU hardware. A shared flag, started, carries the “done”
signal safely from one hart to the others.
The list of calls on lines 14–31 doubles as a table of contents for the kernel: each call is the entry point of one subsystem, and following them one by one is a good way to tour xv6.
Read before: kernel/start.c. Read next: kernel/vm.c (the page tables set up
here) or kernel/proc.c (userinit and scheduler).
Headers
The same xv6 headers as kernel/start.c. kernel/defs.h declares every function
called below.
The "initialization finished" flag
Hart 0 sets started to 1 when it has finished the machine-wide setup; the other
harts wait for that. static keeps the name private to this file.
volatile tells the compiler not to assume the value stays the same between
reads. The real guarantee comes from the atomic built-ins on lines 33
and 35, which also make sure hart 0’s other writes are visible before the flag is.
A file-private int that starts at 0; written once by hart 0, read in a loop by the
others.
main(): supervisor mode, all CPUs
start set mepc to this function’s address, so mret lands here on
every hart, in supervisor mode, with paging off and each hart still on its own slice
of stack0. A kernel’s main takes no arguments and returns nothing (no one could
receive a return value), which is why the Makefile passes -Wno-main;
-ffreestanding (there is no hosted C library) also means main is not special.
Only hart 0 initializes the machine
cpuid reads the hart ID that start stored in tp. The hart whose
ID is 0 does the one-time setup; this is decided by ID, not by which hart gets here
first.
The console comes first so that the rest of the boot can print messages, including
the error message if anything below calls panic. consoleinit sets up the
UART and connects it to the console device; printkinit creates the lock
that keeps lines printed by different harts from being mixed together. The three
printk calls produce the banner you see when xv6 boots.
Is this hart number 0? cpuid returns the value in tp.
Set up the console: initialize the UART (uartinit) and route read and
write on the console device to consoleread and consolewrite.
Initialize the lock used by printk.
Print an empty line. printk is the kernel’s own printf; the kernel cannot use the C
library’s.
The first message you see when xv6 boots.
Memory
These three calls take the kernel from raw physical memory to running with virtual memory on:
kinitmakes every 4096-byte page betweenend(the end of the kernel image) andPHYSTOPavailable tokalloc.kvminitbuilds the kernel’s page table (kvmmake). It maps the kernel’s code, data and RAM at addresses equal to their physical addresses, plus the device registers, the trampoline page, and a kernel stack for each process.kvminithartwrites that table’s address into satp, turning address translation on for this hart. The next instruction still works because the kernel’s code is mapped at the same address it already had.
Hand all free physical memory to the page allocator.
Build the kernel page table; its address goes in kernel_pagetable.
Turn on paging on this hart.
Processes, traps and interrupts
procinitinitializes the process table: a lock per entry, every entry markedUNUSED, and the virtual address of each process’s kernel stack.trapinitinitializes the lock protecting the tick counter.trapinithartpoints stvec atkernelvec, the handler for traps that happen while the kernel itself is running.plicinitgives the UART and disk interrupts a non-zero priority in the PLIC (zero would mean “never deliver”).plicinithartasks the PLIC to deliver those two interrupts to this hart.
Calls ending in hart configure the current hart only, which is why the other harts
repeat them on lines 39–41.
Initialize the process table.
Initialize the lock for the clock-tick counter.
Route traps that occur in the kernel to kernelvec.
Set PLIC priorities for the UART and disk interrupts.
Enable those interrupts for this hart.
Files and the disk
binitsets up the buffer cache, the kernel’s in-memory copies of disk blocks.iinitsets up the table of in-memory inodes (files currently in use).fileinitsets up the table of open files.virtio_disk_initinitializes the virtio disk device, which holdsfs.img.
Nothing is read from the disk yet. Reading the file system needs a process context
(it may have to sleep while waiting for the disk), so it happens later, in
forkret (kernel/proc.c:528).
Initialize the buffer cache.
Initialize the inode table.
Initialize the open-file table.
Initialize the virtio disk driver.
The first process
userinit creates the first process and marks it runnable. It does not run
yet. When a scheduler first picks it, it starts in forkret, which initializes the
file system and loads the program /init (kernel/proc.c:532). /init
(user/init.c) then starts the shell.
Create the first process. It runs once a scheduler picks it.
Tell the other harts
Set started to 1 with release ordering: every memory write hart 0 made above
(the page table, the process table, the locks…) is guaranteed to be visible to any
hart that sees started == 1 through an acquire load. Without this, another hart
could see the flag before the page table it is about to use. See atomic operation and memory ordering.
Publish started = 1 so the other harts can continue. __atomic_store_n is a GCC
built-in; __ATOMIC_RELEASE gives the ordering explained in the block note.
Every other hart
Harts 1, 2, … arrive here immediately and wait in a loop until hart 0 sets the flag.
Then each one announces itself (with the default 3 CPUs you see hart 1 starting and
hart 2 starting, in either order) and does the
per-hart part of the setup: turn on paging with the kernel page table hart 0 built,
install the kernel trap handler, and enable device interrupts from the PLIC.
Read started with acquire ordering, so that once this hart sees 1, it also sees
everything hart 0 wrote before setting it.
The empty loop body: keep re-reading until the flag is 1.
Announce this hart. With make qemu CPUS=2 this prints hart 1 starting.
Turn on paging on this hart, using the page table hart 0 built.
Install the kernel trap handler on this hart.
Enable UART and disk interrupts for this hart.
Become a scheduler
Every hart, including hart 0, ends here. scheduler loops forever, picking a
runnable process and running it. At first the only process is the one userinit
created, so one hart runs it while the others find nothing to do and wait. main
never returns.
Enter the scheduler. Never returns.
No stack switch happens here: scheduler runs on the same slice of stack0 that
main has used all along, and because it never returns, that slice is from now on this
hart’s scheduler stack. The frames of start and main stay at its top for good
(The stacks of xv6).