Glossary
Every term the notes rely on, in plain language. Hovering a RISC-V instruction, register or CSR in the code shows its entry.
Assembler directive
.align
Pads with zeros or no-ops until the address is a multiple of 2^n. On RISC-V, .align 4 means a multiple of 16 bytes.
.globl / .global
Makes a label visible to other files, so that the linker (ld) can connect it to C code that uses the same name. Without it a label is private to its file.
.section
Tells the assembler which section the following code or data belongs to, e.g. .section .text for code.
C
__attribute__
A GCC/Clang extension that attaches extra instructions for the compiler to a variable or function, e.g. __attribute__((aligned(16))) to place a variable at an address that is a multiple of 16.
atomic operation and memory ordering
An operation other harts can never observe half-done. Its “memory order” argument also controls when your other memory writes become visible to other harts, which matters when one hart prepares data and another waits for a flag saying it is ready.
Different harts may see each other’s writes late or in a different order than they were written, and the compiler may reorder memory accesses too. The GCC built-ins used by xv6 fix this:
__atomic_store_n(&flag, 1, __ATOMIC_RELEASE): every memory write this hart did before this store is visible to any hart that readsflag == 1with an acquire load.__atomic_load_n(&flag, __ATOMIC_ACQUIRE): once this load sees the new value, reads that come after it see everything the releasing hart wrote before its store.
Used as a pair, they make a one-way “ready” signal. See kernel/main.c.
compound literal
A C99 expression that creates an unnamed object on the spot: a type in parentheses followed by an initializer list, such as (char *[]){"/init", 0}, an array of two char * values.
It behaves like a variable you never had to name. Inside a function it has automatic
storage, like a local variable: it exists until the enclosing block ends. xv6 uses one in
forkret to build the argv array for kexec without declaring a separate array.
empty parentheses in a C declaration
In C before C23, void f(); declares f without saying anything about its parameters (unlike void f(void), which says “no parameters”). The compiler will not check calls to it.
format string
The first argument of a printf-style function: text printed as is, with % directives such as %d or %s, each of which consumes the next argument and prints it in the stated form.
In printk("hart %d starting\n", cpuid()), the text hart , starting and the
newline are printed literally, and %d is replaced by the next argument in decimal.
%% prints a single %.
Kernel printk understands %d, %ld, %lld, %u, %lu, %llu, %x, %lx,
%llx, %p, %c, %s and %%, with no widths, padding or precision; see
kernel/printk.c. User-space printf (user/printf.c) has its own,
similar set.
inline assembly (asm volatile)
A GCC/Clang extension that puts specific machine instructions in the middle of C code, e.g. asm volatile("mret"). volatile tells the compiler not to delete the instruction (or hoist it out of a loop) even if its result looks unused; it does not stop all reordering, which takes a "memory" clobber or a fence. xv6 uses inline assembly for things C cannot express, like reading CSRs.
static inline
A function defined static inline in a header gets a private copy in every file that includes it, and the compiler is invited to paste its body into each caller instead of making a call. For xv6’s one-instruction wrappers, a call becomes that one instruction.
static_assert
A check done by the compiler, not at run time: static_assert(condition, "message") stops the compilation with the message if the constant condition is false. Standard since C11 (as _Static_assert; <assert.h> defines the name static_assert). Since C23, static_assert is itself a keyword.
Compare assert(condition) from <assert.h>, which checks when the program runs and
aborts it if the condition is false. A static assertion costs nothing at run time and
catches the problem before the program can produce wrong output, but it only works for
conditions the compiler can evaluate, such as sizeof(int) == 4.
mkfs/mkfs.c uses one, and carries a fallback macro that is used wherever
static_assert is not already defined as a macro: with pre-C11 compilers, and with
GCC in C23 mode on glibc, where it is a keyword and not a macro.
variadic function
A C function that accepts a variable number of arguments, declared with ... after its fixed parameters, like printk(char *fmt, ...). It reads the extra arguments one by one with va_start, va_arg and va_end from <stdarg.h>.
The function itself cannot find out how many extra arguments there are or what their
types are; the caller must tell it some other way. printk learns both from its
format string: each %d means “the next argument is an int”.
va_list ap; // cursor over the extra arguments
va_start(ap, fmt); // start after the last named parameter
int x = va_arg(ap, int); // take the next one, as an int
va_end(ap); // finish
Arguments smaller than int (such as char) are promoted to int when passed, and
float to double. Calling va_arg with a type that does not match what was passed,
or reading more arguments than were passed, is undefined behavior: the compiler does
not check it. That is why xv6 marks printk with __attribute__((format(printf, 1, 2))),
which makes the compiler check calls against the format string.
volatile
A C qualifier meaning “this variable may change behind the compiler’s back, so read and write it in memory every time the code says to”. It stops the compiler from keeping the value in a register or deleting accesses it thinks are useless.
C idiom
designated initializer
An array or struct initializer that names the element it sets, as in [SYS_fork] = sys_fork or .x = 1. Elements not named are set to zero, and the array is made long enough for the largest index given.
Designated initializers come from C99. They let a table be written in terms of symbolic indices, so that the order of the lines does not matter and a gap in the numbering leaves a zero (null) entry instead of shifting everything after it:
static char *names[] = { [3] = "three", [1] = "one" }; // names[0] and names[2] are 0
function pointer
A variable holding the address of a function, so that which function runs can be decided at run time. Declared like uint64 (*f)(void) (“f is a pointer to a function taking no arguments and returning uint64”) and called as f().
A function’s name used without parentheses is its address, so f = sys_fork; stores a
pointer and f() calls sys_fork. An array of function pointers is a common way to
dispatch on a number, as syscalls does:
static uint64 (*syscalls[])(void) = { [SYS_fork] = sys_fork, ... };
p->trapframe->a0 = syscalls[num]();
A cast can turn an integer address into a function pointer, as
forkret does to call userret at its trampoline page address.
Compiler option
-c / -o / -E / -x / -S / -t
Common tool options, whose meaning depends on the tool. gcc: -c compile without linking, -o FILE name the output, -E preprocess only, -x c treat input as C. objdump: -S disassemble mixed with source, -t print the symbol table. qemu: -S start with the CPU paused (for a debugger).
-ffile-prefix-map
Replace the build directory’s full path with . wherever the compiler records file names (in debug info), so the output does not depend on where you built it.
-fno-builtin-NAME
Do not treat the function NAME as the standard C library function of that name. xv6 writes its own memset, printf, malloc…, which may behave differently, so the compiler must not substitute its own built-in version or assume standard behavior. (-ffreestanding already implies this for every function.)
-fno-common
Treat each uninitialized global variable (int x;) as a real definition, so that two files defining the same global is a link error instead of being silently merged.
-fno-omit-frame-pointer
Keep a frame pointer (register s0) in every function, so that a debugger or a backtrace routine can walk the chain of stack frames.
-fno-pie / -no-pie
Produce ordinary code for fixed addresses instead of “position-independent executable” code, which some Linux distributions’ compilers generate by default.
-fno-stack-protector
Do not add stack-overflow checking code; it would call library functions that a kernel does not have.
-g / -ggdb / -gdwarf-2
Include debugging information (which source line each instruction came from, variable names and types) for debuggers such as gdb, in the DWARF format.
-I.
Also search the top directory for #include "..." files. That is why user programs can write #include "kernel/types.h".
-march=rv64gc
Which instruction set to generate code for: 64-bit RISC-V (“rv64”) with the general extensions “g” (integer multiply/divide, atomics, floating point, CSR access, fences) and “c” (compressed 2-byte encodings of common instructions).
-mcmodel=medany
Generate code that reaches each symbol relative to the current instruction, so the program can sit anywhere in memory as long as it fits within 2 GiB. The default model, medlow, can only reach addresses between −2 GiB and +2 GiB, and the kernel starts at 0x80000000, exactly 2 GiB, just out of range.
-MD
While compiling x.c, also write x.d: a small makefile listing every header x.c included, so that make rebuilds x.o when any of those headers changes.
-nostdlib
When gcc links, do not add the standard C library or startup files. (xv6 links with ld directly, so this only documents the intent.)
-O
Turn on the basic level of optimization (same as -O1).
-std=gnu99
Compile the C99 language plus GNU extensions (such as asm and __attribute__).
-Wall / -Werror / -Wno-...
-Wall turns on most warnings and -Werror turns every warning into an error, so the build stops on any warning. -Wno-main silences complaints about main’s unusual signature; -Wno-unknown-attributes silences warnings from compilers that do not know the nonstring attribute used in kernel/fs.h:60.
Concept
16550 UART registers
The eight byte-wide registers through which software controls a 16550-compatible UART. Some offsets mean one register when read and another when written, and two change meaning while the divisor-latch bit is set.
On QEMU’s virt machine they sit at UART0 = 0x10000000 (register n at
0x10000000 + n), accessed with memory-mapped I/O. The ones xv6 uses, and
the bits it uses:
| Offset | Read | Write | Bits xv6 uses |
|---|---|---|---|
| 0 | RHR, receive holding: next received byte | THR, transmit holding: byte to send | |
| 1 | IER, interrupt enable | IER | bit 0 received data; bit 1 transmitter empty |
| 2 | ISR (also called IIR), interrupt status | FCR, FIFO control | FCR bit 0 enable FIFOs; bits 1, 2 reset them |
| 3 | LCR, line control | LCR | bits 1–0 word length (11 = 8 bits); bit 7 divisor latch access (DLAB) |
| 5 | LSR, line status | (not written) | bit 0 data ready; bit 5 transmit holding register empty |
While LCR bit 7 (DLAB) is 1, offsets 0 and 1 instead reach the low (DLL) and high (DLM)
bytes of the baud-rate divisor; the bit rate is the input clock divided by 16 × divisor.
xv6 sets the divisor in uartinit and then clears DLAB.
With the FIFOs enabled, the chip queues up to 16 bytes in each direction; LSR bit 0
then means “the receive FIFO is not empty” and bit 5 means “the transmit FIFO is
empty”. Offsets 4, 6 and 7 (modem control, modem status, scratch) exist but xv6 never
touches them. See kernel/uart.c.
address space
The set of virtual addresses a piece of code can use and what each one maps to, as defined by one page table. Each xv6 process has its own; the kernel has one more, shared by all CPUs.
Switching satp switches the address space. The kernel runs on
kernel_pagetable; the trampoline code in kernel/trampoline.S switches to a
process’s page table on the way to user mode and back to the kernel’s on every trap.
Because user memory is not mapped in the kernel’s address space, the kernel reaches it
by translating user addresses in software (copyin, copyout).
argc and argv (program arguments)
The arguments a program receives in main(int argc, char *argv[]): argc is how many there are and argv an array of pointers to the argument strings, ended by a null pointer. By convention argv[0] is the program’s own name.
When you type echo hi, the shell calls exec("echo", argv) with argv = {"echo", "hi", 0}. The kernel (kexec) copies the strings and the pointer array onto the new
program’s user stack and starts the program with argc in register a0 and the
address of the array in a1, which is exactly where the RISC-V
calling convention puts the first two arguments of a function. xv6 allows at most
MAXARG - 1 = 31 arguments.
background job (&)
A command ending in &, which the shell starts without waiting for it to finish, so the next prompt appears at once while the command keeps running.
xv6’s shell gets this effect with an extra process. The child it forks for the command
line forks again, lets the grandchild run the command, and exits immediately. The
shell’s wait returns right away and it prints the next prompt.
The grandchild is now an orphan: its parent has exited. The kernel hands it to init
(reparent), whose endless wait loop (user/init.c) collects it when it
finishes, so it does not stay a zombie forever. xv6 has no job control: you
cannot list, stop or bring back background jobs, and their output can appear in the
middle of your next command line.
block
The unit in which xv6’s file system reads and writes the disk: 1024 bytes (BSIZE). Blocks are numbered from 0 at the start of the disk; block 1 is the superblock.
The disk device itself works in 512-byte sectors, so one xv6 block is two sectors;
virtio_disk_rw converts a block number into a sector number by multiplying by 2.
Everything above the driver (the buffer cache, the log, the
file system in kernel/fs.c) thinks only in blocks. How the 2000 blocks of fs.img
are used is described under disk layout (xv6 file system).
boot
boot stack (stack0)
The first stack each hart uses: its own 4096-byte slice of the array stack0 (kernel/start.c:11), selected by kernel/entry.S:17. _entry, start and main run on it, and it then becomes the hart’s scheduler stack.
stack0 is 4096 * NCPU bytes in the kernel’s .bss, at 0x80007890 in this build. Hart
h starts with sp = stack0 + (h + 1) * 4096, the top of its slice: 0x80008890 for hart
0, 0x80009890 for hart 1, 0x8000a890 for hart 2. The address is physical while paging
is off, and stays valid after kvminithart turns paging on, because the kernel maps its
data at the same virtual address (direct map).
There are no guard pages between or around the slices. A hart that overflowed its slice
would silently overwrite the top of the slice below (where that hart keeps its
start, main and scheduler frames) or, for hart 0, kernel variables such as
ticks. See The stacks of xv6.
buffer cache
A fixed set of in-memory copies of disk blocks (kernel/bio.c). Code that wants a block calls bread, gets a locked buf, uses it, and gives it back with brelse. A block is cached at most once, so the buffer’s lock also serializes all processes that use that block.
xv6 has NBUF (30) buffers. They are kept on a list ordered by how recently they were
released, so that when a new block is needed the least recently used free buffer is
recycled. A buffer that the log has modified but not yet written
home is pinned (bpin) so that it cannot be recycled and lose the change.
byte order (endianness)
The order in which the bytes of a multi-byte number are stored in memory or on disk. Little-endian machines store the least significant byte first (at the lowest address); big-endian machines store the most significant byte first. xv6 on RISC-V is little-endian.
The 4-byte number 0x10203040 is stored as the bytes 40 30 20 10 on a little-endian
machine and 10 20 30 40 on a big-endian one. Inside one program this never matters:
the CPU reads back what it wrote. It matters as soon as bytes travel between machines,
for example a disk image written by one computer and read by another.
mkfs/mkfs.c runs on your computer but writes numbers that the RISC-V kernel will
read, so it converts them with xint and xshort, which always produce
the little-endian layout. Almost every computer you are likely to build on (x86-64,
64-bit ARM) is little-endian too, so on them these functions change nothing.
calling convention
The rules compiled code follows when one function calls another: arguments go in registers a0–a7, the return value comes back in a0, the return address is in ra, sp must stay 16-byte aligned, and s0–s11 must be preserved by the callee.
console
The text terminal through which a user talks to the system: what you type goes in, what programs print comes out. In xv6 it is the UART, wired by QEMU to the terminal you started it from, and programs reach it through the device file /console.
The first user program, init, creates /console with mknod (device number
CONSOLE = 1) and opens it as file descriptors 0, 1 and 2 (standard input, output and
error). Every process it starts inherits them, so the shell and the programs it runs
read and write the console without opening anything.
A read or write on such a descriptor goes through the device table devsw to
consoleread or consolewrite in kernel/console.c. Kernel messages bypass the
file layer and go straight to the UART through printk and consputc.
context switch
Stopping one thread of execution on a CPU and resuming another, by saving the first one’s registers and loading the second one’s. In the xv6 kernel this is swtch, which always switches between a process’s kernel thread and the CPU’s scheduler.
Every xv6 process has a kernel thread: its kernel stack plus the registers saved
in p->context. Each CPU’s scheduler is a thread too, with its registers in
cpus[i].context. A switch from process A to process B is two switches: A to the
scheduler (in sched) and the scheduler to B (in scheduler).
swtch saves only ra, sp and the callee-saved registers s0–s11. The rest are
either already saved by the C code that called swtch (because the
calling convention lets a call destroy them) or belong to the CPU rather than the
thread (such as tp). The user registers are not involved: they were saved
in the trapframe when the process entered the kernel.
crash recovery
The work done at boot to bring the disk back to a consistent state after the machine stopped in the middle of updating it. In xv6 it is recover_from_log, called from initlog before any file system call runs: it replays a committed but not yet installed transaction from the log.
critical section
A stretch of code that reads or updates shared data and must not run on two CPUs at once. In xv6 it is the code between acquire(&lk) and release(&lk).
A lock does not protect code; it protects data. Every piece of code that touches the
data must hold the same lock, or the lock protects nothing. That is why xv6 writes, next
to each shared variable, which lock guards it (for example the comments in
struct proc, kernel/proc.h:85).
Keeping critical sections short matters: other CPUs that want the same lock spin, doing no useful work, until it is released.
CSR (control and status register)
A special register that controls the hart or reports its state, separate from the 32 ordinary registers. CSRs are read and written only with dedicated instructions such as csrr and csrw.
Each CSR has a name and a 12-bit number. Its name usually starts with the lowest
privilege mode allowed to access it: m... CSRs (like mstatus)
are for machine mode only, s... CSRs (like sstatus) for supervisor mode and above.
Touching a CSR from a mode that is not allowed raises an illegal-instruction exception.
xv6 wraps every CSR it uses in a tiny C function in kernel/riscv.h, such as
r_mstatus and w_mstatus.
deadlock
A state in which two or more CPUs (or processes) each wait forever for something the other holds, so none of them can continue.
Two ways it happens in a kernel like xv6:
- Lock ordering. CPU 1 holds lock A and waits for B; CPU 2 holds B and waits for A.
xv6 prevents this by always acquiring locks in a fixed global order. For processes:
wait_lockbefore anyp->lock(kernel/proc.c:23). - Interrupt on the same CPU. Code holds lock A; an interrupt arrives on the same CPU and its handler tries to acquire A. The handler spins forever, because the code that would release A cannot run until the handler returns. xv6 prevents this by turning interrupts off while any spinlock is held; see interrupts and spinlocks (push_off / pop_off).
acquire also panics if a CPU tries to take a lock it already holds, which would
otherwise deadlock the CPU with itself.
The measured order of every lock in the kernel is on Locks and interrupt state; see also lock order.
device interrupt
An interrupt raised by a device (the UART when a key is pressed, the disk when a request completes). On RISC-V it reaches the kernel as a supervisor external interrupt through the PLIC, which tells the kernel which device it was.
The path through xv6:
- The device signals the PLIC. If that source is enabled for a hart’s
supervisor mode and its priority is above the hart’s threshold (
plicinithart), the PLIC raises the hart’s external-interrupt line. - The hart takes a trap with
scause=0x8000000000000009once interrupts are enabled (see trap cause (scause values)). devintrasks the PLIC which device it was (plic_claim), calls that driver’s handler (uartintrorvirtio_disk_intr), then tells the PLIC it is done (plic_complete).
Timer interrupts are not device interrupts in this sense: they come from the hart’s own timer (Sstc), not through the PLIC.
dinode (on-disk inode)
The 64-byte form in which an inode is stored on the disk: type, device numbers, link count, size, and 13 block addresses. 16 of them fit in one 1024-byte block.
Defined as struct dinode in kernel/fs.h. Inode number i lives in block
i / 16 + sb.inodestart (IBLOCK), at slot i % 16. A type of 0 means the
inode is free. The kernel copies a dinode into the in-memory struct inode in
ilock and back out in iupdate.
direct map
xv6’s kernel page table maps RAM and device registers at virtual addresses equal to their physical addresses. So with paging on, the kernel can still use a physical address (from kalloc, or found in a PTE (page-table entry)) directly as a pointer.
kvmmake builds the direct map: everything from KERNBASE (0x80000000) to
PHYSTOP, plus the UART, virtio and PLIC registers. The exceptions are the
TRAMPOLINE page and the per-process kernel stacks, mapped at high virtual addresses
that do not match their physical ones.
This is what lets walk follow page-table pointers, which are physical addresses,
and what lets copyin and copyout reach a user page through its physical
address.
directory
An inode of type T_DIR whose content is a list of 16-byte entries, each pairing a name (up to 14 characters) with an inode number. Looking a name up in a directory gives the number of the inode it names.
The entry format is struct dirent in kernel/fs.h. An entry whose inode
number is 0 is an unused slot. Every directory contains . (itself) and .. (its
parent); in the root directory both refer to the root, inode 1. dirlookup searches
a directory and dirlink adds an entry.
disk layout (xv6 file system)
How xv6 divides the disk into regions: boot block, superblock, log, inode blocks, free bitmap, data blocks, in that order. mkfs chooses the sizes and records them in the superblock; see kernel/fs.h.
For the default fs.img (FSSIZE = 2000 blocks of 1024 bytes):
| Blocks | Region |
|---|---|
| 0 | boot block (unused by xv6) |
| 1 | superblock |
| 2–32 | log: 1 header block + 30 data blocks |
| 33–45 | inodes: 13 blocks, 16 inodes each |
| 46 | free bitmap: 1 block, one bit per disk block |
| 47–1999 | data blocks (1953) |
DMA (direct memory access)
A device reading or writing RAM by itself, without the CPU copying each byte. The driver gives the device a physical address and a length; the device transfers the data and reports when it is done.
Because the device uses physical addresses and knows nothing of page tables, a driver
must hand it physical addresses. In xv6 the kernel’s own memory is mapped at virtual
addresses equal to physical ones, so virtio_disk_rw can pass kernel pointers such as
b->data directly. (On QEMU the “device” is software in QEMU that accesses the guest’s
RAM, but the driver sees the same behavior as with real DMA.)
exception
A trap caused by the instruction being executed: an illegal instruction, a page fault, a misaligned access, or a deliberate ecall (a system call).
exec
The system call that replaces the calling process’s program with a new one read from a file. The process keeps its PID, its open files and its current directory; its memory, registers and name are replaced, and it starts running the new program from the beginning.
exec(path, argv) does not return if it succeeds: the code that called it no longer
exists. If it fails (no such file, not an executable, out of memory), it returns -1 and
the old program continues. Unix creates new programs by combining fork, which
duplicates a process, with exec in the child; the gap between the two is where the
shell sets up redirections and pipes.
In xv6 the work is split between sys_exec, which copies the arguments into the
kernel, and kexec in kernel/exec.c, which builds the new memory image.
exit status
The integer a process hands back when it ends, by calling exit(status). Its parent can collect it with wait. By convention 0 means “success” and any other value means “something went wrong”.
In xv6, kexit stores the status in the dying process’s p->xstate and the process
becomes a zombie until its parent calls wait(&status); kwait then copies
xstate out to the parent. A user program that returns from main exits with
main’s return value, because the library’s start function calls exit(main(...))
(user/ulib.c).
The convention is only a convention. Nothing in the kernel treats 0 specially, and
xv6’s own shell (user/sh.c) passes 0 to wait and so never looks at the status at
all. Several xv6 utilities (mkdir, rm, ln, kill) exit with 0 even when the
operation failed. On Unix systems whose shells do test the status (make, &&,
if), that would hide errors; in xv6 nobody checks.
file descriptor
A small non-negative integer that a process uses to name one of its open files: the fd in read(fd, buf, n). It is an index into the process’s own table of open files, p->ofile, which has 16 slots in xv6 (NOFILE).
Descriptors are per process: descriptor 3 in one process and descriptor 3 in another can
name entirely different files. By convention 0 is standard input, 1 standard output and
2 standard error; in xv6 all three start out as the console, opened by /init and
inherited by every process after it.
A new descriptor is always the lowest free number (fdalloc). fork copies the
whole table, so parent and child share the same open files (filedup); exec keeps
it unchanged, so a program inherits the descriptors of the program that ran it. The
shell builds redirections and pipelines from these rules: close descriptor 1, then
open or dup something, and it lands in slot 1.
The same descriptor can refer to a file on disk, a device such as the console, or one
end of a pipe; read and write work the same way on all of them.
fork and exec
The Unix way to run a new program: fork makes a copy of the current process, and the copy (the child) calls exec to replace itself with the new program, while the original (the parent) usually calls wait until the child exits.
Splitting “make a process” from “load a program” looks wasteful but gives the child a
window to adjust its own state between the two calls: close and reopen descriptors
(I/O redirection), connect pipes (pipeline (a | b)), change
directory. exec keeps open descriptors, so the new program inherits the result
without knowing anything about it. The shell (user/sh.c) is built on this pattern.
The path through xv6: user fork() is a stub in user/usys.S that executes
ecall; the kernel dispatches to sys_fork, which calls
kfork. exec() reaches sys_exec and then kexec;
wait() reaches kwait.
free bitmap
A region of the disk with one bit per block: 1 if the block is in use, 0 if it is free. balloc finds a 0 bit and sets it to allocate a block; bfree clears it.
One 1024-byte bitmap block has 8192 bits (BPB), so the 2000-block default disk
needs a single bitmap block. Block b’s bit is bit b % 8 of byte (b % 8192) / 8 in
bitmap block b / 8192 + sb.bmapstart (BBLOCK). The bits for the boot block,
superblock, log, inode blocks and the bitmap itself are set by mkfs/mkfs.c, so they
are never allocated.
free-list allocator
A memory allocator that keeps the unused blocks of its memory on a linked list, with the list’s links and each block’s size stored in a small header inside the free blocks themselves. malloc searches the list for a big enough block; free puts a block back.
xv6 has two:
- The kernel’s page allocator (
kernel/kalloc.c) is the simplest possible case: every block is one 4096-byte page, so any free block fits any request and the list needs no sizes and no ordering. - The user library’s
malloc(user/umalloc.c) is the classic allocator from Kernighan and Ritchie’s The C Programming Language. Blocks have any size (in 16-byte units). The list is circular and sorted by address.malloctakes the first block that is big enough, cutting the request off its end if it is bigger;freeputs the block back in address order and merges it with a free neighbor directly before or after it, so that freed memory does not stay split into ever smaller pieces (fragmentation). When nothing fits, the allocator asks the kernel for more memory withsbrk.
Because the bookkeeping lives inside the memory it manages, a program that writes past
the end of a malloced block, or frees a block twice, damages the list, and the
failure usually shows up much later, somewhere else.
guard page
A page left inaccessible next to a stack, so that a stack that grows too far causes a page fault instead of silently overwriting whatever lies beyond it.
xv6 has two kinds. Each kernel stack (KSTACK) has an unmapped page below it in the
kernel page table; overflowing into it makes the kernel fault and eventually panic
(not cleanly: the trap handler’s own register saves spill into the next stack down
first). Each user stack has a
page below it whose PTE (page-table entry) is valid but has the U bit cleared (uvmclear in
kexec), so a user-mode access faults and the process is killed.
hard link
A directory entry: a name that leads to an inode. A file can have several hard links, all equal; none of them is the “real” name. The link system call adds one, unlink removes one.
After ln a b, the names a and b refer to the same inode: the same data, the same
size, the same inode number (ls shows it in the third column). Writing through
one name changes what you read through the other. The inode’s link count (nlink) counts
the names; rm a removes only the name a and decrements the count, and the file’s
blocks are freed only when the count reaches zero and no process has it open.
Hard links only work inside one file system (an entry stores an inode number, which is
only meaningful on its own disk), and xv6 refuses to make one to a directory
(sys_link), because a second name for a directory could create a cycle in the
tree. Many Unix systems also have symbolic links, files that contain another path;
xv6 does not.
hart
RISC-V’s word for one CPU core: a “hardware thread” that fetches and executes its own stream of instructions. A machine started with -smp 3 has three harts, numbered 0, 1, 2.
Every hart has its own registers, its own program counter and its own copy of most
control and status registers. All harts share the same physical memory. xv6
calls them “CPUs” in comments and C names (struct cpu, cpuid) and “harts” when talking
to the hardware (mhartid).
heap
The region of a process’s memory from which malloc hands out blocks whose lifetime the program controls (until free). In xv6 it lies above the stack and grows upward when malloc asks the kernel for more with sbrk.
A program’s memory has three kinds of storage: global variables (fixed size, decided
when the program is linked), local variables on the stack (released when the
function returns), and the heap, for data whose size or lifetime is known only while
the program runs, such as the parse tree that user/sh.c builds for each command.
The kernel knows nothing about individual heap blocks. It only tracks one number per
process, its size p->sz; everything between the end of the stack and p->sz is the
heap (see user memory layout). sbrk(n) moves that end up by n bytes and
returns the old end. The user-level allocator in user/umalloc.c carves those bytes
into blocks, keeps freed blocks on a free list for reuse,
and never gives memory back to the kernel.
I/O redirection
Making a program read from or write to a file instead of the terminal, written < file (standard input) or > file (standard output) on a command line. The shell does it by replacing descriptor 0 or 1 before running the program, which never notices.
The trick depends on one rule: a new file descriptor always gets the lowest
free number (fdalloc). For wc > out, the child process that will run wc
does close(1) and then open("out", ...). Descriptor 1 is now the lowest free slot,
so the file lands there. Then it calls exec; descriptors survive exec, so wc
writes “to standard output” as always, and the bytes go into out (user/sh.c).
Because the change happens in the child after fork, the shell’s own descriptors are
untouched, and its next prompt still goes to the console.
In xv6, > opens the file with O_TRUNC (empty it first). >> exists in the parser
but xv6 has no append mode, so it writes from the beginning of the file without
truncating it, overwriting the old bytes instead of adding after them.
indirect block
A disk block that holds no file data, only a list of 256 more block numbers. It lets an inode, which has room for 12 block addresses, describe files of up to 268 blocks.
The 13th address in an inode, addrs[12] (index NDIRECT), points to the indirect
block. Its 1024 bytes are read as 256 uint block numbers (NINDIRECT), for the
file’s blocks 12 to 267. bmap allocates it the first time a file grows past 12
blocks, and itrunc frees it.
inode
The file system’s record for one file, directory or device: its type, size, link count and the list of disk blocks holding its content. An inode has a number but no name; names live in directories.
xv6 has two forms of every inode. The on-disk form, dinode (on-disk inode), sits in the inode
blocks of the disk. The in-memory form, struct inode, is a copy the
kernel keeps in itable while the inode is in use, with extra bookkeeping: a
reference count (ref), a valid flag saying whether the copy has been read
from disk, and a sleep lock.
The usual way to use one is
ip = iget(dev, inum); // or namei(path), dirlookup(...)
ilock(ip); // lock, and read from disk if needed
... use or change ip->size, ip->type, the content ...
iunlock(ip);
iput(ip); // drop the reference
See kernel/fs.c for the functions and the rules about which lock protects what.
inode number
The index of an inode in the disk’s inode area, which identifies a file within the file system. Directory entries store inode numbers; inode 1 is the root directory and inode 0 is never used.
Called inum in the code. Given the number, the kernel can compute where the inode
lives on disk with IBLOCK. fstat reports it to user programs as ino in
struct stat.
intena
mycpu()->intena: whether interrupts (the SIE bit) were on just before this hart’s outermost push_off. The last pop_off turns them back on only if it is 1.
It is recorded only when noff goes from 0 to 1, so it is 1 in a system call (after
usertrap enabled interrupts) and 0 in interrupt handlers, in the scheduler and at
boot. It belongs to the kernel thread, not to the hart: sched saves it across
swtch and restores it, and the scheduler sets it to 0 after each switch back.
When noff is 0 its value is stale and unused. See Locks and interrupt state and
Locks and interrupt state.
interrupt
A trap caused by something outside the running instruction stream, such as a timer expiring or a device (keyboard, disk) needing service. Interrupts are taken only when enabled.
A supervisor-level interrupt is taken only if its bit in sie is set, and
either the hart is running in U-mode, or it is in S-mode with the global SIE bit in
sstatus set. xv6 enables the individual bits once at boot in start and toggles the global
bit with intr_on and intr_off.
interrupts and spinlocks (push_off / pop_off)
xv6 turns interrupts off on a CPU for as long as that CPU holds any spinlock, so that an interrupt handler can never spin on a lock that the interrupted code holds. push_off and pop_off count how many locks are held and restore the original interrupt state when the count returns to zero.
Example: sys_pause holds tickslock. If a timer interrupt arrived on the same CPU (on hart 0,
which counts ticks), clockintr would try to acquire tickslock and spin forever, since the code that
holds it is the code the interrupt paused. With interrupts off the timer interrupt
stays pending and is taken right after the release.
Locks nest, so a plain “off on acquire, on on release” would turn interrupts back on
when the inner lock is released while the outer one is still held. Instead each CPU
keeps two fields in its struct cpu: noff, the nesting depth, and intena, whether
interrupts were on before the outermost push_off. Only the pop_off that brings
noff back to 0 may turn interrupts on, and only if intena says they were on.
intena belongs to the kernel thread, not the hart: other threads (the scheduler,
other processes) overwrite mycpu()->intena while a process is switched out. So sched
keeps the process’s value in a local variable across swtch and puts it back, and the
scheduler sets it to 0 after every switch back to itself (kernel/proc.c:456).
See Locks and interrupt state and Locks and interrupt state.
kernel
The part of the operating system that runs with more hardware privilege than user programs (supervisor mode in xv6), manages memory, processes, files and devices, and serves system calls from user programs.
kernel stack
The stack a process uses while it runs kernel code (during a system call or an interrupt). There is one 4096-byte kernel stack per slot of the process table, at the virtual address p->kstack, separate from the process’s user stack; all 64 are allocated once at boot and never freed.
The kernel cannot run on the user stack: user code controls sp and could point it
anywhere. So each process slot gets a page of kernel memory, allocated and mapped in the
kernel page table at KSTACK(i) for slot i by proc_mapstacks while the
kernel boots. The stack belongs to the slot, not to a process: allocproc and
freeproc never touch it, and whichever process occupies the slot runs on it. No user
page table maps it. The page below each stack is left unmapped as a guard page, so
an overflow causes a page fault instead of silently overwriting a neighbor.
On every trap from user mode, sp holds the kernel stack’s address from
kernel/trampoline.S:76 to kernel/trampoline.S:118, and the stack is usable from
kernel/trampoline.S:92 (kernel page table) until kernel/trampoline.S:111 (user
page table). It always starts at the top: the stack is empty whenever the process is in
user mode. A process is also back on its kernel stack whenever swtch resumes it. A kernel stack is also where a sleeping or preempted process’s kernel state lives:
the call chain that led to sched stays on it until the process runs again, on any
hart. A forked child starts with an empty one, not a copy of its parent’s. The
scheduler does not run on a kernel stack; it has the hart’s scheduler stack.
See The stacks of xv6.
kernelvec frame
The 256 bytes that kernelvec pushes onto the current stack when a trap happens in supervisor mode, holding 17 registers (ra, gp, and the temporary and argument registers) while kerneltrap runs. xv6 has no separate interrupt stack.
The frame lands on whatever stack was active: a process’s kernel stack (an
interrupt during a system call) or the hart’s scheduler stack (an interrupt in the
scheduler’s interrupt window). It holds ra, gp, t0–t2, a0–a7 and t3–t6;
the slots for sp, tp and s0–s11 stay unused.
If the trap was a timer interrupt and the hart has a current process, kerneltrap
calls yield, and the process is suspended with the frame in the middle of its kernel
stack. It may be resumed on another hart, which is why kernelvec does not restore tp
(kernel/kernelvec.S:44). Handlers run with interrupts off, so in normal operation a
stack holds at most one such frame. See The stacks of xv6.
line editing (cooked input)
Processing the kernel applies to typed input before any program sees it: echoing each key to the screen, letting backspace and control-U erase, and holding the text back until Enter completes a line.
Unix systems call this “canonical” or “cooked” mode, as opposed to “raw” mode, in which
a program receives every key the moment it is pressed. xv6 has only the cooked mode,
implemented in consoleintr:
| Key | Byte | Effect |
|---|---|---|
| Enter | '\r' (13), stored as '\n' |
completes the line; a waiting read returns it |
| Backspace / Ctrl+H | 127 / 8 | erases the last character of the line being typed |
| Ctrl+U | 21 | erases the whole line being typed |
| Ctrl+D | 4 | end of file: completes the line; a read with nothing before it returns 0 |
| Ctrl+P | 16 | prints the process list (procdump); not stored |
Because editing happens in the kernel, a program such as the shell needs no code of its
own for backspace: by the time its read returns, the line is already final.
link count (nlink)
The number of directory entries that name an inode, stored on disk in the inode’s nlink field. A file created once has 1; the link system call adds names, unlink removes them.
It is a different count from the in-memory reference count. A file’s space is
freed only when both are zero: no name leads to it and no one has it open. That is
why a file removed with rm while another process reads it keeps working until it is
closed. In xv6, a directory’s nlink also counts the .. entries of its
subdirectories (but not its own .).
lock order
The rule that locks are always acquired in one global order. “A before B” means some code holds A while acquiring B; if any other code held B while acquiring A, two harts could deadlock.
In xv6: sleep-locks before spinlocks, a condition lock (cons.lock, pi->lock,
tickslock, log.lock, disk.vdisk_lock, a sleep-lock’s inner spinlock) before
p->lock, and wait_lock before any p->lock (the comment at kernel/proc.c:23).
p->lock is taken after almost everything; only pid_lock, kmem.lock, ftable.lock
and itable.lock are ever taken while holding it: for a process slot being built, and
(kmem.lock only) when kwait frees a zombie child under the child’s lock. The graph, measured on a running system, is on
Locks and interrupt state.
lost wakeup
The bug where a process decides to sleep because a condition is false, the event that makes it true happens before the process is actually asleep, and the wakeup finds nobody to wake, so the process sleeps forever.
xv6 prevents it with sleep_prepare, which records the channel in p->chan while the
caller still holds the lock that protects the condition, and with wakeup, which clears
p->chan even if the process has not gone to sleep yet. sleep only sleeps if
p->chan is still set, so a wakeup that lands between release and sleep() makes
sleep() return at once. See sleep and wakeup and Locks and interrupt state.
memory barrier (fence)
An instruction, or a compiler directive, that stops memory reads and writes from being reordered across it. Locks need barriers so that the protected data is read and written only while the lock is held.
Both the compiler and the hardware may reorder memory operations that look independent: the compiler to make code faster, a RISC-V hart because its memory model allows other harts to observe its loads and stores in a different order than the program wrote them. Within one hart this is invisible; between harts it is not.
In xv6 the barriers come from the __atomic built-ins in
kernel/spinlock.c:
acquire's__ATOMIC_ACQUIREexchange compiles toamoswap.w.aq. Theaqbit means no later load or store of this hart can be seen by others before the swap.release's__ATOMIC_RELEASEstore compiles tofence rw,wfollowed bysw zero. The fence makes every earlier load and store complete before the store that frees the lock.
Both also restrict the compiler: nothing after the acquire may move above it, and nothing before the release may move below it.
memory-mapped I/O (MMIO)
Controlling a device by reading and writing ordinary memory addresses that are wired to the device’s registers instead of to RAM. xv6 talks to the UART, the disk and the interrupt controller this way (see kernel/memlayout.h).
noff
mycpu()->noff: how many push_off calls this hart has not yet undone with pop_off. Every spinlock held adds one, and myproc adds one for a moment. While it is above 0, interrupts on this hart are off.
It lives in struct cpu (kernel/proc.h:25, offset 120), one per hart. Only the
pop_off that brings it back to 0 may turn interrupts on, and only if intena says
they were on before. sched insists it is exactly 1 (the process’s own p->lock) at
every context switch, and panics sched locks otherwise. The deepest value measured in
this tree is 3. See Locks and interrupt state.
open file (struct file)
The kernel’s record of one opening of a file, pipe or device: what it refers to, whether it may be read or written, and the current read/write offset. In xv6 it is a struct file in the system-wide table ftable (NFILE = 100 entries).
A file descriptor points at an open file, and several descriptors can point at the
same one: after dup, or in a parent and child after fork. They then share one offset.
Each write to a file reads and advances that offset while holding the file’s inode
lock, so a parent and child writing through the same inherited descriptor append after
each other instead of overwriting each other. (A very large write is split into pieces
of about 3 KB, which may interleave.) The open file’s ref field counts these pointers
(reference count); fileclose really closes it only when the count drops to
zero.
type says what kind of object is behind it: FD_INODE (a file or directory on disk,
with an inode and an offset), FD_DEVICE (a device, with a major number that
selects the driver in devsw), or FD_PIPE (one end of a pipe). See
kernel/file.h and kernel/file.c.
orphaned inode
An inode that is still allocated on disk (its type is not 0) but has a link count of 0, so no directory entry leads to it. It is normal while a process still has the file open or as its current directory; after a crash it is garbage that ireclaim frees at the next boot.
Unix lets you delete (unlink) a file that is still open. Unlinking removes the name
and drops the link count (nlink) to 0, but the inode and its data must stay until
the last open file descriptor or current-directory reference goes away, because
a process may still read and write it. In xv6 the final iput (on close, on
exit, or on chdir away) sees nlink == 0 and ref == 1, truncates the file and
marks the inode free.
If the machine crashes, or QEMU is killed, before that final iput, the unlink is
already safely on disk but the free never happens. The inode is then allocated but
unreachable: an orphan, leaking an inode and its blocks forever. At boot,
fsinit calls ireclaim, which scans every on-disk inode for type != 0 and
nlink == 0 and frees each one it finds. user/forphan.c (a file) and
user/dorphan.c (a directory) create orphans on purpose so that
test-xv6.py can crash xv6 and check that the next boot reclaims them.
page
The unit in which xv6 manages memory: a block of 4096 bytes (PGSIZE) whose address is a multiple of 4096. The hardware translates addresses one page at a time, and the kernel allocates physical memory one page at a time.
Because a page starts on a multiple of 4096 (2^12), the low 12 bits of an address are the offset within its page and the remaining bits name the page. RISC-V calls the page number of a physical address its PPN (physical page number): the address shifted right by 12.
PGROUNDDOWN gives the start of the page containing an address; PGROUNDUP
rounds a size up to a whole number of pages. The physical pages that hold memory are
handed out by the page allocator.
page allocator
The part of the kernel that hands out free physical pages and takes them back: kalloc and kfree in kernel/kalloc.c. Every page table, user page, kernel stack, trapframe and pipe buffer comes from it.
page fault
An exception raised when an address cannot be translated, or its PTE (page-table entry) forbids the access. RISC-V has three kinds: instruction (cause 12), load (13) and store/AMO (15); stval holds the faulting virtual address.
In xv6, usertrap handles load and store page faults from user programs by calling
vmfault, which allocates the page if it belongs to memory the process grew lazily
(with sbrklazy). Any other user page fault kills the process. A page fault inside the
kernel is a bug: kerneltrap panics.
page table
A tree of tables in memory that tells the hardware how to translate virtual addresses into physical ones, one 4096-byte page at a time, and what access (read, write, execute, user) each page allows. The satp register points at the active one.
parse tree
A tree of structures that records the grammatical structure of some text: which parts belong together and in what order they apply. xv6’s shell turns each command line into one before running it.
For echo hi | wc > out & the shell builds:
BACK
└─ PIPE
├─ EXEC echo hi
└─ REDIR fd 1 → out
└─ EXEC wc
Each node is a small struct whose first field says what kind of node it is
(EXEC, REDIR, PIPE, LIST, BACK). The tree is built by a
recursive-descent parser (parseline and friends in
user/sh.c) and executed by walking it recursively (runcmd).
Separating parsing from running keeps both simple: the runner never deals with characters, and the parser never deals with processes.
path name
A string such as /a/b/c or b/c naming a file by the directories that lead to it. A leading / starts the search at the root directory; otherwise it starts at the process’s current directory.
Each piece between slashes is an element. namex resolves a path by looking up
each element in turn, using skipelem to split it. Repeated and trailing slashes are
ignored, and elements longer than 14 characters are cut to their first 14.
physical memory / physical address
The real RAM and device registers, addressed by the numbers the memory bus uses. On QEMU’s virt machine RAM starts at physical address 0x80000000; below that are devices.
PID (process ID)
The number that names a process in system calls such as kill and wait. xv6 hands out 1, 2, 3, … from the counter nextpid and does not reuse numbers (short of the int counter overflowing after about two billion processes).
The first process (init) gets PID 1. A PID stays attached to its process until the
parent’s kwait frees the slot; freeproc then sets the slot’s pid to 0, which
is why kkill refuses PID 0.
pipe
A one-way channel for bytes between processes: what one process writes into one end, another reads from the other end, in the same order. Created by the pipe system call, which returns two file descriptors, one per end.
In xv6 a pipe is a 512-byte buffer inside the kernel (kernel/pipe.c). A writer that
finds it full sleeps until a reader makes room; a reader that finds it empty sleeps
until a writer adds data. When every write end has been closed, a reader that has
drained the buffer gets end-of-file (read returns 0); when every read end has been
closed, write fails.
The shell uses pipes for a | b: it creates a pipe, forks twice, and in each child
moves one end onto standard output (for a) or standard input (for b) before
exec, so neither program needs to know it is talking to a pipe (user/sh.c).
pipeline (a | b)
A command line such as echo hi | wc that connects the standard output of one program to the standard input of the next through a pipe, so the programs run at the same time and data flows between them without a temporary file.
The shell creates the pipe, then forks one child per side. The left child moves the
pipe’s write end onto descriptor 1, the right child moves the read end onto
descriptor 0, both close the original pipe descriptors, and both exec their
programs (the same trick as redirection). The shell itself closes
both ends and waits for the two children.
Closing every unused end matters. The reader sees end-of-file only when every write
end is closed; one forgotten copy of the write end (in the shell, or in the reading
child itself) would make wc wait for more input forever.
In xv6, a | b | c is parsed as a | (b | c): the right child of the first pipe is
itself a pipeline and forks two children of its own (user/sh.c).
PLIC (platform-level interrupt controller)
The device that collects interrupt requests from other devices (UART, disk) and routes them to harts. xv6 programs it in kernel/plic.c.
PMP (physical memory protection)
A RISC-V mechanism that lets machine mode limit which physical addresses supervisor and user mode may access. If PMP is implemented and no PMP entry grants access, S-mode and U-mode accesses fail, so xv6 sets one entry that grants everything.
privilege mode
The level of authority a hart is running at. RISC-V defines three basic ones: Machine mode (M, all powers), Supervisor mode (S, where the xv6 kernel runs) and User mode (U, where programs like the shell run).
The privilege mode decides which instructions and CSRs the hart may use and, through page tables and PMP, which memory it may touch. A program in U-mode cannot, for example, turn off interrupts or change the page table; it must ask the kernel through a system call.
| Mode | Who runs there in xv6 | Typical powers |
|---|---|---|
| M (machine) | only kernel/entry.S and kernel/start.c, once, at boot |
everything: all CSRs, all memory |
| S (supervisor) | the kernel | page tables, trap handling, interrupts |
| U (user) | user programs | ordinary computation only |
A hart moves to a less privileged mode with a return instruction (mret,
sret) and to a more privileged one only through a trap.
process
A running program: its memory, its registers, its open files, and its state (running, runnable, sleeping…). In xv6 each process is a struct proc in the proc table.
process state
Where a process is in its life, stored in p->state: UNUSED, USED, RUNNABLE, RUNNING, SLEEPING or ZOMBIE. Changing it requires holding p->lock.
| State | Meaning | Left by |
|---|---|---|
UNUSED |
the slot in proc is free |
allocproc → USED |
USED |
slot taken, process being built | userinit or kfork → RUNNABLE; failure → UNUSED |
RUNNABLE |
ready, waiting for a CPU | scheduler → RUNNING |
RUNNING |
executing on some CPU | yield → RUNNABLE; sleep → SLEEPING; kexit → ZOMBIE |
SLEEPING |
waiting for an event | wakeup or kkill → RUNNABLE |
ZOMBIE |
exited, waiting for the parent | kwait via freeproc → UNUSED |
The scheduler only ever looks for RUNNABLE.
PTE (page-table entry)
One 64-bit entry of a page table. It holds a physical page number and permission flags; a leaf PTE maps one virtual page to one physical page, a non-leaf PTE points to the next-level page-table page.
Layout of an Sv39 PTE (xv6’s macros in kernel/riscv.h):
| Bits | Name | Meaning |
|---|---|---|
| 0 | V (PTE_V) |
valid: if 0, the hardware ignores the rest and any access faults |
| 1 | R (PTE_R) |
the page may be read |
| 2 | W (PTE_W) |
the page may be written |
| 3 | X (PTE_X) |
instructions may be fetched from the page |
| 4 | U (PTE_U) |
user mode may access the page (and supervisor mode, by default, may not) |
| 5 | G | global mapping (xv6 never sets it) |
| 6 | A | accessed (set by hardware, see Svadu extension (A and D bits)) |
| 7 | D | dirty: written (set by hardware) |
| 9–8 | RSW | reserved for the operating system’s own use (xv6 does not use them) |
| 53–10 | PPN | physical page number: the physical address shifted right by 12 |
| 63–54 | reserved for extensions; xv6 leaves them 0 |
A PTE with V = 1 and R = W = X = 0 is not a mapping at all: it points to the next level
of the page table. Any of R, W, X set makes it a leaf. PA2PTE and PTE2PA convert
between a physical address and the PPN field; PTE_FLAGS extracts bits 9–0.
push_off / pop_off
The matched pair that turns interrupts off on this hart and counts how many times (push_off), and undoes one level, turning interrupts back on only at the outermost level and only if they were on before (pop_off). acquire and release call them.
push_off reads and clears sstatus.SIE in one csrrci, records the old value in
intena if noff was 0, and increments noff. pop_off panics if
interrupts are on (pop_off - interruptible) or noff is already 0 (pop_off),
decrements noff, and sets SIE if it reached 0 and intena is 1. Besides the lock code,
myproc and uartputc_sync call them. See interrupts and spinlocks (push_off / pop_off) and
Locks and interrupt state.
race condition
A bug in which the result depends on the exact timing of two or more CPUs (or a CPU and an interrupt handler) touching the same memory, so the program usually works and occasionally does not.
The classic example is two harts incrementing a shared counter. count = count + 1
compiles to a load, an add and a store. If both harts load the old value before either
stores, both store the same new value and one increment is lost. Nothing crashes; the
count is quietly wrong, and only under the right timing.
The cure is to make the load–modify–store sequence a critical section that only one hart can be in at a time, usually with a spinlock.
randomized testing
Testing by choosing operations (or their arguments) with a pseudo-random number generator instead of writing each case by hand, so that a long run tries many combinations and orders nobody thought to write down.
A hand-written test checks the situations its author imagined. A randomized test picks, at every step, one of many operations at random and runs millions of steps, so it also reaches odd sequences such as “unlink the file another process is writing, then create a directory with the same name”. Because the generator is pseudo-random, a run is determined by its starting value (the seed): changing the seed gives a different sequence, and in principle reusing a seed repeats a run, although with several processes the interleaving of their calls still depends on timing.
user/grind.c is xv6’s randomized test: two processes each draw a number from a
Park–Miller generator (do_rand) and perform one of 22 file-system, process or
memory operations, forever. Randomized testing that feeds random or malformed
inputs rather than random operations is usually called fuzzing.
recursive-descent parser
A way to write a parser by hand: one function per grammar rule, each reading the tokens for its rule and calling the functions for the smaller rules it contains. Nesting in the grammar becomes nesting of function calls.
The xv6 shell’s grammar has four rules, each handled by one function (the exec
rule gets help from parseredirs for its redirections):
line = pipe { "&" } [ ";" line ] parseline
pipe = exec [ "|" pipe ] parsepipe
exec = block | redirs { word redirs } parseexec, parseredirs
block = "(" line ")" redirs parseblock
{ } means “zero or more times”, [ ] “optional”. Because block contains a whole
line, parentheses can nest to any depth: the recursion handles it. Each function
looks at the next token without consuming it (peek) to decide which way to go, and
consumes it with gettoken (user/sh.c).
reference count
A counter of how many pointers to a shared object exist; the object can be reused or freed only when the count drops to zero. xv6 counts references to in-memory inodes (ip->ref), to open files (f->ref) and to cached blocks.
The pattern always has three parts: something that takes a reference and increments
the count (iget, idup, filedup), something that drops one and decrements
it (iput, fileclose), and a lock that makes the count’s updates atomic. Forgetting
a decrement leaks the object; an extra decrement frees it while it is still in use.
regression test
A test written after a bug was found and fixed, which recreates the situation that triggered the bug, so that a later change cannot quietly bring it back (a “regression”).
A regression test usually looks odd out of context: it does something no ordinary
program would do, because that is exactly what once broke the system. Its comment
often names the bug, and git log -S on the test’s name usually finds the fix that
came with it.
Much of usertests (user/usertests.c) is of this kind. For example,
killzero calls kill(0) and then forks, because kill(0) once marked an unused
process slot as killed and the next process created in that slot died at once.
reparent2 forks orphans 800 times because the code that hands orphans to init
once deadlocked. Running the whole suite after every change to the kernel checks
that none of these bugs has returned.
regular expression
A small pattern language for describing sets of strings. In the version grep uses in xv6, an ordinary character matches itself, . matches any one character, c* matches zero or more cs, ^ anchors the pattern to the start of the line and $ to the end.
Some examples, with the meanings they have in user/grep.c:
xv6matches any line containing the three charactersxv6.^xv6matches lines that start withxv6.v6.$matches lines whose last three characters arev6followed by any one character.a.*cmatches lines containing anafollowed, anywhere later, by ac.ab*cmatchesac,abc,abbc, …
Full regular-expression languages (POSIX, Perl) add character classes [a-z],
alternation a|b, grouping ( ), +, ? and escapes such as \.. xv6’s matcher has
none of these: it cannot search for only a literal . (a . always matches any
character), and a * is literal only where it has nothing to repeat, at the start of
the pattern or right after another c*. Its whole implementation is
three short recursive functions, from Kernighan and Pike’s The Practice of
Programming.
save area
A fixed place where registers are kept while their owner is not running: the trapframe, p->context, cpus[i].context and the sscratch register. Unlike a stack it has no frames and nothing is pushed or popped; each save overwrites the last.
| Save area | Holds | Saved by | Restored by |
|---|---|---|---|
| trapframe | a user program’s registers and pc |
uservec, usertrap |
userret |
p->context |
a suspended kernel thread’s ra, sp, s0–s11 |
swtch in sched |
swtch in scheduler |
cpus[i].context |
the scheduler’s ra, sp, s0–s11 |
swtch in scheduler |
swtch in sched |
| sscratch | the user’s a0, briefly |
kernel/trampoline.S:32 |
kernel/trampoline.S:72 |
Some of them hold a value of sp (a pointer into a stack), but none of them is one.
See The stacks of xv6.
scheduler
The kernel code that decides which runnable process each CPU runs next. In xv6 every hart ends its boot by entering scheduler, which never returns.
scheduler stack
The stack a hart’s scheduler loop runs on. It is that hart’s slice of stack0, the same memory as its boot stack (stack0): main calls scheduler(), which never returns, so the boot stack simply takes on a new role. It has no guard page.
Inside the loop, sp sits just below the frames of start, main and
scheduler: 0x80008810 on hart 0 in this build (0x80009810 on hart 1, 0x8000a810
on hart 2). swtch saves that value in cpus[i].context.sp when the hart switches to
a process, and loads it again when the process calls sched. struct cpu holds only
this saved pointer, not the stack itself.
Each hart only ever switches to its own scheduler stack, so no two harts share one. An
interrupt taken in the loop’s short interrupt window pushes a kernelvec frame onto
it; kerneltrap never yields there, because the hart has no current process.
See The stacks of xv6.
shell
The ordinary user program that reads command lines (from the keyboard or a file), turns each into processes, and waits for them. In xv6 it is /sh, built from user/sh.c and started by /init.
A shell has no special privileges. Everything it does goes through the same system calls
any program can use: fork and exec to start programs, wait to wait
for them, open, close, dup and pipe to arrange their
file descriptors (I/O redirection, pipeline (a | b)).
That a useful shell fits in about 500 lines of C is a demonstration of how well these
few Unix system calls fit together.
A few commands cannot be ordinary programs because they must change the shell’s own
process state. In xv6 the only one is cd: the current directory belongs to a process,
so a child that changed directory and exited would leave the shell where it was.
If the shell exits (for example when you type Ctrl-D at the prompt), /init
(user/init.c) starts a new one.
sleep and wakeup
xv6’s way for a process to wait for an event without spinning: it registers interest in a “channel” (any address) with sleep_prepare, gives up the CPU with sleep, and becomes runnable again when someone calls wakeup on the same channel.
The usual pattern, with lk the lock protecting the condition:
acquire(&lk);
while (!condition) {
sleep_prepare(chan);
release(&lk);
sleep();
acquire(&lk);
}
and on the other side acquire(&lk); make condition true; wakeup(chan); release(&lk);.
uartwrite uses a variant without a condition lock: it registers first and then
tests the device; a wakeup arriving after the registration clears p->chan, so
sleep returns at once.
The lost-wakeup problem. If a process checked the condition, released the lock and
only then registered on the channel, the event could happen in between: the waker
calls wakeup, finds nobody on the channel, and the process then sleeps forever.
xv6 closes the gap in two halves. sleep_prepare records the channel in p->chan
while the condition lock is still held, so any wakeup that happens after the check
finds the process registered. wakeup clears p->chan, even if the process has not
gone to sleep yet, and sleep only sleeps if p->chan is still set. So a wakeup that
arrives between release(&lk) and sleep() makes sleep() return at once instead of
being lost.
A wakeup means “something changed”, not “the condition is true”: several processes may
be woken and only one may get the resource, and kkill wakes sleepers too. That is
why callers re-check the condition in a while loop.
This differs from the MIT xv6 book and older trees, where sleep(chan, lk) takes the
condition lock as an argument. See Locks and interrupt state and lost wakeup.
sleep lock
A lock whose waiters give up the CPU (sleep) instead of spinning. xv6 uses it for things held for a long time, such as a disk buffer or an inode during disk I/O. Taken with acquiresleep, released with releasesleep.
A struct sleeplock (kernel/sleeplock.h) is a locked flag guarded by a small
spinlock. A process that finds it held calls sleep and is woken by
releasesleep (see sleep and wakeup).
Differences from a spinlock: a process may hold a sleep lock while it sleeps or is switched out, and holding a sleep lock does not by itself turn interrupts off. In exchange, only a process can use one: an interrupt handler cannot sleep, so it can never take a sleep lock.
Ownership is by process: holdingsleep compares the holder’s PID, because a holder can
sleep and resume on another hart. See Locks and interrupt state.
spinlock
A lock that a CPU waits for by looping (“spinning”) until it is free. xv6’s struct spinlock is taken with acquire and given back with release; while a CPU holds one, interrupts on that CPU are off.
The lock is one word, locked: 0 means free, 1 means held. acquire uses an atomic
swap (amoswap) to write 1 and get the old value back in one
indivisible step; if the old value was 0, this CPU now holds the lock, otherwise it tries
again. release stores 0. Both include a memory barrier (fence) so that the protected
data is never read or written outside the lock.
Spinning wastes the CPU, so spinlocks suit short critical sections.
A process must not sleep or give up the CPU while holding one (only the process’s own
p->lock, which sched requires, is allowed). For long waits, such as disk I/O, xv6 uses a sleep lock.
See also interrupts and spinlocks (push_off / pop_off), deadlock and Locks and interrupt state.
Sstc extension
A RISC-V extension that gives supervisor mode its own timer-compare register, stimecmp, so the kernel can schedule its own timer interrupts without help from machine mode.
stack
A region of memory a function uses for its local variables, saved registers and return addresses. On RISC-V it grows downwards (toward lower addresses) and the sp register points at its current top. C code cannot run without one.
xv6 has three kinds. Apart from a few instructions at power-on and two short stretches in
the trampoline (The stacks of xv6), each hart is on exactly one of them,
whichever memory its sp points into:
- a user stack per process, for the program’s own code;
- a kernel stack per process slot, for kernel code running on a process’s behalf;
- a boot stack (stack0) per hart, a slice of
stack0, which becomes the hart’s scheduler stack once boot is over.
Only four instructions ever move sp from one stack to another
(stack pointer and the active stack). The trapframe and the context structures that save registers
are not stacks (save area). See The stacks of xv6.
stack pointer and the active stack
The address in a hart’s sp register: the current top of the stack it is running on. Whichever memory sp points into is the hart’s active stack. Function calls move sp within that stack; in xv6 only four instructions move it to a different one.
Each hart has its own sp. Apart from a few instructions at power-on and two short
stretches in the trampoline (The stacks of xv6), each hart is on exactly one stack,
and with three harts up to three stacks are in use at once. The four instructions that move
sp to another stack are:
| Instruction | Where | Switch |
|---|---|---|
add sp, sp, a0 |
kernel/entry.S:17 |
nothing → boot stack (stack0), once per hart |
ld sp, 8(a0) |
kernel/trampoline.S:76 |
user stack → kernel stack (usable from line 92) |
ld sp, 8(a1) |
kernel/swtch.S:26 |
kernel stack ↔ scheduler stack |
ld sp, 48(a0) |
kernel/trampoline.S:118 |
kernel stack → user stack (usable once sret, line 153, returns to user mode) |
Traps, sret and mret never change sp. Neither does kernelvec, which pushes its
frame onto the stack that is already active. See The stacks of xv6.
standard input, output and error
The Unix convention that every program starts with three open file descriptors: 0 (standard input) to read from, 1 (standard output) for normal results, and 2 (standard error) for error messages.
The program does not open them; it inherits them. In xv6, init opens the
console as descriptor 0 and duplicates it into 1 and 2 (user/init.c), and
every later process inherits these through fork and exec. The shell can replace any
of them before running a program: cat < f makes descriptor 0 the file f,
ls > out makes descriptor 1 the file out, and ls | wc connects descriptor 1 of
ls to descriptor 0 of wc through a pipe.
Keeping errors on a separate descriptor matters when output is redirected: cat a b > out should put the contents in out but still show “cannot open b” on the screen.
That only works if the message goes to descriptor 2 (fprintf(2, ...)), not to
descriptor 1 (printf(...)). Some xv6 utilities get this wrong.
A program that reads its input from descriptor 0 when given no file names, and writes
its results to descriptor 1, works as a filter: it can sit anywhere in a pipeline.
cat, grep and wc are filters in this sense.
stress test
A test that pushes the system hard (many processes at once, a full table, a long stream of operations) to expose bugs that ordinary use rarely triggers, such as races, leaks and exhausted limits. It usually checks that the system survives, not exact results.
Most kernel bugs that survive careful reading are timing bugs: a race condition that needs two CPUs to be in exactly the wrong place, or a leak that matters only after thousands of operations. A stress test makes such events likely by running many operations concurrently and for a long time.
Its success criterion is usually weak: the kernel did not panic, did not hang,
and a few simple invariants still hold (a newly created file can be written and has
size 1, wait returns the right number of children). xv6’s stress tests are
user/grind.c (random system calls, forever), user/forktest.c (fill the
process table), user/stressfs.c and user/logstress.c (concurrent file
writers), and many tests in user/usertests.c.
superblock
Block 1 of the disk, which describes the file system as a whole: its size in blocks, the number of inodes, and where the log, the inode blocks and the free bitmap start. It also holds a magic number identifying an xv6 file system.
Written once by mkfs/mkfs.c when fs.img is built, read once at boot by
readsb into the global sb, and never changed. See disk layout (xv6 file system) for the
values on the default disk.
Sv39
The RISC-V paging scheme xv6 uses: 39-bit virtual addresses translated through a three-level tree of page table pages, each a 4096-byte array of 512 PTEs.
A virtual address is split into five fields:
63 39 38 30 29 21 20 12 11 0
+----------+--------+--------+--------+------------+
| unused | L2 idx | L1 idx | L0 idx | offset |
+----------+--------+--------+--------+------------+
25 bits 9 bits 9 bits 9 bits 12 bits
Bits 63–39 must be copies of bit 38; xv6 keeps bit 38 at 0 (MAXVA), so they are 0.
To translate, the hardware:
- takes the root page-table page from satp and reads entry L2 idx;
- if that PTE is valid and not a leaf, reads entry L1 idx of the page it points to;
- does the same with L0 idx at the last level, which yields a leaf PTE;
- checks the leaf’s permission bits against the access, and forms the physical address as the leaf’s PPN × 4096 + offset.
An invalid PTE at any level, or a permission mismatch, raises a page fault. Each
index is 9 bits because 2^9 = 512 eight-byte PTEs fill exactly one page. Sv39 also
allows leaves at the upper levels (2 MiB and 1 GiB “superpages”); xv6 never creates
them. The translated results are cached in the TLB (translation lookaside buffer). xv6’s walk performs the
same steps in software.
Svadu extension (A and D bits)
A RISC-V extension in which the hardware itself sets the “accessed” (A) and “dirty” (D) bits in a page-table entry when a page is used or written. When Svadu is present but turned off (menvcfg.ADUE = 0), as on QEMU by default, such an access raises a page fault instead and software must set the bits.
system call
A request from a user program to the kernel (read a file, create a process…). The program executes ecall, which traps into the kernel; the kernel does the work and returns the result.
system call arguments
The values a user program passes to a system call. They travel in registers a0–a5, are saved into the trapframe on entry, and the kernel reads them back with argint, argaddr and argstr. The result goes back in a0.
A user program calls a system call stub like an ordinary C function, so the
calling convention has already put the arguments in a0, a1, … The stub
executes ecall without touching them; uservec saves them into the trapframe.
Everything that arrives this way is untrusted. Integers can be anything, and pointers
may point at memory the process does not own, at kernel memory, or nowhere. The kernel
therefore never dereferences a user pointer directly: it copies data in and out with
copyin, copyinstr and copyout, which translate the address through the
process’s own page table and fail if the address is not valid user memory.
system call number
The small integer that names which system call a user program wants (1 = fork, 5 = read, …). The user stub puts it in register a7 before ecall; the kernel uses it as an index into the table syscalls.
The numbers are defined once, in kernel/syscall.h, and that header is included by
both sides: by kernel/syscall.c to build the dispatch table, and by the generated
user stubs in user/usys.S (user/usys.pl), each of which is three instructions:
li a7, SYS_fork
ecall
ret
Because the number is the only thing that connects the two sides, user programs and
the kernel must be built from the same syscall.h.
system call stub
A tiny assembly function, one per system call, that a user program calls like any C function. In xv6 each stub is three instructions: put the system call number in a7, execute ecall to enter the kernel, and ret to the caller with the kernel’s result in a0.
C cannot express “trap into the kernel”, so the boundary between a user program and the
kernel is crossed in assembly. xv6 generates all its stubs with the Perl script
user/usys.pl; the output is user/usys.S, for example:
write:
li a7, SYS_write # 16, from kernel/syscall.h
ecall # trap into the kernel
ret # back to the C caller; a0 holds the result
The stub does not touch the arguments. The C calling convention has already put
them in a0, a1, … before the call, and that is exactly where the kernel looks for
them (system call arguments). On the kernel side, syscall reads a7 from
the trapframe, calls the matching sys_ function, and stores its return value in
the saved a0, which the stub’s ret hands back to the caller.
Real C libraries work the same way, though their stubs usually also convert the
kernel’s error return into the errno variable. xv6 has no errno: a failing call
returns -1 and the program learns nothing more.
test harness
The code that runs a collection of tests: it picks which tests to run, runs each one in a controlled way, decides whether it passed, and reports the results. In xv6 the harness is the end of user/usertests.c: a table of test functions plus runtests, drivetests and main.
A harness separates what is tested (the individual test functions) from how tests are run. That lets the same tests be run all at once, one at a time by name, or over and over, without changing the tests themselves.
usertests’s harness runs every test in a child process created with fork, so a test
that crashes, is killed by the kernel, or deliberately ruins its own memory cannot
take the harness down with it. The child’s exit status is the verdict: 0 means
the test passed, anything else (1 from the test itself, or -1 when the kernel killed
it) means it failed. Before and after the whole run, the harness also counts free
memory pages (countfree), so a kernel that leaks memory fails even if every
individual test passed.
One level up, test-xv6.py is a harness for the harness: it boots xv6 in QEMU, types
usertests at the shell and waits for the line ALL TESTS PASSED. The project’s
continuous integration (CI) job runs it on every change.
timer interrupt
TLB (translation lookaside buffer)
A small cache inside the hart of recent virtual-to-physical translations, so that the page table does not have to be walked on every memory access. It must be flushed with sfence.vma when page tables change.
trampoline page
The one page of code (kernel/trampoline.S) that switches between a user page table and the kernel page table on every trap. It is mapped at the same virtual address, TRAMPOLINE, in every page table, so the code keeps running while satp changes under it.
RISC-V’s trap hardware does not change satp: a trap from user mode lands in the kernel with the user page table still installed. The handler’s first instructions must therefore be mapped in the user page table, and after the instruction that switches to the kernel page table, the next instruction must be found at the same address in the kernel page table. Mapping one physical page at the same virtual address in both solves this.
The page is mapped without PTE_U, so user code cannot read or run it; supervisor mode
can. It sits at the very top of the usable virtual address range,
TRAMPOLINE = MAXVA - PGSIZE = 0x3ffffff000, so it never collides with user memory
that grows up from address 0. The kernel maps it in kvmmake
(kernel/vm.c:47); each process’s page table gets it in proc_pagetable
(kernel/proc.c:189).
transaction
A group of disk writes that must happen all together or not at all. In xv6 each file system operation that may modify the disk (usually one system call) runs between begin_op and end_op, and the writes of all such operations that overlap in time are committed to disk as one transaction.
trap
A forced transfer of control from the running code to a handler, caused either by an exception (the running instruction cannot proceed) or an interrupt (a device or timer wants attention). It is also how system calls enter the kernel.
On a trap the hart saves where it was (in mepc or sepc) and why (in mcause or
scause), switches to the handler’s privilege mode (the same or
a higher one, never lower), and jumps to the handler
address held in mtvec or stvec. Which of the two sets is used is
decided by trap delegation.
trap cause (scause values)
The number the hardware writes into scause to say why a trap happened. Bit 63 is 1 for an interrupt and 0 for an exception; the low bits are the cause code.
The values xv6 tests for:
scause |
Kind | Meaning | Handled in |
|---|---|---|---|
8 |
exception | ecall from user mode (a system call) |
usertrap |
13 |
exception | load page fault | usertrap (lazy allocation) |
15 |
exception | store/AMO page fault | usertrap (lazy allocation) |
0x8000000000000005 |
interrupt | supervisor timer interrupt | devintr |
0x8000000000000009 |
interrupt | supervisor external interrupt (from the PLIC) | devintr |
Other exception codes include 2 (illegal instruction), 5 (load access fault),
7 (store/AMO access fault) and 12 (instruction page fault). xv6 handles none of them:
from user mode the process is killed, and in the kernel kerneltrap panics.
trap delegation
Configuring the hardware so that a trap goes straight to the supervisor-mode handler instead of machine mode. Set with the medeleg and mideleg CSRs.
By default (on QEMU, where the delegation registers start at 0) every trap is handled
in machine mode. xv6’s kernel runs in supervisor mode
and wants to handle traps itself, so start delegates all of them. Delegation only
applies to traps that happen while the hart is in S or U mode; a trap in M mode always
stays in M mode.
trapframe
A per-process page where the trap entry code saves all of a user program’s registers when it enters the kernel, and from which they are restored on the way back. It also holds the few kernel values the entry code needs (kernel stack, page table, handler).
Each process has one, allocated by allocproc and pointed to by p->trapframe. The
same physical page is visible at two addresses:
- at
TRAPFRAME(just below the trampoline page page) in the process’s own page table, which is whereuservecanduserretuse it, because they run while the user page table is still (or already) installed; - at its ordinary direct-mapped physical address in the kernel, which
is how C code reaches it through
p->trapframe.
Its layout is struct trapframe: four kernel fields (kernel_satp,
kernel_sp, kernel_trap, kernel_hartid) plus epc, the saved user PC, followed by
31 user registers. The
assembly in kernel/trampoline.S uses the byte offsets written in the comments
there, so the struct and the assembly must be changed together.
The kernel also uses the trapframe to talk to the user program: system call arguments
are read from its a0–a5 slots, the call number from a7, and the return value is
written into its a0 slot (syscall).
UART
A serial-port device that sends and receives characters one at a time. On QEMU it is connected to your terminal, so it is xv6’s console: everything xv6 prints goes through it (kernel/uart.c).
user memory layout
How a process’s virtual addresses are arranged: program text at 0, then data, a guard page, the stack and the heap; at the very top, the trapframe and trampoline pages.
MAXVA 0x4000000000 +---------------------------+
TRAMPOLINE 0x3ffffff000 | trampoline code R X | (not user-accessible)
TRAPFRAME 0x3fffffe000 | trapframe R W | (not user-accessible)
+---------------------------+
| unmapped |
p->sz ----> +---------------------------+
| heap (sbrk) R W U |
+---------------------------+
| stack (USERSTACK) R W U |
| guard page R W | (U cleared)
+---------------------------+
| data, bss R W U |
| text, rodata R X U |
0 +---------------------------+
p->sz is the size of everything from 0 up to the top of the heap. kexec builds
the lower part; proc_pagetable maps the top two pages. See
kernel/memlayout.h.
user stack
The stack a user program runs on, in its own memory. In xv6 it is USERSTACK (1) page of 4096 bytes, placed right above the program’s code and data, with an inaccessible guard page below it to catch overflow.
kexec creates it, copies the program’s argument strings and argc and argv (program arguments) array to its
top, and sets the saved sp to point at argv. The stack does not grow: a program
whose stack needs more than the page runs into the guard page and is killed. (The
guard is only one page: a single stack frame larger than 4 KiB can jump over it and
land in the program’s data.) It is
separate from the kernel stack the kernel uses while handling the process’s
system calls and traps. See user memory layout.
The stack is mapped only in the process’s own page table. A single instruction moves sp
onto it, userret's ld sp, 48(a0) (kernel/trampoline.S:118), but the hart can use
it only from the sret at kernel/trampoline.S:153, when the mode becomes user
(supervisor code cannot use user pages). It stops being usable at the next trap, which
returns to supervisor mode, and uservec then saves the user’s sp in the trapframe
and loads the kernel stack’s address (kernel/trampoline.S:76). kfork copies it along
with the rest of user memory (uvmcopy); it is freed by freeproc when the
parent’s kwait collects the process. See The stacks of xv6.
virtio
A standard interface for devices that virtual machines such as QEMU provide. xv6’s “hard disk” is a virtio block device, driven by kernel/virtio_disk.c.
virtqueue
The shared-memory mechanism a virtio driver and device use to exchange requests: a table of descriptors (each naming a buffer in memory), an “available” ring where the driver lists requests for the device, and a “used” ring where the device lists the ones it has finished.
All three parts live in ordinary RAM that the driver allocates; the driver tells the
device their physical addresses once, at initialization. After that, submitting a
request is a matter of writing memory plus one write to a notify register, and
completion is signalled by the device writing the used ring and raising an
interrupt. This is the “split virtqueue” layout of the virtio 1.x specification.
See kernel/virtio.h and kernel/virtio_disk.c.
virtual memory
The scheme in which every address a program uses (a virtual address) is translated by the hardware into a physical memory address, using a page table chosen by the kernel. It lets each process have its own private view of memory.
write-ahead log
A crash-safety technique: before changing any block in its real place on the disk, first write all the new block contents to a separate log area, then write a record saying “these changes are complete”. Only then copy them to their real places. xv6’s log is in kernel/log.c.
After a crash, the recovery code looks at the log. If the “complete” record (xv6’s log header with a non-zero count) is there, it copies the logged blocks to their places again, finishing the job; if not, it ignores the log, and none of the changes happened. Either way the disk ends up with all of a transaction's changes or none of them. xv6’s log is a redo log of whole blocks: it records new block contents, never old ones, and replaying it twice gives the same result as replaying it once.
zombie
A process that has exited but whose slot in the process table is not yet free, because its parent has not yet collected its exit status with wait.
kexit cannot mark its own slot UNUSED: another CPU could then reuse the slot,
kernel stack included, while it is still running on it. It also must keep its exit
status somewhere until the parent asks. So it
stores the status in p->xstate, sets p->state = ZOMBIE and switches away for good.
The parent’s kwait later copies out the status and calls freeproc. If the parent
exits first, the zombie is handed to init (reparent), whose endless wait loop
(user/init.c) collects it.
Linker option
-T / -Ttext / -N / -e / -z max-page-size
ld options: -T script use a linker script; -Ttext 0 place code at address 0; -N do not page-align sections; -e main use main as the entry point; -z max-page-size=4096 align the program’s segments to 4096-byte pages (already the RISC-V default).
Linker script
. (location counter)
The address where the linker will place the next thing. Reading . gives the current address; assigning to it (. = 0x80000000;) moves it forward.
ALIGN
ALIGN(n) is the location counter rounded up to a multiple of n (unchanged if it already is one), so . = ALIGN(0x1000); moves to a 4096-byte page boundary.
ASSERT
ASSERT(condition, "message") makes linking fail with the message if the condition is false. A build-time sanity check.
ENTRY
ENTRY(symbol) records symbol’s address as the program’s entry point in the ELF header.
OUTPUT_ARCH
Names the machine architecture of the output file (here riscv).
PROVIDE
PROVIDE(name = value); defines the symbol name, but only if some file uses it and no file defines it itself.
SECTIONS
The block that describes the output file: which output sections exist, in what order, at what addresses, and which input sections go into each.
Make
$(shell ...)
Runs a shell command while the Makefile is being read and uses its output as text.
$(wildcard ...)
Expands to the list of existing files matching a pattern such as kernel/*.c.
-include
Reads other makefiles at this point. With the leading -, files that do not exist are silently skipped.
.PHONY
Declares targets that are names of actions, not files, so make runs them even if a file with that name happens to exist.
.PRECIOUS
Tells make not to delete the listed files (here, every %.o) even when they were only built as intermediate steps.
CURDIR
A variable make sets to the directory it is running in.
ifndef / ifneq / endif
Conditionals evaluated while the Makefile is read: ifndef X keeps the lines up to endif only if variable X is not defined; ifneq (a,b) only if a and b differ.
make automatic variables ($@ $< $^ $*)
Inside a recipe: $@ is the target being built, $< its first prerequisite, $^ all its prerequisites, and $* the part matched by % in a pattern rule (including any directory).
QEMU option
QEMU options
-machine virt emulate QEMU’s generic RISC-V board; -bios none load no firmware, so QEMU’s tiny boot ROM jumps straight to the kernel at 0x80000000; -kernel FILE load this ELF file into memory; -m 128M 128 MiB of RAM; -smp N N harts; -nographic use the terminal as the serial console; -drive/-device attach fs.img as a virtio disk; -gdb tcp::PORT accept a debugger.
RISC-V CSR
mcounteren
Machine counter enable: which counter CSRs supervisor mode may access. Bit 0 = cycle, bit 1 ™ = time (and, with Sstc, also stimecmp), bit 2 = instret.
medeleg / mideleg
Machine exception / interrupt delegation registers. Bit n set means: a trap with cause number n that happens in S or U mode goes to the supervisor-mode handler instead of machine mode (trap delegation).
menvcfg
mepc
Machine exception program counter: the address mret jumps to. On a trap into machine mode the hardware stores the interrupted instruction’s address here.
mhartid
Read-only machine-mode CSR (control and status register) holding this hart's ID number (0, 1, 2…). Supervisor mode cannot read it, which is why xv6 copies it into tp while still in machine mode.
mie
Machine interrupt enable: one bit per kind of machine-level interrupt.
mstatus
Machine status: a machine-mode CSR (control and status register) packed with control fields. xv6 uses its MPP field (bits 12–11), the “previous privilege mode” that mret switches to.
pmpaddr0 / pmpcfg0
The first PMP entry. pmpaddr0 holds an address (shifted right by 2) and pmpcfg0 holds 8-bit permission settings for entries 0–7.
Entry 0’s settings are the low byte of pmpcfg0:
| Bit | Name | Meaning |
|---|---|---|
| 0 | R | reads allowed |
| 1 | W | writes allowed |
| 2 | X | instruction fetch allowed |
| 4–3 | A | how the region is matched: 0 off, 1 TOR, 2 NA4, 3 NAPOT |
| 7 | L | locked (also applies to M mode) |
With A = 1 (TOR, “top of range”), entry 0 covers addresses from 0 up to (not including)
pmpaddr0 × 4.
satp
Supervisor address translation and protection: selects the address-translation scheme and points at the root page table. Value 0 means “Bare”: no translation, virtual address = physical address.
| Bits | Field | Meaning |
|---|---|---|
| 63–60 | MODE | 0 = Bare (paging off), 8 = Sv39 (three-level page tables) |
| 59–44 | ASID | address-space ID (xv6 leaves it 0) |
| 43–0 | PPN | physical page number of the root page table (its address ÷ 4096) |
xv6 builds this value with MAKE_SATP.
sepc / scause / stval / sip / sscratch
Supervisor trap CSRs: sepc holds the address of the interrupted instruction, scause the reason for the trap, stval extra information (such as a faulting address), sip which interrupts are pending, and sscratch is a spare register for trap handlers.
sie
Supervisor interrupt enable: one bit per kind of supervisor interrupt. Bit 9 (SEIE) enables external (device) interrupts, bit 5 (STIE) timer interrupts, bit 1 (SSIE) software interrupts.
SIE (sstatus bit 1)
The global supervisor interrupt-enable bit, bit 1 of sstatus. With it 0, a hart in supervisor mode takes no interrupts; they stay pending. Tours show it as intr: on/off.
A trap clears it (saving the old value in SPIE) and sret restores it. In xv6 it is
cleared by push_off's csrrci and by intr_off, and set by pop_off (only at
noff 0), usertrap's intr_on for a system call, and the scheduler's
one-instruction window. Whenever noff is above 0, SIE is 0. In user mode, supervisor
interrupts are taken whatever SIE says, and wfi wakes on a pending interrupt even with
SIE 0. See Locks and interrupt state and Locks and interrupt state.
sstatus
Supervisor status. Its SIE bit (bit 1) is the global on/off switch for interrupts in supervisor mode; its SPP bit records the mode a trap came from.
stimecmp
Supervisor timer compare (Sstc extension): a supervisor timer interrupt becomes pending whenever time ≥ stimecmp.
stvec
Supervisor trap vector: the address the hart jumps to when a trap is handled in supervisor mode.
time
A read-only counter that increases at a constant rate. On QEMU’s virt machine it ticks 10,000,000 times per second.
RISC-V instruction
add
add rd, rs1, rs2 sets rd = rs1 + rs2.
addi (add immediate)
addi rd, rs, imm sets rd = rs + imm, where imm is a constant from −2048 to 2047.
amoswap.w (atomic swap)
amoswap.w rd, rs2, (rs1) atomically loads the 32-bit word at address rs1 into rd and stores rs2 there, as one indivisible operation that no other hart can interleave with. The .aq suffix adds acquire ordering.
It belongs to the “A” (atomic) extension, included in rv64gc. xv6’s acquire is the
only place in the kernel that uses it (amoswap.w.aq a5,a5,(s1) in kernel/kernel.asm).
With the aq bit set, no later memory operation of the same hart can be observed by
other harts before the swap; see memory barrier (fence).
call
A pseudo-instruction: call f jumps to function f and stores the return address in ra. It assembles to auipc + jalr, which the linker shrinks to a single jal when f is close enough.
csrr (read CSR)
csrr rd, csr copies a CSR (control and status register) into register rd. It is a pseudo-instruction for csrrs rd, csr, x0.
csrrc (read and clear CSR bits)
csrrc rd, csr, rs copies the old value of a CSR (control and status register) into rd and clears the bits that are 1 in rs, as one instruction. csrrci takes a 5-bit constant instead of rs.
csrs / csrc (set / clear CSR bits)
csrs csr, rs sets in a CSR (control and status register) the bits that are 1 in rs; csrc csr, rs clears them. Other bits are unchanged. The i forms (csrsi, csrci) take a 5-bit constant instead of a register.
csrw (write CSR)
csrw csr, rs copies register rs into a CSR (control and status register). It is a pseudo-instruction for csrrw x0, csr, rs.
ecall
Deliberately causes an exception so that more privileged code runs. User programs use it to make a system call.
fence
Orders memory and device accesses: every access of the kinds named before the comma that comes earlier in the program is seen by other harts and devices before every access of the kinds named after it that comes later. fence iorw, iorw orders all of them.
fence.i
Makes this hart’s later instruction fetches see every store to memory that is already visible to this hart. Needed after writing code into memory (for example, after loading a program) and before running it.
j (jump)
j label jumps to label without saving a return address; a pseudo-instruction for jal x0, label.
jalr (jump and link register)
jalr rd, off(rs) jumps to the address in register rs (plus off) and stores the address of the next instruction in rd. Written jalr rs alone, it uses ra as rd, so it is a call through a register.
la (load address)
A pseudo-instruction: la rd, symbol puts the address of symbol into rd. In xv6’s kernel it becomes auipc + addi, computing the address relative to the current instruction.
li (load immediate)
A pseudo-instruction: li rd, constant puts a constant into rd. The assembler picks the shortest real instruction sequence (addi, lui, or several) for the value.
mret (return from machine mode)
Leaves machine mode: the hart switches to the privilege mode stored in the MPP field of mstatus and jumps to the address in mepc. Meant for returning from a trap, but also usable to enter a lower mode for the first time.
In detail, mret:
- sets the privilege mode to
mstatus.MPP, then setsMPPto U (the lowest mode); - copies
mstatus.MPIEintomstatus.MIE(restoring the machine interrupt enable) and setsMPIEto 1; - sets the program counter to
mepc.
mul (multiply)
mul rd, rs1, rs2 sets rd to the low 64 bits of rs1 × rs2. It belongs to the “M” extension, which rv64gc includes.
mv (move)
mv rd, rs copies rs into rd; a pseudo-instruction for addi rd, rs, 0.
ret
Returns from a function by jumping to the address in ra; a pseudo-instruction for jalr x0, 0(ra).
sd / ld (store / load doubleword)
sd rs, off(base) stores the 8 bytes of rs at address base + off; ld rd, off(base) loads 8 bytes from there into rd.
sfence.vma
Orders earlier writes to page tables before later address translations, and discards cached translations in the TLB (translation lookaside buffer). Needed whenever page tables or satp change.
sret (return from supervisor mode)
Returns from a trap handled in supervisor mode: the hart switches to the mode recorded in sstatus.SPP (user or supervisor) and jumps to the address in sepc.
In detail, sret:
- sets the privilege mode to
sstatus.SPP(0 = user, 1 = supervisor), then setsSPPto 0 (user); - copies
sstatus.SPIEintosstatus.SIE(restoring the interrupt enable that was in force before the trap) and setsSPIEto 1; - sets the program counter to
sepc.
It is the mirror image of what the hardware does on trap entry (save the mode in SPP,
save SIE in SPIE, clear SIE, save the PC in sepc). xv6 also uses it to enter
user mode for the first time, by filling SPP, SPIE and sepc itself
(prepare_return).
wfi (wait for interrupt)
A hint that the hart has nothing to do and may pause (saving power) until an interrupt is pending.
RISC-V register
a0–a7 (argument registers, x10–x17)
Registers that carry a function’s first eight arguments; a0 (and a1) also carry the return value. In a system call a7 holds the call number.
gp (global pointer, x3)
A register reserved by convention for reaching small global data quickly. The xv6 kernel does not use it for that.
ra (return address, x1)
The register where a call instruction stores the address to return to. ret jumps back to it.
s0–s11 (saved registers)
Registers a called function must leave unchanged (saving and restoring them if it uses them). s0 doubles as the frame pointer fp.
sp (stack pointer, x2)
The register holding the address of the top of the current stack. Compiled C code assumes sp points at valid, 16-byte-aligned memory.
t0–t6 (temporary registers)
Scratch registers. A called function may overwrite them, so the caller must save them if it needs their values afterwards.
tp (thread pointer, x4)
A general-purpose register reserved by convention for a “thread pointer”. The xv6 kernel uses it to hold the current hart’s ID, so cpuid can find it quickly.
zero (x0)
A register that always reads as 0; writes to it are discarded.
Toolchain
.bss section
The section for global and static variables that start as zero (int x;). The program file records only its size, not its contents; the memory must be zero when the program starts. .sbss is the same for small objects.
.data section
The section for global and static variables that have a non-zero initial value (int x = 5;). Their initial bytes are stored in the program file. .sdata is the same for small objects.
.rodata section
The section for read-only data, such as string literals and const tables. .srodata is the same for small objects.
.text section
The section holding machine code (instructions).
C preprocessor
The first stage of compiling C: it handles #include, #define and #if by editing the source text before the compiler proper sees it. Assembly files named .S (capital S) are also run through it, which is how kernel/trampoline.S can use #defines from kernel/memlayout.h.
C runtime start-up code
The code that runs before main in a user program and calls it. It receives the arguments the kernel set up, calls main(argc, argv), and passes main’s return value to exit. In xv6 this is the function start in user/ulib.c.
A C program’s life does not begin in main. The operating system starts a program at
its entry point, an address recorded in the ELF header, and something there
has to turn the raw starting state (registers, a stack) into an ordinary call to main,
and has to deal with main returning, which the kernel knows nothing about.
On Linux this is the file crt0.o or crt1.o (“C runtime”), whose entry symbol is
_start, and it does much more: it sets up the C library, the environment variables
and atexit handlers. xv6 needs only the essentials, written in C:
void start(int argc, char **argv) { exit(main(argc, argv)); }
kexec puts argc in a0 and argv in a1, so by the
calling convention they arrive as start’s two parameters. user/user.ld names
no entry symbol, and GNU ld then picks the symbol called start.
continuous integration (CI)
A service that automatically builds and tests a project every time someone pushes a change, on fresh machines, so that a change that breaks the build or a test is noticed at once. xv6 uses GitHub Actions, configured in .github/workflows/test.yml.
A CI configuration lists jobs; each job names a kind of machine to run on and a
sequence of steps (shell commands). If any command exits with a non-zero status the
job fails, and GitHub marks the commit or pull request with a red cross. xv6’s jobs
build the kernel and fs.img on Linux and macOS, check the formatting, and on Linux run
test-xv6.py, which boots xv6 in QEMU and runs usertests and the crash tests.
cross-compiler / toolchain
A compiler, assembler and linker that run on one machine (your Mac or PC) but produce code for another (RISC-V). The tool names carry a prefix saying the target, e.g. riscv64-unknown-elf-gcc; the Makefile stores it in TOOLPREFIX.
disk image (fs.img)
A file whose bytes are the contents of a whole disk. make builds fs.img with mkfs/mkfs.c and QEMU presents it to xv6 as a virtio disk, holding the file system with /init, the shell and the other programs.
ELF
The standard file format for programs and object files on Unix-like systems. Its header records, among other things, the entry point and which bytes to load at which addresses. Both kernel/kernel and xv6’s user programs are ELF files.
entry point
The address of a program’s first instruction, recorded in its ELF header. For the kernel it is _entry, set by ENTRY(_entry) in kernel/kernel.ld.
freestanding (vs. hosted) C
C without an operating system underneath: no standard library functions (only a few headers such as <stdarg.h> and <stdint.h>), no printf or malloc unless you write them, and main is not special. A kernel is freestanding code; -ffreestanding tells the compiler so.
gdb (the GNU debugger)
A debugger: a program that lets you stop another program, step through it line by line or instruction by instruction, set breakpoints, and inspect registers and memory. For xv6 you run a RISC-V-capable gdb (${TOOLPREFIX}gdb, with your RISC-V toolchain’s prefix, or gdb-multiarch) on your computer and connect it to QEMU with make qemu-gdb.
QEMU contains a small gdb stub: with the -gdb tcp::PORT option it listens on a TCP
port and lets gdb control the emulated machine through gdb’s remote protocol. gdb then
sees the emulated harts as threads, can stop them anywhere, even in the kernel’s first
instruction, and uses the symbols and line numbers in kernel/kernel (compiled with
-ggdb) to show you C source. The commands gdb runs at start-up come from .gdbinit,
generated from .gdbinit.tmpl-riscv by the Makefile (Makefile:185).
A few commands to start with: b kvminithart (breakpoint on a function), c
(continue), si (step one instruction), n (next C line), info registers, x/4x ADDR (show memory), bt (stack backtrace).
linker (ld)
The tool that combines object files into one program: it decides the final address of every piece of code and data, and fills in each reference to a symbol with the symbol’s address. A linker script controls the layout.
linker script
A file that tells the linker (ld) exactly where to place each kind of section in memory and which symbols to define. xv6 has one for the kernel (kernel/kernel.ld) and one for user programs (user/user.ld).
make / Makefile
A build tool. A Makefile lists targets (files to build), the prerequisites each one depends on, and the shell commands (the recipe) that build it. make rebuilds a target only when a prerequisite is newer than it.
A rule looks like this (the recipe line must start with a tab):
target: prerequisite1 prerequisite2
command that builds target
Variables are set with = (expanded every time they are used), := (expanded once,
now), += (append) or ?= (set only if not set yet), and used as $(NAME), or $X
for a one-letter name. A pattern rule such as %.o: %.S is a template for many
targets; % matches any stem. If no rule in the Makefile matches, make falls back to its
built-in implicit rules, such as compiling x.c into x.o with $(CC) $(CFLAGS) -c.
object file (.o)
The output of compiling or assembling one source file: machine code and data split into sections, plus a list of the symbols it defines and the ones it still needs from other files. The linker (ld) combines object files into a program.
program header (segment)
An entry in an ELF executable’s table of segments. Each one says: take filesz bytes from this offset in the file, place them at this virtual address, reserve memsz bytes of memory in total (the rest zero-filled), with these permissions. In xv6 it is proghdr.
The loader (kexec in xv6) only needs the program headers, not the finer-grained
section headers that linkers and debuggers use. An xv6 user program has two loadable
segments: code plus read-only data (readable and executable) at address 0, and data
plus bss (readable and writable) at the next page boundary (all except _forktest,
which is linked without user.ld and has a single read-write-execute segment). You can list them with
${TOOLPREFIX}readelf -l user/_cat (TOOLPREFIX is your RISC-V toolchain’s prefix; see Tour 1: From make qemu to a disk image and a kernel).
pseudo-instruction
An assembly-language shorthand that the assembler turns into one or more real instructions. la, li, call, j, mv, csrr and csrw are all pseudo-instructions.
QEMU
An emulator: a program that pretends to be a whole computer. qemu-system-riscv64 -machine virt emulates a RISC-V board with RAM, a UART, a disk and an interrupt controller, and is the only “hardware” xv6 runs on in this course.
section
A named chunk of an object file (.o) or program holding one kind of content: .text (code), .rodata (constants), .data (initialized variables), .bss (zero-initialized variables).
small data sections (.sdata, .sbss, .srodata)
RISC-V compilers put small variables in separate “small” sections so they can be reached with one instruction relative to the gp register. xv6 does not use that optimization, so its linker scripts just merge them with the normal sections.
symbol
A name for an address in a program: a function, a global variable, an assembly label, or a name defined in a linker script. The linker (ld) matches each use of a symbol with its single definition.