Entry
The CPU lands here. Each hart sets up its own stack and enters early machine-mode setup.
- Stack pointer is set to the base of
stack0 - This hart’s ID is read from the hardware
- Offset is computed as
(hartid + 1) × 4096 - Stack pointer is advanced by that offset, landing at the top of this hart’s slot
- Early machine-mode setup is entered.
- A spin loop catches any unexpected return.
All harts now have a valid stack in M-mode. This must happen before any C code runs.
Machine Mode
The CPU arrives in M-mode. The hardware is configured for supervisor mode, then mret is used to drop into it by pre-loading the target privilege in mstatus.MPP and the target address in mepc before firing it.
mstatus.MPPis set to supervisor somretdrops to S-modemepcis set to the kernel initialization entry point somretjumps there- Paging is disabled
- All interrupts and exceptions are delegated to supervisor mode
- External and timer interrupts are enabled for supervisor mode
- PMP is configured to give supervisor access to all physical memory
- Timer interrupts are routed to supervisor mode via
mie; since they are a machine-mode feature, they must be configured here before the mode switch - The sstc extension is enabled via
menvcfg, allowing S-mode to usestimecmpdirectly; before sstc, S-mode had no timer register of its own and M-mode had to forward timer events as software interrupts mcounterenbit 1 is set, allowing S-mode to readtime;timeis a hardware counter in the CLINT that increments continuously at a fixed frequency, when it reaches the value instimecmpthe hardware fires a supervisor timer interrupt- The first timer interrupt is scheduled via
stimecmp - The hart ID is stored in
tpso later per-CPU code can identify the current hart mretfires, dropping the CPU to S-mode and jumping to the kernel initialization entry point
After mret the CPU is in S-mode at the kernel initialization entry point with paging still off; all hardware is wired for supervisor mode and kernel initialization begins.
Kernel Init
All harts arrive at the kernel initialization path after mret, but only CPU 0 runs the full initialization sequence. Other secondary harts spin on the started flag until CPU 0 finishes, then run a smaller per-hart setup. All harts enter the scheduler at the end.
CPU 0 runs these in order:
consoleinitandprintfinit— console and printf, first safe point for kernel outputkinit— physical page allocatorkvminitandkvminithart— kernel page table built and paging turned onprocinit— process table and kernel stackstrapinitandtrapinithart— trap vectors installedplicinitandplicinithart— interrupt controller configuredbinit,iinit,fileinit— buffer cache, inode table, open file tablevirtio_disk_init— disk driveruserinit— first user process createdstarted = 1— signals secondary harts to proceed
Secondary harts then run kvminithart, trapinithart, and plicinithart for their own per-hart setup.
Without a barrier, the compiler or hardware could reorder CPU 0’s writes, leaving secondary harts with a partially initialized kernel when they see started = 1.
consoleinit
Sets up the console path so the kernel can print and processes can read terminal input.
cons.lockprotects console input shared by readers and interrupts.- Input is line-buffered before readers receive it.
- UART becomes the terminal hardware interface.
- UART setup enables serial I/O and interrupts.
tx_lockprotects UART transmit state.- The transmit lock lets writers sleep until hardware is ready.
- Console read/write are registered as device operations.
printfinit
Sets up the lock used by kernel printf.
pr.lockserializes formatted kernel output.- The lock prevents console text from different CPUs from interleaving.
- Kernel messages use the console output path initialized earlier.
- Panic output bypasses normal lock waiting.
- After a panic, other CPUs stop producing UART output.
After this, kernel printf calls can safely produce readable boot messages even when multiple harts are running.
kinit
Sets up the physical page allocator.
kmem.lockis initialized for allocator synchronization.- The initial free range runs from
endtoPHYSTOP. endis the first address after the kernel image.PHYSTOPis the top of physical memory xv6 will manage.- The start of the range is rounded up to a page boundary.
- Each complete 4096-byte page in that range is added to the freelist.
- The freelist becomes the pool used for later physical-page allocation.
After this, the kernel can allocate whole physical pages for page tables, kernel stacks, user memory, pipe buffers, and disk structures.
kvminit
Builds the kernel page table, but does not turn paging on yet.
- Creates the shared kernel page table.
- Allocates and clears the root page-table page.
- Adds device regions, kernel sections, trampoline code, and guarded kernel stacks.
- Stores the completed kernel page table for later per-hart installation.
- Paging is still off until each hart installs this page table.
After this, the kernel virtual address space is described, but the CPU still uses physical addresses until each hart installs the page table into satp.
kvminithart
Turns on paging for the current hart.
- Each hart has its own
satp, so each installs the kernel page table separately. - A fence orders page-table writes before the switch.
satpenables Sv39 translation through the kernel page table.- A second fence clears stale address translations after the switch.
After this, the hart uses the kernel virtual address space. Direct-mapped kernel addresses keep working because most kernel virtual addresses equal their physical addresses.
procinit
Initializes the process table.
- Prepares the fixed process table; no user process exists yet.
pid_lockprotects process-ID allocation.wait_lockprotects parent-child coordination and prevents lost wakeups inwait/exit.- Each process slot gets its own
p->lock. - Each slot starts in the
UNUSEDstate. - Each slot records the virtual address of its already mapped kernel stack.
After this, process slots can be safely claimed later when the first process or forked children are created.
trapinit
Initializes the global timer tick lock.
- The global tick counter tracks timer ticks since boot.
tickslockprotects the shared tick counter.- Timer interrupts update the counter and wake sleepers.
- Time-related system calls read or sleep on the counter.
- The trap vector itself is not installed here.
- Per-hart trap-vector setup happens later.
After this, the global tick counter has a lock, but traps are not yet routed to a handler.
trapinithart
Installs the kernel trap entry point for the current hart.
stvecis the supervisor trap-vector register.- Each hart has its own
stvec, so each hart installs its trap entry separately. - Kernel-mode traps are routed to the kernel trap-entry code.
- This setup covers interrupts and exceptions that occur while already in supervisor mode.
After this, kernel-mode interrupts and exceptions on this hart have a valid entry path.
plicinit
Globally enables the external interrupt sources xv6 cares about.
- PLIC means Platform-Level Interrupt Controller.
- The PLIC routes external device interrupts.
- xv6 uses it for UART and virtio disk interrupts.
- UART and virtio are given nonzero priorities.
- A zero-priority interrupt source is disabled.
- Per-hart enablement and threshold setup happen later.
- Claim and completion are part of interrupt handling, not initialization.
After this, UART and virtio interrupts are enabled at the PLIC source-priority level, but each hart still needs to opt in through plicinithart.
plicinithart
Enables PLIC delivery for the current hart.
- PLIC delivery is configured per hart.
- This hart enables supervisor-mode UART and virtio disk interrupts.
- This hart’s priority threshold is set to allow those enabled sources through.
- Interrupts still arrive through the normal supervisor trap path.
After this, the current hart can receive external interrupts from the UART and virtio disk through the PLIC.
binit
Initializes the buffer cache.
- The buffer cache holds RAM copies of disk blocks and synchronizes shared block access.
bcache.lockprotects cache-wide metadata and the LRU list.- Each buffer gets a sleeplock for its block contents.
- The fixed buffer array is linked into an LRU list.
- The list starts empty of disk contents but ready for block reuse.
After this, buffer metadata is ready; actual disk I/O still waits for virtio setup.
iinit
Initializes the in-memory inode table.
- The inode table caches active in-memory inodes.
itable.lockprotects inode-table allocation and lookup metadata.- Each inode gets a sleeplock for its own contents.
- The spinlock covers short table operations.
- The sleeplocks cover longer inode operations that may sleep.
- No inode contents are read from disk here.
After this, the inode cache is ready; actual inode contents are loaded later on demand.
fileinit
Initializes the global open-file table.
- The file table is the kernel-wide pool of open file objects.
ftable.lockprotects file allocation and reference counts.- Per-process file descriptors later point into this global table.
- No files are opened or allocated here.
After this, the global open-file table is ready.
virtio_disk_init
Initializes the virtio block-device driver for fs.img.
- Virtio MMIO is the disk control interface; block data moves through RAM buffers.
vdisk_lockprotects virtio driver state.- The driver verifies that the device is a QEMU virtio block device.
- The device is reset and feature negotiation is completed.
- Queue 0 is prepared as the request queue.
- Descriptor, available, and used-ring pages are allocated and cleared.
- Queue addresses and size are given to the device.
- Descriptors start marked free for later disk requests.
- The device is marked ready, with completion interrupts handled through the PLIC path.
After this, filesystem block reads and writes can reach fs.img through the virtio disk driver.
userinit
Creates the first process slot and makes it schedulable.
- Claims one unused process slot.
- The allocation path prepares PID state, trapframe storage, a minimal user page table, and an initial kernel context.
- The process is recorded as the special init process.
- Its current directory is set to the root directory.
p->lockprotects the slot while its state changes toRUNNABLE.- Releasing
p->lockmakes the process visible to the scheduler.
After this, xv6 has one runnable process slot that will become /init when the scheduler runs it.
scheduler
Starts the per-CPU scheduling loop.
- Every hart enters its own scheduler loop.
- The loop never returns.
- The scheduler scans process slots for
RUNNABLEwork. p->lockprotects each slot while its state is inspected or changed.- A chosen process becomes
RUNNINGand receives the CPU through a kernel context switch. - When that process yields, sleeps, or exits, control returns to the scheduler.
- If no process is runnable, the hart waits for an interrupt.
After this, CPU time is driven by scheduler loops rather than the boot sequence.