kalloc: never-used memory stays a range; 256 GB; free is a counter
Every page of RAM went onto the free list at boot, which means writing each one -- a link, and kfree()'s 4 KB of junk -- before the first process ran: 64 GB of memset on a 64 GB machine to say it was free. Memory nobody has used yet is kept as a cursor and an end per region now, and kalloc() takes from those once the list of given-back pages is empty. A page is first touched by whoever asked for it. That makes the cap on RAM cheap to raise, and PHYSTOP_MAX goes from 64 to 256 GB, which still leaves 248 GB of the kernel PDPT for device windows. MEMREGION_MAX goes from 32 to 64 for the firmware maps that cut RAM into many pieces. kalloc_free_bytes() walked the whole free list under the allocator's lock -- two million links on 8 GB, every second top or fastfetch was running, with every CPU that wanted a page waiting behind it. It reads a counter now. And the buffer cache, which keeps what it grows to, is reported as Buffers in /proc/meminfo and sysinfo(2) rather than as memory used by nothing. Checked under QEMU at 1, 2 and 6 GB: MemFree right, zsh splitting 200000 words, fork loops, a 74 MB write. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
This commit is contained in:
parent
4805dbd892
commit
b70e36e0a2
4 changed files with 106 additions and 23 deletions
|
|
@ -19,12 +19,53 @@ struct run {
|
|||
struct run *next;
|
||||
};
|
||||
|
||||
// Free memory is two things: pages that have been allocated and given back,
|
||||
// on a list through their first word, and ranges that have never been handed
|
||||
// out at all, as a cursor and an end. kalloc() takes from the list first.
|
||||
//
|
||||
// Everything used to go on the list at boot, which means writing every page
|
||||
// of RAM -- a pointer and, from kfree(), 4 KB of junk -- before the first
|
||||
// process ran. On a 64 GB machine that is 64 GB of memset to say "this is
|
||||
// free". A range costs sixteen bytes however large it is, and a page is first
|
||||
// touched by whoever asked for it.
|
||||
//
|
||||
// `nfree` counts both, so the free total is a read rather than a walk of
|
||||
// millions of links under the lock every time top refreshes.
|
||||
#define NFRESH (2 * MEMREGION_MAX)
|
||||
|
||||
struct fresh {
|
||||
addr_t next, end; // physical, page aligned
|
||||
};
|
||||
|
||||
struct {
|
||||
struct spinlock lock;
|
||||
int use_lock;
|
||||
struct run *freelist;
|
||||
struct fresh fresh[NFRESH];
|
||||
int nfresh, cur; // ranges, and the first that may have pages
|
||||
uint64 nfree; // pages, list and ranges together
|
||||
} kmem;
|
||||
|
||||
// Before the allocator has a lock, or under it.
|
||||
static void
|
||||
add_fresh(addr_t lo, addr_t hi)
|
||||
{
|
||||
lo = PGROUNDUP(lo);
|
||||
hi &= ~(addr_t)(PGSIZE - 1);
|
||||
if (lo >= hi)
|
||||
return;
|
||||
if (kmem.nfresh == NFRESH) {
|
||||
// Not reachable with MEMREGION_MAX regions per pass; but if it were, the
|
||||
// old way still works.
|
||||
freerange(P2V(lo), P2V(hi));
|
||||
return;
|
||||
}
|
||||
kmem.fresh[kmem.nfresh].next = lo;
|
||||
kmem.fresh[kmem.nfresh].end = hi;
|
||||
kmem.nfresh++;
|
||||
kmem.nfree += (hi - lo) / PGSIZE;
|
||||
}
|
||||
|
||||
// Catch kmem.freelist corruption at the point it's touched, rather than
|
||||
// letting a bad pointer be silently returned by kalloc() and crash whatever
|
||||
// code dereferences it minutes later on an unrelated call stack.
|
||||
|
|
@ -60,7 +101,7 @@ kinit1(void *vstart, void *vend)
|
|||
// synthetic region behind for exactly this reason, so this is belt and
|
||||
// braces rather than a path anything takes.
|
||||
if(nmem_usable == 0){
|
||||
freerange(vstart, vend);
|
||||
add_fresh(V2P((addr_t)vstart), V2P((addr_t)vend));
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -78,7 +119,7 @@ kinit1(void *vstart, void *vend)
|
|||
if(lo >= hi)
|
||||
continue;
|
||||
|
||||
freerange(P2V(lo), P2V(hi));
|
||||
add_fresh(lo, hi);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -110,8 +151,16 @@ mem_dump(struct sbuf *out, const char *arg)
|
|||
sb_puts(out, " upward; PHYSTOP is 0x");
|
||||
sb_puthex(out, (addr_t)PHYSTOP, 16);
|
||||
sb_putc(out, '\n');
|
||||
sb_kv(out, "free ", n, "pages");
|
||||
sb_kv(out, " ", n * PGSIZE / 1024, "KB");
|
||||
sb_kv(out, "listed ", n, "pages");
|
||||
sb_kv(out, "free ", kmem.nfree, "pages");
|
||||
sb_kv(out, " ", kmem.nfree * PGSIZE / 1024, "KB");
|
||||
for (int i = 0; i < kmem.nfresh; i++) {
|
||||
sb_puts(out, " never used 0x");
|
||||
sb_puthex(out, kmem.fresh[i].next, 12);
|
||||
sb_puts(out, "-0x");
|
||||
sb_puthex(out, kmem.fresh[i].end, 12);
|
||||
sb_putc(out, '\n');
|
||||
}
|
||||
if (looped)
|
||||
sb_puts(out, "\n the free list did not end -- it has a CYCLE in it\n");
|
||||
}
|
||||
|
|
@ -154,6 +203,14 @@ mem_whose(struct sbuf *out, struct whose *w)
|
|||
if (kmem.use_lock)
|
||||
release(&kmem.lock);
|
||||
|
||||
for (int i = 0; !found && i < kmem.nfresh; i++)
|
||||
if (w->phys >= kmem.fresh[i].next && w->phys < kmem.fresh[i].end) {
|
||||
w->owners++;
|
||||
w->free_owners++;
|
||||
sb_puts(out, " kalloc FREE -- never handed out yet\n");
|
||||
return;
|
||||
}
|
||||
|
||||
if (found) {
|
||||
w->owners++;
|
||||
w->free_owners++;
|
||||
|
|
@ -189,7 +246,11 @@ kinit_high(void)
|
|||
if (lo >= hi)
|
||||
continue;
|
||||
|
||||
freerange(P2V(lo), P2V(hi));
|
||||
if (kmem.use_lock)
|
||||
acquire(&kmem.lock);
|
||||
add_fresh(lo, hi);
|
||||
if (kmem.use_lock)
|
||||
release(&kmem.lock);
|
||||
added += hi - lo;
|
||||
}
|
||||
|
||||
|
|
@ -236,6 +297,7 @@ kfree(char *v)
|
|||
r = (struct run*)v;
|
||||
r->next = kmem.freelist;
|
||||
kmem.freelist = r;
|
||||
kmem.nfree++;
|
||||
if(kmem.use_lock)
|
||||
release(&kmem.lock);
|
||||
}
|
||||
|
|
@ -252,27 +314,32 @@ kalloc(void)
|
|||
acquire(&kmem.lock);
|
||||
check_run(kmem.freelist, "kalloc entry");
|
||||
r = kmem.freelist;
|
||||
if(r)
|
||||
if(r){
|
||||
kmem.freelist = r->next;
|
||||
} else {
|
||||
while(kmem.cur < kmem.nfresh &&
|
||||
kmem.fresh[kmem.cur].next >= kmem.fresh[kmem.cur].end)
|
||||
kmem.cur++;
|
||||
if(kmem.cur < kmem.nfresh){
|
||||
r = (struct run*)P2V(kmem.fresh[kmem.cur].next);
|
||||
kmem.fresh[kmem.cur].next += PGSIZE;
|
||||
}
|
||||
}
|
||||
if(r)
|
||||
kmem.nfree--;
|
||||
|
||||
if(kmem.use_lock)
|
||||
release(&kmem.lock);
|
||||
return (char*)r;
|
||||
}
|
||||
|
||||
// Total free physical memory, in bytes. Walks the free list; only /proc
|
||||
// readers call it, so the O(n) scan is not on any hot path.
|
||||
// Total free physical memory, in bytes.
|
||||
//
|
||||
// This walked the free list under the allocator's lock: two million links on
|
||||
// 8 GB, cache misses all, every second that top or a status bar was running --
|
||||
// and every CPU that wanted a page in that time waited for it. One word now.
|
||||
uint64
|
||||
kalloc_free_bytes(void)
|
||||
{
|
||||
struct run *r;
|
||||
uint64 n = 0;
|
||||
|
||||
if(kmem.use_lock)
|
||||
acquire(&kmem.lock);
|
||||
for(r = kmem.freelist; r; r = r->next)
|
||||
n += PGSIZE;
|
||||
if(kmem.use_lock)
|
||||
release(&kmem.lock);
|
||||
return n;
|
||||
return kmem.nfree * PGSIZE;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue