Tour 24 · Memory · about 31 minutes · 19 steps
Until now, every address the kernel used went straight to physical memory. That cannot last: user processes need their own private views of memory, kernel stacks need guard pages, and the trampoline must appear at the same address in every address space. All of that needs virtual memory, and virtual memory needs a page table.
This tour watches hart 0 build the kernel’s page table during main, one mapping at a
time, with real addresses and real page-table entries read out of memory with gdb. You
will see walk allocate the tree’s pages as it goes (each one through kmem.lock),
the device registers, kernel code, RAM, the trampoline and 64 kernel stacks being mapped,
and then the single csrw satp that switches translation on, first on hart 0, later on
harts 1 and 2.
The page table that results is shared by all three harts and is never changed by software again. Understanding why that makes it lock-free, and why each hart must still flush its own TLB (translation lookaside buffer), is the point of the tour.
Best after: 3. main: one hart builds the kernel, the others wait
| Hart | What it is doing |
|---|---|
| 0 | In main, just past kinit: about to call kvminit. Paging off, interrupts off |
| 1 | Spinning on started with paging off (Tour 3: main: one hart builds the kernel, the others wait) |
| 2 | The same as hart 1 |
The free-page list holds every page from 0x80021000 to 0x88000000, with 0x87fff000
at its head.
stack0hart 0’s slice of stack0: 0x80007890–0x80008890add sp, sp, a0 in _entry (kernel/entry.S:17), at power-onStep 1 of 19
kvminit (line 69) calls kvmmake, and kvmmake starts with one page from
kalloc: the root of the page table. It is the head of the free list,
0x87fff000, the highest page of RAM. Line 27 zeroes it: 512 entries, all invalid.
A page table on RISC-V is a tree of 4096-byte pages, each holding 512 eight-byte page-table entries (PTEs). The CPU’s hardware walks this tree on every memory access it cannot answer from its TLB (translation lookaside buffer). The kernel’s job is only to fill it in.
Paging is still off on hart 0 (satp is 0 since start), so kvmmake writes the
tree through physical addresses. Its own stack is physical too: hart 0 is still on the
boot stack that _entry set up, its 4 KiB slice of stack0, a plain array in
the kernel’s .bss (The stacks of xv6). That works because nothing is translated yet; later,
with paging on, it will keep working because the kernel maps physical memory at the
same addresses (direct map).
stack0Step 2 of 19
xv6 uses Sv39: 39-bit virtual addresses, three levels. A virtual address
splits into three 9-bit indexes and a 12-bit offset, which PX extracts:
| Bits | Meaning | 0x10000000 (the UART) |
0x3ffffff000 (TRAMPOLINE) |
|---|---|---|---|
| 38–30 | index in the root (level 2) | 0 | 255 |
| 29–21 | index in a level-1 page | 128 | 511 |
| 20–12 | index in a level-0 page | 0 | 511 |
| 11–0 | byte in the page | 0 | 0 |
A PTE holds a physical page number in bits 53–10 (PA2PTE, PTE2PA) and flags
in the low bits: V valid, R read, W write, X execute, U user. A valid PTE
with none of R, W, X points to the next level down; one with any of them is a
leaf. xv6 puts leaves only in level-0 pages, where each maps one 4 KiB page (a leaf
higher up would map a 2 MiB or 1 GiB “superpage”, which xv6 never uses).
MAXVA is 1 << 38, 256 GiB: xv6 uses only the lower half of the Sv39 space, to
avoid the sign-extension rule for addresses with bit 38 set. So the root index never
exceeds 255.
stack0Step 3 of 19
Every mapping in kvmmake goes through kvmmap, which calls mappages and
panics on failure (at boot, failure means the machine is broken).
mappages maps a range page by page: for each page, walk finds (and if needed
creates) the level-0 PTE, and line 168 fills it with the physical page number, the
permissions and PTE_V. The remap panic on line 167 catches a kernel bug where two
mappings overlap.
There is no huge-page support here. The 128 MiB of RAM become 32,768 separate 4 KiB
mappings, written one by one. It costs a little memory and boot time and keeps
walk simple.
stack0Step 4 of 19
walk descends from the root. At each level it reads the PTE for this address’s
index. If it is valid, it follows it down. If not, and alloc is set, it takes a new
page from kalloc, zeroes it, and points the PTE at it (with only V set: an inner
node).
The very first mapping, the UART at 0x10000000, shows it from scratch. With gdb,
after kvmmake finished:
| Level | Table page | Index | PTE |
|---|---|---|---|
| 2 | 0x87fff000 (root) |
0 | 0x21fff801 → 0x87ffe000 |
| 1 | 0x87ffe000 |
128 | 0x21fff401 → 0x87ffd000 |
| 0 | 0x87ffd000 |
0 | 0x4000007: page 0x10000000, R W V |
The two lower pages, 0x87ffe000 and 0x87ffd000, are the next two pages
kalloc handed out, allocated by walk for this mapping. The VIRTIO0 mapping
that follows, at 0x10001000, reuses both: same level-1 and level-0 pages, index 1.
stack0acquire’s push_off, so intena is 0 and release leaves them offkmem.lockStep 5 of 19
Each new table page comes from kalloc, which pops the free list under
kmem.lock. By the end of kvmmake it will have done this 166 times: 102
page-table pages and 64 kernel-stack pages. The free list’s head moves from
0x87fff000 down to 0x87f59000.
Here every one of those acquisitions succeeds on the first amoswap. Hart 0 is the
only hart running kernel code, and its interrupts are off, so nothing can compete for
the lock and nothing can interrupt the critical section. The lock is taken because
kalloc does not know it is being called at boot, and should not have to.
push_off copes with boot too: interrupts were already off when it ran, so it
records intena 0 and the release leaves them off (Locks and interrupt state).
Note also line 80: kalloc fills each page with junk (5 bytes) after taking it.
walk zeroes table pages itself (line 111), because a table full of 0x05 bytes
would be full of “valid” entries.
stack0Step 6 of 19
The first three mappings make the device registers reachable once paging is on, each at the same virtual address as its physical address, readable and writable, not executable:
| Device | Address | Size | Level-0 tables |
|---|---|---|---|
| UART | 0x10000000 |
1 page | 0x87ffd000 (shared with virtio) |
| virtio disk | 0x10001000 |
1 page | 0x87ffd000 |
| PLIC | 0x0c000000 |
64 MiB = 16,384 pages | 32 tables, 0x87ffc000 down to 0x87fdd000 |
The PLIC is large because it has register blocks for many harts and many interrupt
sources; xv6 maps all of it rather than work out which pages it uses. Its range,
0x0c000000–0x10000000, covers level-1 indexes 96–127 of the first gigabyte, each
needing its own level-0 table.
Without these mappings, the first printk after paging was turned on would fault:
uartputc_sync writes to 0x10000000 with a plain store, and with paging on that
store is translated like any other.
stack0Step 7 of 19
Then all of RAM, in two parts split at etext (0x80007000, page-aligned by the
linker script, Tour 1: From make qemu to a disk image and a kernel):
| Range | Pages | Permissions | Sample PTE |
|---|---|---|---|
0x80000000–0x80007000 |
7 | R X |
0x2000000b for 0x80000000 |
0x80007000–0x88000000 |
32,761 | R W |
0x20001c07 for 0x80007000 |
They need a new level-1 page for the third gigabyte (0x87fdc000) and 64 level-0
pages, 0x87fdb000 down to 0x87f9c000.
Code is not writable, so a stray store into the kernel’s instructions faults instead of silently corrupting them. Data is not executable.
The second range is far more than the kernel’s data. It is every free page:
everything kalloc will ever hand out, including the pages of this very page table.
The kernel can read and write any physical page through its own address, which is how
walk can follow PTE2PA(*pte) as an ordinary pointer once paging is on, and how
copyin reaches user pages (Tour 28: Crossing the user/kernel boundary in memory).
stack0Step 8 of 19
The trampoline page, physically at 0x80006000, is already mapped at 0x80006000 by
the previous step. Line 47 maps it a second time, at TRAMPOLINE =
0x3ffffff000, the highest page below MAXVA, read-execute. Its PTE is
0x2000180b, the same physical page as the 0x80006000 mapping.
Why twice? Every user page table maps the trampoline at 0x3ffffff000 too
(proc_pagetable). The trap code in the trampoline switches satp between a user
table and this one (Tour 7: The trampoline and the trapframe). The instruction after the csrw satp is fetched
under the new table, so it must be at the same virtual address in both. The
direct-mapped copy at 0x80006000 does not exist in user tables; the one at
0x3ffffff000 exists everywhere.
This mapping needs a new level-1 page (0x87f9b000, root index 255) and level-0 page
(0x87f9a000, index 511). The kernel stacks, next, will share that level-0 page.
stack0Step 9 of 19
proc_mapstacks gives each of the 64 process slots one page of kernel stack,
mapped just below the trampoline at KSTACK(i) = TRAMPOLINE - (i+1) × 2 × 4096:
| Slot | Virtual address | Physical page | Level-0 index |
|---|---|---|---|
proc[0] |
0x3fffffd000 |
0x87f99000 |
509 |
| (gap) | 0x3fffffe000 |
none: PTE is 0 | 510 |
proc[1] |
0x3fffffb000 |
0x87f98000 |
507 |
proc[63] |
0x3ffff7f000 |
0x87f5a000 |
383 |
Every other page is left unmapped, so each stack has an unmapped page directly
below it: for proc[0], the gap at 0x3fffffc000 (which is also the gap above
proc[1]'s stack). Stacks grow down, so a kernel stack that overflows runs into that
page, and the access faults instead of silently overwriting a neighbor’s data
(guard page). (What happens after the fault is messier; see the quiz.) With a
direct-mapped stack, an overflow would silently overwrite whatever page happened to
be adjacent in physical memory.
These stacks are virtually contiguous and physically scattered: exactly what a page table is for.
Some facts worth fixing in mind now, because every later tour depends on them:
proc[], not to a process: procinit records p->kstack = KSTACK(slot) (kernel/proc.c:57), fork never allocates a kernel stack and
exit never frees one (Tour 20: fork, Tour 21: exit, wait and zombies).uservec switches satp to the kernel table before the first push on the kernel
stack (Tour 7: The trampoline and the trapframe).stack0, 0x80007890–0x80008890, inside the direct-mapped RAM range. Below
hart 0’s slice lie ticks, initproc, kernel_pagetable and started
(kernel/kernel.sym); below hart 1’s slice lies hart 0’s. An overflow there would
corrupt them silently. That slice becomes hart 0’s scheduler stack later and stays
unguarded for the machine’s whole life.stack0Step 10 of 19
kvmmake returns the root, and line 69 stores it in the global
kernel_pagetable = 0x87fff000. The root has exactly three valid entries:
| Root index | Covers | Level-1 page | What is mapped |
|---|---|---|---|
| 0 | 0x0–0x3fffffff |
0x87ffe000 |
PLIC, UART, virtio |
| 2 | 0x80000000–0xbfffffff |
0x87fdc000 |
kernel and all RAM |
| 255 | 0x3fc0000000–0x3fffffffff |
0x87f9b000 |
trampoline and kernel stacks |
In total: 102 page-table pages (1 root, 3 level-1, 98 level-0), which is 408 KiB,
holding 49,219 leaf entries (1 + 1 + 16,384 device pages, 7 + 32,761 RAM pages, 1
trampoline, 64 stacks), plus the 64 stack pages themselves. That matches the free
list’s head moving from 0x87fff000 to 0x87f59000: 166 pages.
Nothing here is mapped with PTE_U. User mode, if it somehow used this table, could
touch none of it.
stack0Step 11 of 19
Line 21 of main calls kvminithart. It writes satp with
MAKE_SATP(kernel_pagetable):
| Bits | Field | Value |
|---|---|---|
| 63–60 | MODE |
8 = Sv39 (SATP_SV39) |
| 59–44 | ASID |
0 |
| 43–0 | PPN: root page number |
0x87fff000 >> 12 = 0x87fff |
Together: 0x8000000000087fff. The compiler turned this into a load of
kernel_pagetable, a right shift by 12, and an OR with 1 << 63, just before the
csrw at 0x80000f2e.
xv6 never uses the ASID field (address-space identifier). Because of that it must
flush the TLB on every satp change; with ASIDs, entries for different address spaces
could coexist in the TLB.
stack0Step 12 of 19
sfence_vma executes sfence.vma zero, zero. The comment says why: “wait for any
previous writes to the page table memory to finish”. The page-table walker is a
separate part of the CPU that reads memory on its own. The RISC-V spec does not
promise that it sees this hart’s earlier ordinary stores to the page table unless an
sfence.vma comes between them. Hart 0 just wrote tens of thousands of PTEs with
ordinary stores; the fence makes sure the walker will see all of them.
This instruction affects only hart 0. It orders hart 0’s own stores against hart 0’s own page-table walks. It says nothing to harts 1 and 2.
stack0Step 13 of 19
csrw satp, a5 at 0x80000f2e. From the very next instruction on, every address hart
0 uses, for fetching instructions, for loads, for stores, goes through the tree.
The next instruction is at 0x80000f32. Nothing in the program says “jump to the
virtual version of the kernel”; the program counter simply keeps counting. This works
only because the kernel is mapped at the address where it already is: virtual
0x80000f32 translates (root index 2, level-1 index 0, level-0 index 0) to physical
0x80000f32. If the kernel had been mapped anywhere else, this fetch would get some
other bytes, or fault, and since stvec is not set yet the fault would go to whatever
address stvec held at reset.
The same holds for the stack: sp is in hart 0’s slice of stack0, just below
0x80008890, which is in the read-write range, so kvminithart’s ret and everything after keep working.
sp is not changed by csrw satp: the very same number simply starts being
translated, and the direct map translates it to itself. (The kernel stacks at
KSTACK(i) are the opposite case: their addresses are not physical addresses, so they
work only while this table is installed.)
stack0Step 14 of 19
The second sfence.vma discards any cached translations in hart 0’s
translation lookaside buffer, the small per-hart cache of recent
virtual-to-physical lookups. Here there are none worth keeping (in Bare mode there
are no translations to cache), but the rule is: after changing satp or a page table,
fence before relying on the new translations. xv6 follows it every time it installs a
page table: here, and in the trampoline (kernel/trampoline.S:95,
kernel/trampoline.S:112). The only satp write without fences is start's
w_satp(0), which turns translation off at reset, when nothing is cached.
From now on, hart 0’s TLB will fill with kernel translations as it runs:
0x80000f32 → 0x80000f32, 0x10000000 → 0x10000000, and so on. Each hart has its
own TLB. Nothing hart 0 caches is visible to hart 1, and nothing hart 0 flushes is
flushed for hart 1.
stack0each hart its own slice of stack0: tops 0x80009890 and 0x8000a890Step 15 of 19
Much later in story time, after userinit, hart 0 sets started with a release
store. Harts 1 and 2 see it with acquire loads, leave their loop, and each prints
hart N starting with printk.
They are still in Bare mode here. The acquire load of started, the printk and the
UART store at 0x10000000 all use physical addresses, exactly as they did before
hart 0 built anything. That works because the kernel’s page table maps every one of
those addresses to itself: the code is about to switch views, and both views agree.
The acquire is what matters for the next step. Hart 0’s stores to the 102 table pages
and to kernel_pagetable happened before its release store of started, so once a
hart has seen started == 1 with acquire ordering, it is guaranteed to see all of
them too (Tour 3: main: one hart builds the kernel, the others wait).
stack0each hart its own slice of stack0, now reached through the direct mapStep 16 of 19
Line 39 of main: each of harts 1 and 2 calls kvminithart: fence, csrw satp
with the same 0x8000000000087fff, fence. The state above is after the csrw. Two things had to be true for this to be safe:
sfence.vma orders every
store already visible to the executing hart before that hart’s later page-table
walks. Thanks to the acquire, hart 0’s page-table stores are already visible to
harts 1 and 2, so each hart’s first sfence.vma guarantees its walker sees the
finished table.Each hart flushes its own TLB. No hart can flush another’s: sfence.vma is local.
stack0each hart on its own slice of stack0wfi; for most of the loop each hart holds some p->lock (noff 1)Step 17 of 19
Three harts use kernel_pagetable for the rest of the machine’s life: every kernel
instruction fetch, every kernel load and store, on every hart. There is no lock around
it. Why that is safe:
kvmmap says “only used
when booting”, and indeed its only callers are kvmmake and proc_mapstacks.
Even the kernel stacks of processes that come and go are mapped once, for all 64
slots, up front. Process creation and exit change only user page tables.One honest qualification: the hardware may write it. start enabled
Svadu, so when a hart uses a leaf PTE whose accessed (A) bit is clear, or
stores through one whose dirty (D) bit is clear, the hardware sets that bit in
memory. The PTEs above were written with
A = D = 0 (0x...07, 0x...0b). The RISC-V spec requires those updates to be
atomic, and xv6 never reads the A and D bits, so the concurrent hardware updates
do not need a software lock either.
Step 18 of 19
After boot, harts spend much of their time in user page tables. They return to the
kernel’s table on every trap. prepare_return stores the current satp, the
kernel’s, in each process’s trapframe as kernel_satp (line 116), and uservec
in the trampoline loads it back into satp, with an sfence.vma on each side, on the
way into the kernel (Tour 7: The trampoline and the trapframe).
Line 117 stores the other half of the pair: kernel_sp = p->kstack + PGSIZE, the top
of this process’s KSTACK page. That address means something only under this table.
uservec loads it into sp (kernel/trampoline.S:76) while the user table is
still installed, but does not push anything until after csrw satp has brought this
table back; the first push is usertrap's prologue.
So the value 0x8000000000087fff circulates through every process’s trapframe, and
any hart can switch to it at any moment. Because the table never changes, it does not
matter which hart saved it or when.
The price of xv6’s simple design shows up here: every switch between a user table and this one flushes the hart’s whole TLB, so after each system call the kernel starts with a cold TLB and refills it, one walk per page touched. Kernels that care more about speed map the kernel into every user page table, or use ASIDs.
stack0each hart on its own slice of stack0wfi; for most of the loop each hart holds some p->lock (noff 1)Step 19 of 19
The kernel page table cost 166 pages from the allocator (102 table pages, 64 stacks),
about 49,000 PTE writes, 166 trips through kmem.lock, and one csrw satp plus two
sfence.vma on each hart: 3 satp writes that turn paging on (after the 3
w_satp(0) in start that turned it off) and 6 fences for the whole machine.
What it bought:
stack0 do not: they are
plain direct-mapped RAM.)And the concurrency lesson of this tour: build shared data on one hart, publish it
with a release/acquire pair, never modify it again, and it can be shared by every hart
with no lock at all. The only per-hart work left is the part that is truly per-hart:
each CPU’s own satp and its own TLB.
Tour 24 · wrap-up
| Lock | Taken in | Protects |
|---|---|---|
kmem.lock | kalloc, from kvmmake (the root), walk (101 table pages) and proc_mapstacks (64 stacks) | The free-page list; uncontended at boot, but the same code runs on any hart later |
started (release/acquire, not a lock) | main | Makes hart 0’s page-table stores visible to harts 1 and 2 before they load satp |
(no lock) building the table | kvmmake, mappages, walk | Only hart 0 runs kernel code during boot, and the table is unpublished until started |
(no lock) kernel_pagetable after boot | every hart’s page-table walker; kvminithart; uservec | Never written by software after boot, so any number of harts can read it; hardware A/D updates are atomic |
(no lock) satp and the TLB | kvminithart, sfence_vma | Per-hart: each hart writes its own satp and flushes its own TLB |
Why does the instruction right after csrw satp in kvminithart not crash, even though nothing jumps to a new address?
The kernel’s code is mapped at virtual addresses equal to its physical addresses
(KERNBASE to etext). The program counter’s value means the same location before
and after translation is turned on, so the next fetch finds the next instruction.
Hart 0 executed sfence.vma after turning on paging. Why must harts 1 and 2 execute their own?
sfence.vma acts only on the hart that executes it: it orders that hart’s own
page-table accesses and flushes that hart’s own TLB. Each hart has its own TLB and its
own walker, so each must fence for itself.
walk zeroes each new table page with memset, although kalloc already filled it. What would happen without that memset?
kalloc fills pages with the byte 5, so every PTE would read 0x0505050505050505,
which has V set. In a fresh level-1 page, walk would follow that “valid” entry to
PTE2PA(0x0505…) = 0x1414141414141000, which is not memory, and the next access
would fault. In a fresh level-0 page, mappages would find V already set and panic
with “remap”.
The kernel page table is read by three harts’ walkers concurrently with no lock. Why is this safe, and what would change if xv6 started adding kernel mappings after boot?
After boot no software writes it, so there is nothing to order; readers cannot see a
half-made change. If xv6 added mappings later, it would need a lock to serialize
writers, and if it ever removed or changed a mapping, it would also have to make every
other hart flush its TLB (a TLB shootdown), since sfence.vma is local.
Why is the trampoline page mapped twice in the kernel page table?
Once at 0x80006000 as part of the direct map of kernel code, and once at
TRAMPOLINE (0x3ffffff000), the address where every user page table also maps it.
The trap code switches between user and kernel page tables while executing in that
page, so it must be at the same virtual address in both.
What would happen if a kernel stack overflowed by a few hundred bytes?
The first store below the stack lands in the unmapped guard page and raises a page
fault, so the overflow is not silent. xv6 does not recover cleanly, though.
kernelvec saves registers on the same overflowed stack, which faults again, 256
bytes lower each time, until sp reaches the next mapped page: the top of the
neighbouring slot’s kernel stack. There the save succeeds and kerneltrap panics.
The guard page turns a silent overflow into a crash, but in xv6 it does not fully
protect the neighbour.
Keys: ← → step · Home start