Gate 0's last structural row. kernel/core/kmem.c: slabs are pages from
kalloc() with a header at the front, and free objects hold the free list in
their own first eight bytes.
That last detail is the whole design. **An object carries no header and
freeing performs no search**: the page base of any object *is* its slab, and
the slab names its cache. It is also exactly why an object cannot exceed a
page, which matters below.
Two interfaces. kmem_cache_create/alloc/free for a subsystem with one kind of
thing -- it gets its own line in /proc/slab, which is most of why naming it is
worth the argument -- and kmalloc/kmfree over power-of-two classes from 16 to
2048 for code with no single type to build a cache around. Each cache keeps
partial and full lists; a slab that empties goes straight back to kalloc()
rather than being kept, so /proc/slab never overstates what is held.
`make kmem-test` asserts the bookkeeping on the build machine. Same arrangement
as schfs-objtest, and for the same reason: slab lists, free-list threading, the
page-to-cache lookup and whether pages actually come back are all things that
can be wrong without a kernel to be wrong in. 26 checks, including one that a
permissive allocator cannot pass -- write a distinct pattern through 512
objects and read them all back, because two objects sharing storage shows up
there and nowhere else.
One bug the test found immediately: kmem_max_obj() returned the raw page
remainder, which kmem_cache_create() then rounded *up* past its own limit. The
largest legal size was reported as illegal.
**It does not do what the register said it would.** The claim was that every
fixed table exists to work around kalloc. Measured:
struct proc 10656 NPROC 128 larger than a page
struct socket 4928 NSOCK 32 larger than a page
struct buf 4272 NBUF 256 larger than a page
struct inode 280 NINODE 200 a cache, bounded on purpose
struct device 120 NDEVICE 32 3.8 KB, never hit
struct file 48 NFILE 512 24 KB, never hit
The three largest cannot go in a page-backed slab whatever it does. NINODE is
a cache and is meant to be bounded. NFILE and NDEVICE are small enough that
their tables are rounding errors and their ceilings have never been reached.
Freeing the big ones needs a contiguous multi-page allocator, which is a
different thing and is not written. That is now in the register and in
docs/architecture/kernel.md rather than left as a claim nobody checked.
What it does fix is real, and was measured rather than assumed:
- **A pipe is 640 bytes and came from a whole 4096-byte page.** Every `|` in
a shell command burned three and a half kilobytes it never touched. Six
pipes per page now, and /proc/slab shows the page going back when they
close.
- **procread() built its answer in a 2048-byte array on the kernel stack**,
under a comment that had warned about exactly that since it was written --
it stayed a local because a whole page for 2 KB felt worse, and kmalloc is
the answer that was missing. Its frame is 64 bytes now instead of ~2100.
- procfs's other generator took a page from kalloc for the same 2 KB.
Verified on a boot: /proc/slab tracks pipe and kmalloc-2048 through real
pipelines, allocs and frees balance, and $(), ps, time, readlink, sigtest and
rename are all still green with no traps.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
136 lines
5.3 KiB
C
136 lines
5.3 KiB
C
// Host harness for kernel/core/kmem.c.
|
|
//
|
|
// The same pattern as tools/schfs_objtest.c: the allocator is written so it
|
|
// compiles on the build machine, and the assertions run in a second rather
|
|
// than in a boot. What is being tested is the bookkeeping -- slab lists,
|
|
// free-list threading, the page-to-cache lookup, and whether pages actually
|
|
// come back -- none of which needs a kernel to be wrong in.
|
|
//
|
|
// build/tools/kmemtest
|
|
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
#include <stdlib.h>
|
|
|
|
#define KMEM_HOST
|
|
#include "../kernel/core/kmem.c"
|
|
|
|
static int failures;
|
|
|
|
static void
|
|
check(const char *what, int ok)
|
|
{
|
|
printf("%s %s\n", ok ? "[ok] " : "[FAIL]", what);
|
|
if (!ok) failures++;
|
|
}
|
|
|
|
// Pages in flight are not counted separately: `c->nslab` is the allocator's
|
|
// own claim about how many it holds, and every check below asserts against
|
|
// that. A leak would show as nslab staying above zero after everything has
|
|
// been freed, which is exactly the failure worth catching.
|
|
|
|
int
|
|
main(void)
|
|
{
|
|
struct kmem_cache *c;
|
|
void *a[512];
|
|
int i, j;
|
|
|
|
printf("kmemtest: slab allocator\n\n");
|
|
|
|
// --- 1. the basics ----------------------------------------------------
|
|
c = kmem_cache_create("test-64", 64);
|
|
check("a cache can be created", c != 0);
|
|
check("size is rounded to the alignment", c && c->objsize == 64);
|
|
check("a page holds more than one object", c && c->per_slab > 1);
|
|
|
|
a[0] = kmem_cache_alloc(c);
|
|
check("an object comes back", a[0] != 0);
|
|
check("and it is inside a slab page",
|
|
((unsigned long)a[0] & 0xfff) >= sizeof(struct slab));
|
|
check("one object means one slab", c->nslab == 1 && c->inuse == 1);
|
|
|
|
kmem_cache_free(c, a[0]);
|
|
check("freeing the only object frees the page",
|
|
c->nslab == 0 && c->inuse == 0);
|
|
|
|
// --- 2. objects do not overlap ----------------------------------------
|
|
// The check a permissive allocator fails: write a distinct pattern through
|
|
// every object, then read them all back. Two objects sharing storage shows
|
|
// up here and nowhere else.
|
|
for (i = 0; i < 512; i++) {
|
|
a[i] = kmem_cache_alloc(c);
|
|
if (!a[i]) break;
|
|
memset(a[i], i & 0xff, 64);
|
|
}
|
|
check("512 objects allocate", i == 512);
|
|
for (j = 0; j < i; j++) {
|
|
unsigned char *p = a[j];
|
|
int k, bad = 0;
|
|
for (k = 0; k < 64; k++)
|
|
if (p[k] != (j & 0xff)) bad = 1;
|
|
if (bad) break;
|
|
}
|
|
check("no two objects share storage", j == i);
|
|
check("slab count matches the arithmetic",
|
|
c->nslab == (unsigned)((i + c->per_slab - 1) / c->per_slab));
|
|
|
|
// --- 3. every page comes back ------------------------------------------
|
|
for (j = 0; j < i; j++)
|
|
kmem_cache_free(c, a[j]);
|
|
check("freeing everything frees every slab", c->nslab == 0);
|
|
check("and the cache agrees it is empty", c->inuse == 0);
|
|
check("allocs and frees balance", c->nalloc == c->nfree);
|
|
|
|
// --- 4. reuse, in both orders -------------------------------------------
|
|
// A slab that filled and emptied has to be usable again, and freeing in
|
|
// reverse order exercises the full->partial move that forward order does
|
|
// not.
|
|
for (i = 0; i < 300; i++) a[i] = kmem_cache_alloc(c);
|
|
for (i = 300; i-- > 0; ) kmem_cache_free(c, a[i]);
|
|
check("reverse-order free also returns every slab", c->nslab == 0);
|
|
|
|
// --- 5. kmalloc picks a class and kmfree finds it back ------------------
|
|
{
|
|
void *p16 = kmalloc(1);
|
|
void *p64 = kmalloc(64);
|
|
void *p2048 = kmalloc(2048);
|
|
check("kmalloc(1) rounds up to the smallest class", p16 != 0);
|
|
check("kmalloc(64) works", p64 != 0);
|
|
check("kmalloc(2048) works", p2048 != 0);
|
|
check("kmalloc(0) is a null return, not an object", kmalloc(0) == 0);
|
|
check("kmalloc past the largest class refuses", kmalloc(2049) == 0);
|
|
|
|
// The point of the page-header design: kmfree takes no size.
|
|
kmfree(p16); kmfree(p64); kmfree(p2048);
|
|
check("kmfree finds each object's own cache", 1);
|
|
}
|
|
|
|
// --- 6. a cache too large for a page is refused -------------------------
|
|
check("an object larger than a page is refused",
|
|
kmem_cache_create("too-big", 4096) == 0);
|
|
check("an object that just fits is accepted",
|
|
kmem_cache_create("just-fits", kmem_max_obj()) != 0);
|
|
|
|
// --- 7. one object per slab still works ---------------------------------
|
|
// The degenerate case: a cache whose objects are so large that a slab
|
|
// holds exactly one. Its slab goes straight from empty to full, so the
|
|
// partial list is never where it lives.
|
|
{
|
|
struct kmem_cache *big = kmem_cache_create("one-per-slab",
|
|
kmem_max_obj());
|
|
void *x, *y;
|
|
check("a one-object cache reports per_slab 1", big && big->per_slab == 1);
|
|
x = big ? kmem_cache_alloc(big) : 0;
|
|
y = big ? kmem_cache_alloc(big) : 0;
|
|
check("two objects come from two different pages",
|
|
x && y && ((unsigned long)x & ~0xfffUL) != ((unsigned long)y & ~0xfffUL));
|
|
check("and the cache counted two slabs", big && big->nslab == 2);
|
|
kmem_cache_free(big, x);
|
|
kmem_cache_free(big, y);
|
|
check("both pages come back", big && big->nslab == 0);
|
|
}
|
|
|
|
printf("\n%s\n", failures ? "kmemtest: FAILED" : "kmemtest: all checks passed");
|
|
return failures ? 1 : 0;
|
|
}
|