ke+mm: add TLB shootdown and memory reclamation.

This commit is contained in:
Loïc Grégoire
2026-06-25 21:33:14 -04:00
parent 20a38f3a49
commit 4ba57ad42a
19 changed files with 422 additions and 63 deletions
+2
View File
@@ -116,6 +116,8 @@ fn particle(param: ?*anyopaque) void {
data.act_y = parent_data.act_y;
const explosion_range = parent_data.explosion_range;
mm.zone.gpa.destroy(parent_data);
const angle = @rem(rand(), 65536);
data.vel_x = mul_fp_fp(cos(angle), rand_fp_sign()) * explosion_range;
data.vel_y = mul_fp_fp(sin(angle), rand_fp_sign()) * explosion_range;
+4 -47
View File
@@ -8,64 +8,21 @@ const ps = r.ps;
const amd64 = @import("arch");
const turnstile = ke.private.turnstile;
var mtx: ke.Mutex = .init();
var shared_counter: usize = 0;
var work_item: ex.WorkItem = undefined;
var work_item2: ex.WorkItem = undefined;
fn mutex_test(_: ?*anyopaque) void {
while (true) {
mtx.acquire();
shared_counter += 1;
mtx.release();
}
}
fn foo(_: ?*anyopaque) void {
std.log.info("foo cpu {}", .{ke.cpu.current()});
var timer: ke.Timer = undefined;
timer.init();
ke.timer.set(&timer, std.time.ns_per_ms * 500, null);
_ = ke.wait.wait_one(&timer.hdr, null) catch unreachable;
}
fn bar(_: ?*anyopaque) void {
std.log.info("bar cpu {}", .{ke.cpu.current()});
}
pub fn init(boot_info: *pl.BootInfo) void {
mm.init(boot_info);
ps.init();
pl.late_init(boot_info);
mm.late_init();
ke.sched.late_init();
ps.init();
ex.private.workqueue.init() catch @panic("e");
work_item = .init(.Normal, foo, null);
work_item2 = .init(.Normal, bar, null);
ex.workqueue.enqueue(&work_item);
ex.workqueue.enqueue(&work_item2);
if (false) {
if (boot_info.framebuffer != null) {
const t = ps.thread.create_kernel(ke.Thread.Priority.default, ex.fireworks.start, boot_info) catch @panic("oom");
ke.sched.enqueue(&t.kern);
}
while (true) {}
}
const ipl = ke.ipl.raise(.Dispatch);
for (0..10) |_| {
const t = ps.thread.create_kernel(ke.Thread.Priority.default, mutex_test, null) catch @panic("oom");
if (boot_info.framebuffer != null) {
const t = ps.thread.create_kernel(ke.Thread.Priority.default, ex.fireworks.start, boot_info) catch @panic("oom");
ke.sched.enqueue(&t.kern);
}
ke.ipl.lower(ipl);
while (true) {}
while (true) {
std.atomic.spinLoopHint();
+18
View File
@@ -5,6 +5,8 @@ const amd64 = r.arch;
const std = @import("std");
pub const tlb_max_pages = 33;
pub const init = @import("init.zig");
pub const early_init = init.early_init;
@@ -114,3 +116,19 @@ pub inline fn restore_interrupts(state: bool) void {
pub inline fn send_resched_ipi(target: u32) void {
pl.impl.send_resched_ipi(target);
}
pub inline fn send_tlb_ipi(target: u32) void {
pl.impl.send_tlb_ipi(target);
}
pub inline fn flush_full_tlb() void {
const prev_cr3 = amd64.read_cr(3);
// Reload CR3 to flush the entire TLB.
// This works because we assume global pages are never unmapped.
// If they are in the future, this should toggle CR4.PGE instead.
amd64.write_cr(3, prev_cr3);
}
pub inline fn flush_tlb(va: r.VAddr) void {
amd64.invlpg(va);
}
+5
View File
@@ -4,6 +4,7 @@ const r = @import("root");
const ke = r.ke;
const ki = ke.private;
const pl = r.pl;
const mi = r.mm.private;
extern const __interrupt_vectors: [256]usize;
@@ -84,6 +85,10 @@ export fn isr_handler_main(frame: *const amd64.IrqFrame) callconv(.{ .x86_64_sys
ki.timer.clock();
}
if (frame.intno == 241) {
ki.shootdown.ipi_handler();
}
if (frame.intno >= 32) {
pl.impl.apic.eoi();
}
+2
View File
@@ -121,6 +121,8 @@ fn dispatch_queue(cpu: u32) void {
sched_cpu.start_timer = false;
sched_cpu.preemption_reason = .None;
ki.shootdown.process_shootdowns();
if (sched_cpu.next_thread != null) {
ki.sched.handle_preemption(sched_cpu);
}
+1
View File
@@ -15,6 +15,7 @@ pub fn init(boot_info: *pl.BootInfo) linksection(r.init) void {
pl.early_init(boot_info);
ki.cpu.init_cpu(0);
ki.turnstile.init_turnstiles();
ki.shootdown.init();
thread0.init(@intFromPtr(&stack), r.kib(16), ke.Thread.Priority.idle, ki.sched.idle, null);
thread0.priority = 0;
+1
View File
@@ -28,3 +28,4 @@ pub const event = @import("event.zig");
pub const turnstile = @import("turnstile.zig");
pub const mutex = @import("mutex.zig");
pub const queue = @import("queue.zig");
pub const shootdown = @import("shootdown.zig");
+7
View File
@@ -17,9 +17,11 @@ pub const Console = p.log.Console;
pub const CpuLocal = p.cpu.CpuLocal;
pub const ExportedCpuLocal = p.cpu.ExportedCpuLocal;
pub const CpuMask = p.cpu.CpuMask;
pub const AtomicCpuMask = p.cpu.AtomicCpuMask;
pub const Event = p.event.Event;
pub const Queue = p.queue.Queue;
pub const Mutex = p.mutex.Mutex;
pub const ShootdownState = p.shootdown.ShootdownState;
// === Exported Interfaces ===
pub const panic = p.panic.panic;
@@ -72,5 +74,10 @@ pub const cpu = struct {
pub const current = p.cpu.current;
};
pub const shootdown = struct {
pub const submit = p.shootdown.submit;
pub const shootdowns = &p.shootdown.shootdowns;
};
/// Number of CPUs on the system
pub var ncpus: usize = 0;
+199
View File
@@ -0,0 +1,199 @@
//! Kernel-side mechanism for asynchronous TLB shootdown and deferred memory reclamation.
const std = @import("std");
const config = @import("config");
const rtl = @import("rtl");
const r = @import("root");
const ke = r.ke;
const ki = ke.private;
const mm = r.mm;
comptime {
if (!@hasDecl(ki.impl, "send_tlb_ipi")) @compileError("impl must provide send_tlb_ipi");
if (!@hasDecl(ki.impl, "flush_full_tlb")) @compileError("impl must provide flush_full_tlb");
if (!@hasDecl(ki.impl, "flush_tlb")) @compileError("impl must provide flush_tlb");
}
/// Structure representing a single shootdown request.
pub const ShootdownState = struct {
/// State of the request.
/// This is used as a counter when the request is outstanding,
/// otherwise it is set to the `slot_*` values below.
state: std.atomic.Value(u16),
link: rtl.List.Entry,
/// Base virtual address of the shootdown.
base: r.VAddr,
/// Number of pages to flush.
npages: u32,
/// Additional metadata.
payload: [2]usize,
pub fn release(self: *ShootdownState) void {
self.state.store(slot_free, .release);
}
};
const PerCpu = struct {
/// States attached to this CPU.
states: [64]ShootdownState,
/// States relevant to each CPU.
/// If CPU0 sent a shootdown to CPU1, then CPU0 will have `valid_states[CPU1]`
/// pointing to that shootdown, which will sit in `states`.
valid_states: [config.ncpus]std.atomic.Value(u64),
/// CPUs that have sent this CPU a shootdown.
senders: ke.AtomicCpuMask,
};
const slot_free: u16 = std.math.maxInt(u16);
const slot_reserved: u16 = std.math.maxInt(u16) - 1;
const percpu = ke.CpuLocal(PerCpu, undefined);
pub var shootdowns: ke.Queue = undefined;
var sync_counter: std.atomic.Value(usize) = .init(0);
var sync_addr: r.VAddr = 0;
var sync_count: usize = 0;
var shootdown_lock: ke.SpinLock = .init();
export const tlb_percpu_init linksection(r.percpu_init) = &pcpu_init;
fn pcpu_init() linksection(r.init) callconv(.c) void {
const local = percpu.local();
local.valid_states = @splat(.init(0));
local.senders = .init(false);
local.states = @splat(.{ .base = 0, .npages = 0, .state = .init(slot_free), .payload = undefined, .link = undefined });
}
/// Allocate an invalidation state slot on this CPU.
/// O(N) algorithm but N is tiny.
fn allocate_slot(cpu: *PerCpu) ?usize {
for (0.., &cpu.states) |slot, *state| {
if (state.state.load(.monotonic) != slot_free) continue;
if (state.state.cmpxchgStrong(slot_free, slot_reserved, .acquire, .monotonic) == null) {
return slot;
}
}
return null;
}
fn flush_range(va: r.VAddr, npages: usize) void {
if (npages > ki.impl.tlb_max_pages) {
// Just flush the entire thing.
ki.impl.flush_full_tlb();
return;
}
for (0..npages) |i| {
ki.impl.flush_tlb(va + i * mm.page_size);
}
}
/// Synchronous shootdown path.
fn do_synchronous_shootdown(state: ShootdownState) void {
shootdown_lock.acquire_no_ipl();
sync_addr = state.base;
sync_count = state.npages;
sync_counter.store(ke.ncpus - 1, .release);
const cur = ke.cpu.current();
for (0..ke.ncpus) |i| {
if (i == cur) continue;
ki.impl.send_tlb_ipi(@truncate(i));
}
while (sync_counter.load(.monotonic) != 0) {
std.atomic.spinLoopHint();
}
shootdown_lock.release_no_ipl();
}
/// Called by the TLB interrupt handler.
pub fn ipi_handler() void {
flush_range(sync_addr, sync_count);
_ = sync_counter.fetchSub(1, .release);
}
/// Called in a quiescent state to process the pending shootdowns.
/// This must be called at IPL dispatch.
pub fn process_shootdowns() void {
const cpu = percpu.local();
const curcpu = ke.cpu.current();
if (cpu.senders.is_all(false, .monotonic)) {
// Nothing to do.
return;
}
var iter = cpu.senders.iter(.monotonic);
// Go through every sender and get their states.
while (iter.next()) |i| {
if (i == curcpu) continue;
cpu.senders.clear(i, .monotonic);
const sender = percpu.remote(@truncate(i));
while (true) {
var states = sender.valid_states[curcpu].swap(0, .acquire);
if (states == 0) break;
while (states != 0) {
// Go through each set bit and invalidate the slot.
const bit = @ctz(states);
states &= ~(@as(u64, 1) << @intCast(bit));
const state = &sender.states[bit];
flush_range(state.base, state.npages);
if (state.state.fetchSub(1, .release) == 1) {
shootdowns.insert(&state.link, .Tail);
}
}
}
}
}
/// Submit a shootdown to occur asynchronously on `target_mask`.
/// Returns an error if a synchronous shootdown was done instead.
pub fn submit(state: ShootdownState, target_mask: ke.CpuMask) !void {
const ipl = ke.ipl.raise(.Dispatch);
defer ke.ipl.lower(ipl);
// Flush on our local TLB.
flush_range(state.base, state.npages);
// Get a slot and fill it.
const curcpu = ke.cpu.current();
const cpu = percpu.local();
const slot = allocate_slot(cpu) orelse {
do_synchronous_shootdown(state);
return error.NoSlot;
};
cpu.states[slot] = state;
cpu.states[slot].state.store(@truncate(target_mask.count()), .release);
var iter = target_mask.iter();
while (iter.next()) |bit| {
if (bit == curcpu) continue;
_ = cpu.valid_states[bit].fetchOr(@as(u64, 1) << @truncate(slot), .release);
ki.ipl.set_softint_pending(@truncate(bit), .Dispatch);
// Tell him we have something for him.
percpu.remote(@truncate(bit)).senders.set(curcpu, .release);
}
}
pub fn init() void {
shootdowns.init(ke.ncpus);
}
+7 -1
View File
@@ -16,8 +16,14 @@ pub fn alloc(size: usize) mm.Error!*anyopaque {
mi.kernel_space.pmap.map_range_allocating(addr, size, .{
.read = true,
.write = true,
.global = true,
.global = false,
});
return @ptrFromInt(addr);
}
pub fn free(va: r.VAddr, size: usize) void {
const ipl = mi.kernel_space.lock.acquire();
mi.tlb.reclaim_range(&mi.kernel_space, va, size);
mi.kernel_space.lock.release(ipl);
}
+1
View File
@@ -103,4 +103,5 @@ pub fn init(boot_info: *pl.BootInfo) linksection(r.init) void {
pub fn late_init() linksection(r.init) void {
mi.zone.late_init();
mi.tlb.init();
}
+5 -9
View File
@@ -16,13 +16,11 @@ const ke = r.ke;
const log = std.log.scoped(.@"mm/phys");
// === Early state ===
var bootstrapped = false;
var early_alloc_entry_idx: usize = 0;
var memory_map: *pl.BootInfo.MemMap = undefined;
var early_allocs: usize = 0;
// === Memory allocator state ===
/// Head for the global treiber stack.
const GlobalHead = packed struct(u64) {
pfn: mm.Pfn,
@@ -41,13 +39,11 @@ const PerCpu = struct {
const percpu = ke.CpuLocal(PerCpu, undefined);
const null_pfn: mm.Pfn = std.math.maxInt(mm.Pfn);
// TODO: make this configurable (or dynamic).
const batch_size: u8 = 64;
var batch_pool: std.atomic.Value(GlobalHead) = .init(.{
.pfn = null_pfn,
.pfn = mm.null_pfn,
.tag = 0,
});
@@ -83,7 +79,7 @@ fn pop_from_pool() ?*mm.Page {
while (true) {
const batch_pfn = old_head.pfn;
if (batch_pfn == null_pfn) {
if (batch_pfn == mm.null_pfn) {
return null;
}
@@ -177,15 +173,15 @@ pub fn free(addr: r.PAddr) void {
const page = mm.pfn_to_struct_page(mm.page_to_pfn(addr));
page.free.next_pfn = null_pfn;
page.free.batch_next = null_pfn;
page.free.next_pfn = mm.null_pfn;
page.free.batch_next = mm.null_pfn;
page.free.batch_count = 0;
if (cpu.active_batch) |curr| {
// Add the page to the current batch.
page.free.next_pfn = mm.struct_page_to_pfn(curr);
} else {
page.free.next_pfn = null_pfn;
page.free.next_pfn = mm.null_pfn;
}
cpu.active_batch = page;
+11
View File
@@ -103,6 +103,17 @@ pub const PMap = struct {
self.map_from(va, size, src);
}
/// Unmap a contiguous range of virtual pages.
/// Returns a list threaded through PFNs of the unmapped physical pages if any.
pub fn unmap(self: *Self, va: r.VAddr, size: usize) ?mm.Pfn {
return self.impl.unmap(va, size);
}
/// Flush the TLB entry of a single page.
pub fn flush(self: *Self, va: r.VAddr) void {
self.impl.flush(va);
}
/// Map a single page. Convenience wrapper around map_contiguous_range.
pub fn map_page(self: *Self, va: r.VAddr, pa: r.PAddr, flags: mm.MapFlags) void {
self.map_contiguous_range(va, pa, mm.page_size, flags);
+6 -1
View File
@@ -7,6 +7,7 @@ pub const vmem = @import("vmem.zig");
pub const heap = @import("heap.zig");
pub const zone = @import("zone.zig");
pub const PMap = pmap.PMap;
pub const tlb = @import("tlb.zig");
const ke = @import("root").ke;
@@ -22,4 +23,8 @@ pub const impl = switch (config.arch) {
else => @compileError("unsupported architecture"),
};
pub var kernel_space: Space = undefined;
pub var kernel_space: Space = .{
.pmap = undefined,
.arena = undefined,
.lock = .init(),
};
+40
View File
@@ -109,6 +109,46 @@ pub fn RadixPmap(comptime Impl: type) type {
}
}
/// Unmap a contiguous range of virtual pages.
/// Only small pages are supported.
/// Return a pfn that points to the physical pages that were unmapped.
pub fn unmap(self: *Self, va: r.VAddr, size: usize) ?mm.Pfn {
std.debug.assert(std.mem.isAligned(va, mm.page_size));
var c = self.cursor(va);
const npages: usize = size / mm.page_size;
var head_pfn: mm.Pfn = mm.null_pfn;
for (0..npages) |_| {
var level: usize = num_levels - 1;
blk: while (true) {
const idx = index_for_level(c.va, level);
const pte = c.tables[level][idx];
if (!pte.present) break :blk;
if (level == 0) {
// Found a present entry, add it to the list.
const addr = pte.address();
const pfn = mm.page_to_pfn(addr);
const page = mm.pfn_to_struct_page(pfn);
page.free.next_pfn = head_pfn;
head_pfn = pfn;
c.tables[level][idx] = std.mem.zeroes(Impl.Pte);
break;
}
if (!c.walk_down_resolve(level - 1)) return null;
level -= 1;
}
c.advance(mm.page_size);
}
return if (head_pfn == mm.null_pfn) null else head_pfn;
}
/// Ensure all intermediate page tables down to `target_level` exist for
/// the given virtual address range, allocating them as needed. Useful for
/// pre-populating a shared region (e.g. the kernel half of a user pmap)
+1
View File
@@ -15,6 +15,7 @@ pub const Space = private.Space;
// 16 TB of maximum physical memory.
// This should be fine on consumer hardware for at least a decade :^)
pub const Pfn = u32;
pub const null_pfn: Pfn = std.math.maxInt(Pfn);
pub const Page = extern union {
free: extern struct {
+101
View File
@@ -0,0 +1,101 @@
//! MM-level TLB shootdown support
const r = @import("root");
const std = @import("std");
const rtl = @import("rtl");
const ke = r.ke;
const mm = r.mm;
const mi = mm.private;
const ps = r.ps;
pub var sync_shootdowns: std.atomic.Value(usize) = .init(0);
pub var async_shootdowns: std.atomic.Value(usize) = .init(0);
fn free_pages(head_pfn: mm.Pfn) void {
var pfn = head_pfn;
// Free all the physical pages.
while (pfn != mm.null_pfn) {
const page = mm.pfn_to_struct_page(pfn);
const next = page.free.next_pfn;
mm.phys.free(mm.pfn_to_page(pfn));
pfn = next;
}
}
/// Reclaim the memory associated with a state.
/// This can only be done when all CPUs have flushed their TLB.
fn reclaim_state(state: *ke.ShootdownState) void {
const space: *mm.Space = @ptrFromInt(state.payload[0]);
const pfn: u32 = @truncate(state.payload[1]);
const base = state.base;
const npages = state.npages;
// Reclaim the virtual address space.
const ipl = space.lock.acquire();
space.arena.free(base, @as(usize, npages) * mm.page_size) catch unreachable;
space.lock.release(ipl);
_ = async_shootdowns.fetchAdd(1, .monotonic);
// Free the physical pages
free_pages(pfn);
// Release the slot after all copied state has been consumed.
state.release();
}
fn worker_thread(_: ?*anyopaque) void {
while (true) {
const entry = ke.shootdown.shootdowns.remove();
const state: *ke.ShootdownState = @fieldParentPtr("link", entry);
_ = async_shootdowns.fetchAdd(1, .monotonic);
reclaim_state(state);
}
}
/// Unmap and flush a virtual range on the given space.
/// Space lock is held on entry and IPL is raised to Dispatch.
pub fn reclaim_range(space: *mm.Space, va: r.VAddr, size: usize) void {
std.debug.assert(std.mem.isAligned(va, mm.page_size));
// Unmap the virtual addresses and get the backing physical pages.
const head_pfn = space.pmap.unmap(va, size) orelse {
// If no physical pages, free the VA directly.
space.arena.free(va, size) catch unreachable;
return;
};
const state: ke.ShootdownState = .{
.base = va,
.npages = @truncate(size / mm.page_size),
.link = undefined,
.state = .init(0),
.payload = .{
@intFromPtr(space),
@intCast(head_pfn),
},
};
var mask: ke.CpuMask = .init(false);
for (0..ke.ncpus) |i| {
if (i != ke.cpu.current()) {
mask.set(i);
}
}
ke.shootdown.submit(state, mask) catch {
_ = sync_shootdowns.fetchAdd(1, .monotonic);
space.arena.free(va, size) catch unreachable;
free_pages(head_pfn);
return;
};
}
pub fn init() void {
const td = ps.thread.create_kernel(ke.Thread.Priority.default, worker_thread, null) catch unreachable;
ke.sched.enqueue(&td.kern);
}
+7 -5
View File
@@ -20,8 +20,10 @@ const Slab = struct {
/// Reference count indicating how many objects are in use.
refcount: u16,
capacity: u16,
/// Base address of the slab.
/// Base address of the slab + its color.
base: usize,
/// Base address of the slab allocation.
buf: usize,
pub fn bitmap(self: *Slab) [*]u64 {
return @ptrFromInt(@intFromPtr(self) + @sizeOf(Slab));
@@ -623,6 +625,7 @@ pub const Zone = struct {
if (rem > 0) bm[full_words] = (@as(u64, 1) << @intCast(rem)) - 1;
ret.base = @intFromPtr(buf) + self.color;
ret.buf = @intFromPtr(buf);
ret.capacity = @intCast(capacity);
ret.refcount = 0;
@@ -667,7 +670,7 @@ pub const Zone = struct {
const alloc_size = @sizeOf(Slab) + Slab.bitmap_bytes(slab.capacity);
gpa.free(@as([*]u8, @ptrCast(slab))[0..alloc_size]);
// FIXME: call free_pages on slab.base here!
mm.heap.free(slab.buf, self.slab_size);
} else {
free_page(@ptrFromInt(std.mem.alignBackward(usize, @intFromPtr(slab), mm.page_size)));
}
@@ -842,9 +845,8 @@ fn gpa_free(
_: usize,
) void {
if (buf.len > 2048) {
//const pages = std.mem.alignForward(usize, buf.len, mm.page_size);
// TODO
//mi.heap.free_pages(@ptrCast(buf.ptr), pages);
const pages = std.mem.alignForward(usize, buf.len, mm.page_size);
mm.heap.free(@intFromPtr(buf.ptr), pages);
return;
}
+4
View File
@@ -109,3 +109,7 @@ pub fn arm_timer(deadline: r.Nanoseconds) void {
pub fn send_resched_ipi(target_cpu: u32) void {
apic.send_ipi(smp.cpu_id_to_apic_id[target_cpu], 240, 0);
}
pub fn send_tlb_ipi(target_cpu: u32) void {
apic.send_ipi(smp.cpu_id_to_apic_id[target_cpu], 241, 0);
}