Lab 16 · reveal · 20 steps · 11 commits
In this tree a process can use only as much memory as there are free pages. When
kalloc finds its free list empty, sbrk fails and a lazy page fault kills the process.
In this lab you make the kernel take a page that some process has not used for a while,
write it to a reserved area at the end of the disk, and give its physical page to whoever
needs it. When the owner touches that page again, it takes a page fault, and the
kernel reads the page back in. The test program writes and checks 36,000 pages on a
machine with 32,768 pages of RAM.
Swapping touches more of the kernel than any lab before it, because it moves pages out
from under processes that are not expecting it. Where on the disk do the pages go, and
through which layer? How does a PTE say “this page is on disk, in slot 2128”? Which page
do you pick, and how do you get from a physical page to the PTE that maps it? The page you
pick may belong to a process that is running on another hart at that very moment:
what does that hart’s TLB (translation lookaside buffer) still believe? Eviction needs the disk, and the disk
sleeps: from which of kalloc's many callers is that allowed? The kernel copies into
user memory while holding spinlocks: what if the page it copies to is on disk? The think
section asks these questions in the order a designer meets them.
The reference solution is twelve commits. It passes usertests -q on three harts, and
during that run it writes about 49,000 pages to swap without anyone noticing.
Each step shows one change on the branch ext/16-swap, the code around it, and the state of the machine when that code runs.
mkfs/mkfs.cStep 1 of 20 · commit 1: Reserve a swap area at the end of the disk
The story of this tour comes from gdb runs of the reference branch on three harts
(QEMU 10.2.1), mostly while swaptest runs: its big test (pid 4)
writes and checks 36,000 pages, the main swaptest process (pid 3) waits for it, and
later tests fork, use a pipe, and run three big processes at once. Where a state below
was not recorded, the note says so.
Commit 1 changes no kernel code. mkfs used to write exactly FSSIZE = 2,000 blocks;
now it also writes the very last block of a swap area of NSWAP = 8,192 slots of 4,096
bytes. The file grows to 34,768 blocks (35,602,432 bytes); the blocks in between are
never written by mkfs and read back as zeros. param.h gains NSWAP and SWAPSTART
(= FSSIZE), shared by mkfs and the kernel.
The superblock’s size stays 2,000. That one decision is the whole protection: the
kernel’s balloc only hands out blocks below sb.size
(kernel/fs.c:74), so no file can ever be given a block of the swap area, and no
code path except the swapping code will ever name one.
QEMU’s virtio disk takes its size from the image file, so the bigger file is all the
device needs. (A smaller image would make the device fail the request; the driver then
panics with virtio_disk_intr status; reasoned from the driver, not run.)
kernel/virtio_disk.cStep 2 of 20 · commit 2: Let the disk driver transfer a whole page
virtio_disk_rw used to take its sector, data address and length from the buffer:
b->blockno, b->data, BSIZE. Its body is now disk_rw (line 218), which takes
them as arguments; virtio_disk_rw passes the old values, so the file system sees no
change.
virtio_disk_rwpage asks for PGSIZE = 4,096 bytes starting at blockno, four
consecutive blocks, in one request. The data descriptor points at the physical page
itself (line 255 of disk_rw): the device reads or writes guest physical memory, and
in the kernel’s direct map a page’s address is its physical address. No copy through
a buffer.
The struct buf argument survives for one reason: the completion protocol. disk_rw
sets b->disk = 1, sleeps on b, and virtio_disk_intr clears b->disk and wakes
b (lines 287–292 and kernel/virtio_disk.c:326). The swap code passes a
struct buf of its own, used only as that sleep channel and flag; its 1,024-byte
data array is never touched.
Getting PGSIZE wrong here (clinic 6) loses three quarters of every page, silently.
kernel/swap.cStep 3 of 20 · commit 3: Add swap slots
The new file starts with the slot allocator. SLOTBLOCK(s) is the first block of slot
s: 2,000 + 4s.
A slot is not just used or free: ref[s] counts the PTEs that refer to it, because
fork will let a parent and a child share one swapped-out page (commit 5). swapalloc
takes the first slot with count 0 and sets it to 1; swapdup adds one; swapfree
drops one, and the slot is free again at 0. A uchar is enough: one slot can be shared
by at most NPROC = 64 processes, and swapdup panics before the count could wrap.
The lock is a spinlock, slots.lock, and that is a requirement, not a convenience:
swapfree will be called from uvmunmap, and when kwait frees a zombie it runs
uvmunmap holding wait_lock and the child’s p->lock. A sleep-lock there would
panic in sched. Nothing inside the critical sections sleeps or takes another lock.
swapalloc scans from slot 0 every time: at most 8,192 bytes, under a lock that is
held only by eviction. Simple beats clever here.
kernel/swap.cStep 4 of 20 · commit 4: Keep a frame table: which process maps each user page
A page table maps a virtual address to a physical page. Eviction needs the opposite:
given a physical page, which PTE maps it? In this kernel a user page is mapped by
exactly one PTE (fork copies pages), so the answer is one (process, virtual address)
pair per page. ft.f has one entry for every page from KERNBASE to PHYSTOP:
32,768 entries of 16 bytes, 512 KiB of .bss. In this build ft is 0x80020
bytes, and the kernel’s end moves from 0x80020bb0 to 0x800a40b8 with all the
branch’s additions: 131 fewer free pages, 128 of them this table.
FRAME(pa) is the index, FRAMEPA(i) the way back. An entry with proc = 0 is not a
user page: page-table pages, trapframes, kernel stacks, pipe buffers and free pages are
never recorded, and so can never be chosen for eviction.
ft.lock protects the entries (and, from commit 9, the clock hand). Its place in the
lock order is fixed by its users: uvmunmap clears entries while freeproc holds
the zombie’s p->lock, so p->lock comes before ft.lock. Commit 9 has to respect
that.
kernel/vm.cStep 5 of 20 · commit 4: Keep a frame table: which process maps each user page
uvmalloc records each page right after mappages has mapped it: ftset(mem, myproc(), a) (line 241). The same call appears in vmfault's lazy path and in
uvmcopy, which now receives the child (np) as a fourth argument, because the
pages it maps belong to the child, not to myproc(). Swap-in will be the fourth place.
uvmunmap clears the entry just before it frees the page (line 209).
myproc() in uvmalloc is right for both of its callers: sbrk grows the calling
process, and kexec allocates the new image for the calling process. The new image
is mapped in a page table that is not yet p->pagetable: the entry says “pid 4, va
a” while p->pagetable maps something else at a. Commit 9’s clock checks for
exactly that.
(State: a lazy sbrk never reaches uvmalloc; this is the eager path, as in
usertests’ sbrkmuch. Reasoned from the code, not recorded.)
kernel/riscv.hStep 6 of 20 · commit 5: Mark swapped-out PTEs, free their slots, share them on fork
When V is clear, the RISC-V page-table walk stops and faults without looking at any other bit of the PTE: the rest is the kernel’s. The reference uses it as follows:
| bits | in a swapped-out PTE |
|---|---|
| 0 V | 0 |
| 1–4 R W X U | the permissions the page will get back |
8 PTE_SWAP |
1 |
| 10 and up | the slot number (SLOT2PTE, PTE2SLOT) |
PTE_SWAP is bit 8, one of the two RSW bits reserved for supervisor software. Any
bit would do when V is clear, but a dedicated, named bit is what lets the code tell a
swapped page from a zero PTE, which in this tree already means “lazily allocated, not
touched yet”.
A real one, recorded by gdb in swapin: 0x10b516 = slot 0x10b516 >> 10 = 1,069,
flags 0x116 = R W U PTE_SWAP. Slot 1,069 is blocks 6,276 to 6,279 of fs.img.
wait_lockthe zombie's p->lockkernel/vm.cStep 7 of 20 · commit 5: Mark swapped-out PTEs, free their slots, share them on fork
Every path that gives back user memory ends here: exit (through the parent’s
kwait → freeproc), kexec dropping the old image, sbrk(-n) through
uvmdealloc. A swapped PTE now has its own case, before the V test that used to
skip it: drop the PTE’s reference to the slot (if the caller frees memory at all) and
clear the PTE.
The state shown is the worst case, reasoned from the code: a parent reaping a zombie
holds wait_lock and the zombie’s p->lock (noff 2), and swapfree adds slots.lock
for a moment (noff 3). That is why slot counts are protected by a spinlock and
swapfree never sleeps.
Without these six lines, nothing crashes at first. Clinic 4 removes them: after two tests the swap area is full, and the third test’s process is killed for lack of memory, with 8,184 of the 8,192 slots owned by processes that no longer exist.
np->lock (the child's)kernel/vm.cStep 8 of 20 · commit 5: Mark swapped-out PTEs, free their slots, share them on fork
uvmcopy runs under the child’s np->lock (from allocproc until
kernel/proc.c:294), so it may not sleep, and reading a page from the disk sleeps.
It does not need to: it gives the child the same PTE, slot number and permissions
included, and counts one more reference. Each process will read its own copy when it
touches the page; the second reader finds the slot still there because the count kept
it alive.
Recorded: swaptest’s main process (pid 3) forks a test child, and its data page
0x2000 is in swap: *pte = 0x116, slot 0, flags R W U PTE_SWAP. gdb printed
slots.ref[0] = 1 just before swapdup (line 323), and noff 1, intena 1, SIE 0, on
hart 1. Page 0x2000 holds the program’s globals, npages among them: this exact
sharing is what keeps the child’s npages at 36,000. Clinic 3 removes the case, the
child gets a zero page there, and the syscalls test copies 0 pages.
walk(new, i, 1) may allocate page-table pages for the child with kalloc: that is
the reserve of free pages at work (commit 10), since no eviction is possible under
np->lock. If it fails, the error path’s uvmunmap drops the references the child
already holds, slots included.
0x3fffff7fa0, in pid 4’s kernel stackkernel/vm.cStep 9 of 20 · commit 6: Read a swapped-out page back in on a page fault
vmfault already handles every user page fault and every copyin or
copyout that finds no mapping. It now asks one more question before the old ones:
is there a PTE marked PTE_SWAP? If so the page exists, on disk, and swapin brings it
back. Only then come the old cases: mapped (decline) or lazy (allocate a zero page).
Without the new test, the lazy branch would map a zero page over the swapped PTE and
the data would be lost.
Recorded: pid 4 (swaptest big, checking its pages) loads from 0x5da1000, whose PTE
is 0xe6d16: slot 923, R W U. Hart 2, noff 0, sstatus = 0x200000020 (SIE clear, SPIE
set): as in every page fault (Tour 26: sbrk, eager and lazy, and page faults), usertrap left interrupts off, and
vmfault will sleep with them off. That is allowed: sleep only requires that no
spinlock but p->lock be held, and the scheduler turns interrupts on for itself.
kernel/swap.cStep 10 of 20 · commit 6: Read a swapped-out page back in on a page fault
The order of the steps is the point:
ualloc, which may evict another page
first, so swapin must not hold swapio.lock yet: the eviction takes it too.swapio.lock (lines 142–144). Swap I/O
is one page at a time; and, more important, a page that is still being written out
holds that lock until its write completes (commit 9), so this read can never see a
half-written slot.Recorded right after the read, at the PTE line (line 256 in the branch head): pid 4, va
0x5d0f000, slot 1,069, *pte = 0x10b516, the new page at 0x83074000. gdb showed
swapio.lock free again (locked = 0), noff 0, interrupts off, hart 0.
Between step 1 and step 3, could anyone change this PTE? Only pid 4 itself: it is RUNNING, so other harts’ evictions leave its pages alone (commit 9), and a sleep in step 2 does not matter because a swapped PTE has nothing to evict.
kernel/trap.cStep 11 of 20 · commit 6: Read a swapped-out page back in on a page fault
Two small changes, both because a page fault can now sleep.
scause and stval are read once, into locals, before anything else. While pid 4
sleeps in swapin, its hart runs other processes, which trap and overwrite those
registers; pid 4 may even resume on another hart. The old code read them again after
vmfault returned, to print the kill message. The message now prints the saved
values and p->trapframe->epc.
The fault test (lines 76–77) now also accepts scause 12, an instruction page fault: a
text page can be evicted like any other, and fetching an instruction from it must
bring it back. In the recorded run the clock took one of pid 3’s text pages
(0x21fc881b: V R X U) while pid 3 slept in wait; its next fetch from that page
will be a cause-12 fault. Before this lab such a fault was always fatal. A fetch from a lazy
heap page still ends in a kill, one fault later: vmfault maps it without X, the
fetch faults again, and the page is now mapped.
kernel/vm.cStep 12 of 20 · commit 7: Keep copyin and copyout from losing their page
copyout turns a user address into a physical one with walkaddr, then writes
there through the kernel’s direct map. That physical address is only good while the
page stays where it is. A system call runs with interrupts on; a timer interrupt
between walkaddr and memmove would yield, making the process RUNNABLE, and
another hart’s clock could then evict the page and give it to someone else. The
memmove would write into a stranger’s memory.
push_off at line 375 closes that window: with interrupts off no timer can preempt
this hart, the process stays RUNNING, and RUNNING processes’ pages are not taken. The
translation, the PTE_W check and the copy all happen inside it. vmfault can
sleep, so the loop pops out (line 378), lets it bring the page in, and then translates
again from the top (line 382): it does not trust the physical address vmfault
returned, because the page may have moved again before the push_off.
The state shown is reasoned, not recorded: swapstat’s copyout runs with no lock,
so after line 375 noff is 1 and intena 1 (interrupts were on). copyin and
copyinstr get the same treatment.
pi->lockStep 13 of 20 · commit 7: Keep copyin and copyout from losing their page
Recorded in the branch head, during swaptest syscalls: pid 5 writes from 0x5000 to
a pipe; pipewrite holds pi->lock and calls copyin for one byte at a time
(kernel/pipe.c:96). At the memmove (line 426) gdb showed noff 2 (pi->lock and
this push_off), intena 1, SIE 0, pa0 = 0x804f3000, on hart 1. The page is in
memory because sys_write pinned it first (commit 8).
Here the push_off adds nothing: the spinlock already keeps interrupts off. What
matters is the other branch (line 417): if this page were not in memory, vmfault
would read it from the disk with pi->lock held, and sched would panic. Nothing
in commit 7 prevents that. Commit 8 makes sure that, on this path, the page is always
there.
kernel/vm.cStep 14 of 20 · commit 8: Pin the buffer while read, write or wait copy under a spinlock
uvmpin walks the pages of the user buffer. For each one:
vmfault bring it back (it may sleep, and may evict other
pages, even this process’s, but never a pinned one), then look at the same page
again.PTE_PIN, bit 9. The test and the set
are done with interrupts off (line 502), the copyout trick again, so the page cannot
be evicted between “it is here” and “it is pinned”.PTE_U, or beyond p->sz, makes the copy fail as it always did.Only the first case costs anything, and it brings back pages the process had before,
which in the original kernel would all have been in memory. So uvmpin never asks for
more memory than the program already used. If it cannot bring a page back (memory and
swap both exhausted), the pins already set are removed and the call returns -1.
Recorded: pid 4 calls write (a printf from swaptest big) with its buffer on the
stack page 0x4000, PTE 0x203d00d7 (V R W U A D) just before the pin. Hart 0, noff 1
(this push_off), intena 1: the system call had interrupts on.
Why pin instead of “bring in and hope”: a pipe reader brings its buffer in, then sleeps
in piperead until data arrives, possibly for minutes. Memory pressure meanwhile
would evict an unpinned buffer, and the copy that follows, under pi->lock, would need
the disk.
wait_lockpid 4's p->lockkernel/sysfile.cStep 15 of 20 · commit 8: Pin the buffer while read, write or wait copy under a spinlock
sys_read and sys_write pin [p, p+n) before they call fileread /
filewrite, and unpin afterwards, but only when the file is not an inode
(FD_INODE): a pipe or a device. readi and writei copy holding sleep-locks
only, so their copies may bring a page back from swap themselves, and pinning a whole
buffer for them would only waste memory. sys_wait pins the 4-byte status, because
kwait copies it out holding wait_lock and the child’s p->lock. Together these
cover every copy this kernel makes under a spinlock: piperead, pipewrite,
consoleread and kwait.
The design’s test is the probe pinread (measure section): read of the 2,441-byte
README into a lazy buffer of 1, 1,000, 34,000 or 100,000 pages returns 2441 every
time, as on the original kernel. Pinning the whole buffer, lazy pages included, would
have returned -1 for the two big ones.
The state shown is the other end of the story, recorded inside kwait just after its
copyout (pp->parent = 0, kernel/proc.c:397 in the branch): pid 3 has waited
for pid 4; hart 0, noff 2, intena 1. The status address 0x4f1c is on pid 3’s stack
page, whose PTE was 0x21fc72d7: flags 0x2d7, V R W U A D and PTE_PIN. The page had
been pinned for the whole time pid 3 slept in wait, while big evicted thousands of
pages; in another run gdb caught one of pid 3’s text pages being taken at such a
moment (next step).
swapio.lock (sleep-lock)pid 3's p->lockft.lockkernel/swap.cStep 16 of 20 · commit 9: Evict a page with the clock algorithm
Each step of the hand takes three locks, in an order dictated by the rest of the kernel:
ft.lock alone (lines 145–150): read entry i and advance the hand.p->lock (line 155). The order p->lock →
ft.lock comes from freeproc, which clears frame-table entries while holding the
zombie’s p->lock; taking them the other way round here could deadlock with it.ft.lock again and check that entry i still names this process and address: in
the gap, the page may have been freed and reused.The state check is the TLB rule of the think section. A SLEEPING or RUNNABLE process
flushed its translations when it entered the kernel through uservec, and it
cannot run again while this hart holds its p->lock: scheduler would need that
lock to switch to it. Its PTE can be changed safely. A RUNNING process on another hart
is skipped, whatever it is doing.
Recorded at the eviction a few lines further on: hart 1 runs pid 4 (big, inside a
swap-in from a load fault), and the hand has stopped on a page of pid 3, SLEEPING in
wait. Its PTE 0x21fc881b has flags 0x1b, V R X U: a text page, A clear. noff 2
(pid 3’s p->lock and ft.lock), intena 0 because a page fault left interrupts off.
swapio.lock, a sleep-lock, is not counted in noff.
swapio.lock (sleep-lock)pid 4's p->lockft.lockStep 17 of 20 · commit 9: Evict a page with the clock algorithm
The PTE must still be valid, be a user page, map exactly frame i, and not be pinned
(lines 164–165). “Map exactly frame i” is what makes exec safe: while kexec
builds a new image, its pages are recorded under the process, but p->pagetable is
still the old one, and the PTE found there maps something else. PTE_U excludes the
stack guard page, which is valid but never usable from user mode: writing it out would
only cost a disk write now and a read back later.
Then the clock algorithm proper. A set: clear it, move on. The hardware sets A in the
PTE whenever it walks the page table for an access (menvcfg.ADUE,
kernel/start.c:41); gdb found 0x21fc78d7 here (hart 2), flags 0xd7 = V R W U
A D, a page of pid 4 used since the last lap. A clear: the victim. Its PTE becomes
“in slot s”: V off, R W X U kept, PTE_SWAP, slot number (line 169), and the
frame-table entry is cleared.
Recorded victim: pid 4’s own page (pid 4 is RUNNING, but it is the caller: allowed),
PTE 0x20c1d497 (V R W U D, A clear), frame 0x3075, slot 1,069, on hart 0. noff 2,
intena 0.
No sfence.vma follows the PTE change, and none is needed. pid 4 is in the kernel, on
the kernel page table since uservec flushed; userret flushes again before it
runs a user instruction. For a SLEEPING or RUNNABLE owner the same two flushes apply.
That is the whole TLB argument, and it is why the state test cannot be relaxed (clinic
1).
0x3fffff7ee0 in pid 4’s kernel stack (p->kstack = 0x3fffff7000)swapio.lock (sleep-lock)Step 18 of 20 · commit 9: Evict a page with the clock algorithm
evict takes swapio.lock first, then a slot, then lets the clock rewrite a PTE, then
writes the page, releases the lock, and frees the page last:
swapin’s acquiresleep until the page is on the disk.kfree comes last, when no PTE and no I/O refers to the page.Recorded at the write (line 203 here, line 210 in the head): hart 0, pid 4, slot 1,069,
page 0x83075000 (frame 0x3075 of the previous step). gdb printed swapio.lock as
locked = 1, pid = 4, noff 0: no spinlock is held across the disk I/O, only the
sleep-lock, which is what makes sleeping here legal.
If the clock finds nothing (every page pinned, running, or used within the last lap,
twice round), evict returns 0 and frees the slot it took.
kernel/swap.cStep 19 of 20 · commit 10: Evict when free memory runs low
The switch. ualloc evicts pages until FREELOW = 32 are free, or until nothing more
can be evicted, then takes one with kalloc. From this commit, uvmalloc,
vmfault's lazy path and swapin (line 242) use it. kalloc gains a free-page
counter (kmem.nfree, under kmem.lock) and nothing else: it still never evicts and
never sleeps.
One of those callers can hold a spinlock: a copy under pi->lock or cons.lock that
reaches a lazy page of its buffer (uvmpin leaves those alone) allocates it through
vmfault. So ualloc first asks whether this hart holds a spinlock (lines
224–226: noff above 1, counting its own push_off), and if so it does not evict,
it only takes what is free, which is what the original kernel did there.
So there are two kinds of allocation. Those that may sleep evict, and keep 32 pages in
reserve. Those that may not (page-table pages in walk, the trapframe in
allocproc, the pages uvmcopy copies, pipe buffers, lazy pages under a lock)
take from that reserve. When the reserve is gone and nothing can be evicted, both
kinds fail exactly as in the original kernel: sbrk returns -1, a faulting process is
killed, fork returns -1.
The state shown is the moment before the eviction recorded in the two previous steps:
a swap-in from a page fault, no lock held, so ualloc evicts.
user/swaptest.cStep 20 of 20 · commit 11: Add swapstat and swaptest, a test program for swapping
swaptest’s five checks follow the think section: big writes 36,000 pages and
checks every word, newest first; syscalls makes the kernel read and write pages that
are in swap; fork forks a process that is partly in swap; procs, shown here, makes
three processes evict each other’s pages; and a leak check compares free pages plus
free slots before and after (a page that moves between memory and swap changes both by
one).
procs has to make sure the three children overlap. Started simply, they can finish
one after another and never need swap at all: a version without the handshake printed
procs: 0 out, 0 in. So each child fills its 12,500 pages, reports through ready,
and waits on go until all three have filled: 37,500 pages must exist at the same moment, more than the
32,768 of RAM, so they must take pages from each other, and the test fails if nothing
was written out. A killed child closes its end of ready by exiting, so the parent’s
read returns 0 instead of waiting forever. (Without the close calls, the kernel of
clinic 4, whose children are killed, leaves the parent waiting there for good.)
Blocked in read(go[0]), each child has one page pinned (its 1-byte buffer); the rest
of its 12,500 pages are fair game for the other two, which are running.
Lab 16 · wrap-up
On the branch (ext/16-swap, 12 commits), built with the project toolchain and run on 3
harts (-smp 3 -m 128M), one boot:
$ swaptest
swaptest: big: wrote 36000 pages: 3710 out, 0 in
swaptest: big: checked 36000 pages, 0 wrong: 32299 out, 32299 in
swaptest: big: OK
swaptest: syscalls: copied 32 pages, 0 wrong; 5 sources and 4 destinations were in swap
swaptest: syscalls: OK
swaptest: fork: 21 slots in use at fork
swaptest: fork: child ok, parent 0 wrong, 26 pages read in
swaptest: fork: OK
swaptest: procs: 15838 out, 13651 in
swaptest: procs: OK
swaptest: free pages + free slots: 40594 before, 40594 after
swaptest: leak: OK
swaptest: 62267 pages written out, 45992 read in
swaptest: ALL OK
$ usertests -q
usertests starting
test copyin: OK
test copyout: OK
...
test kernmem: usertrap(): unexpected scause 0xd pid=6485
...
test sbrkfail: OK
...
test lazy_sbrk: OK
...
ALL TESTS PASSED
$ swaptest
swaptest: big: wrote 36000 pages: 3708 out, 0 in
swaptest: big: checked 36000 pages, 0 wrong: 16073 out, 16073 in
swaptest: big: OK
swaptest: syscalls: copied 31 pages, 0 wrong; 4 sources and 4 destinations were in swap
swaptest: syscalls: OK
swaptest: fork: 22 slots in use at fork
swaptest: fork: child ok, parent 0 wrong, 24 pages read in
swaptest: fork: OK
swaptest: procs: 14608 out, 14603 in
swaptest: procs: OK
swaptest: free pages + free slots: 40594 before, 40594 after
swaptest: leak: OK
swaptest: 44807 pages written out, 30716 read in
swaptest: ALL OK
What this shows, check by check:
write read 5 source pages back from swap, read 4 destination pages, all
before the copy itself, which runs under pi->lock; no panic, no wrong byte.sbrk(-n) does not free, are gone too.usertrap() lines are the expected
kills of tests such as kernmem, MAXVAplus, stacktest and nowrite. Several tests
(sbrkfail, execout, the countfree at the start and end) use up all of memory on
purpose; with swap they also fill the swap area first, and their final count of free
pages before and after still matches.usertests -q also passes at commits 8, 9 and 10 alone, and other boots of the head ran
swaptest three more times: ALL OK each time.
Keys: ← → step · Home start