mirror of
https://github.com/rdmsr/zag.git
synced 2026-10-30 06:48:22 +00:00
289 lines
11 KiB
Zig
289 lines
11 KiB
Zig
//! Generic radix-tree page map implementation.
|
|
//!
|
|
//! `RadixPmap` is a comptime-parameterized page table walker that implements
|
|
//! virtual-to-physical address mapping over a multi-level radix tree (e.g.
|
|
//! x86-64's four-level PT). The caller supplies an
|
|
//! `Impl` type that describes the tree's level structure and provides
|
|
//! platform-specific PTE constructors.
|
|
//!
|
|
//! The `Impl` type passed to `RadixPmap` must provide:
|
|
//!
|
|
//! - `levels: []const Level`: ordered from the leaf up to the root,
|
|
//! each entry carrying the VA index shift and whether that level can
|
|
//! hold a leaf (large-page) mapping.
|
|
//! - `is_leaf_level_enabled(level: usize) bool`:runtime gate that
|
|
//! controls whether large pages are used at a given level (e.g. to
|
|
//! disable 1 GiB pages on hardware that lacks them).
|
|
//! - `Pte`: the PTE type. Must be zero-initializable and expose a
|
|
//! `present: bool` field and an `address() PAddr` method.
|
|
//! - `make_table_pte(pa: PAddr) Pte`: construct a non-leaf PTE pointing
|
|
//! to a child page table at `pa`.
|
|
//! - `make_leaf_pte(pa: PAddr, flags: MapFlags, level: usize) Pte`:
|
|
//! construct a leaf PTE mapping `pa` with the given flags at the
|
|
//! chosen level.
|
|
//! - `activate(root_pa: PAddr) void`: install the root page table into
|
|
//! the CPU (e.g. write CR3 or satp).
|
|
|
|
const r = @import("root");
|
|
const rtl = @import("rtl");
|
|
const std = @import("std");
|
|
const mm = r.mm;
|
|
const mi = mm.private;
|
|
|
|
pub const Level = struct {
|
|
shift: u6,
|
|
leaf: bool,
|
|
};
|
|
|
|
pub fn RadixPmap(comptime Impl: type) type {
|
|
return struct {
|
|
const Self = @This();
|
|
|
|
const num_levels = Impl.levels.len;
|
|
const entries_per_table = 512;
|
|
const index_mask = entries_per_table - 1;
|
|
|
|
root_pa: r.PAddr,
|
|
|
|
fn level_shift(level: usize) u6 {
|
|
return Impl.levels[level].shift;
|
|
}
|
|
|
|
fn page_size(level: usize) usize {
|
|
return @as(usize, 1) << level_shift(level);
|
|
}
|
|
|
|
fn index_for_level(va: usize, level: usize) usize {
|
|
return (va >> level_shift(level)) & index_mask;
|
|
}
|
|
|
|
fn is_leaf_level(level: usize) bool {
|
|
return Impl.levels[level].leaf;
|
|
}
|
|
|
|
fn leaf_level_enabled(level: usize) bool {
|
|
return is_leaf_level(level) and Impl.is_leaf_level_enabled(level);
|
|
}
|
|
|
|
fn cursor(self: *Self, start_va: usize) Cursor {
|
|
var tables: [num_levels][*]Impl.Pte = undefined;
|
|
tables[num_levels - 1] = @ptrFromInt(mm.p2v(self.root_pa));
|
|
return .{
|
|
.pmap = self,
|
|
.va = start_va,
|
|
.tables = tables,
|
|
.top_level = num_levels - 1,
|
|
};
|
|
}
|
|
|
|
pub fn activate(self: *Self) void {
|
|
Impl.activate(self.root_pa);
|
|
}
|
|
|
|
pub fn map_from(self: *Self, va: r.VAddr, size: usize, source: anytype) void {
|
|
var c = self.cursor(va);
|
|
var remain = size;
|
|
|
|
while (remain > 0) {
|
|
const item = source.next(c.va);
|
|
c.map_range(item.pa, item.len, item.flags);
|
|
remain -|= item.len;
|
|
}
|
|
}
|
|
|
|
pub fn query(self: *Self, va: r.VAddr) ?r.PAddr {
|
|
var c = self.cursor(va);
|
|
|
|
var level: usize = num_levels - 1;
|
|
while (true) {
|
|
const idx = index_for_level(va, level);
|
|
const pte = c.tables[level][idx];
|
|
|
|
if (!pte.present) return null;
|
|
|
|
if (level == 0 or (leaf_level_enabled(level) and pte.huge_page)) {
|
|
return pte.address() + (va & (page_size(level) - 1));
|
|
}
|
|
|
|
if (!c.walk_down_resolve(level - 1)) return null;
|
|
level -= 1;
|
|
}
|
|
}
|
|
|
|
/// 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) ?mi.PfnList {
|
|
std.debug.assert(std.mem.isAligned(va, mm.page_size));
|
|
|
|
var c = self.cursor(va);
|
|
|
|
const npages: usize = size / mm.page_size;
|
|
var list: rtl.List = undefined;
|
|
list.init();
|
|
|
|
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);
|
|
list.insert_tail(&page.free.link);
|
|
|
|
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);
|
|
}
|
|
|
|
if (list.is_empty()) return null;
|
|
|
|
const head_free: *mm.PageFree = @fieldParentPtr("link", list.first());
|
|
const tail_free: *mm.PageFree = @fieldParentPtr("link", list.last());
|
|
const head: *mm.Page = @ptrCast(head_free);
|
|
const tail: *mm.Page = @ptrCast(tail_free);
|
|
|
|
return .{ .head = mm.struct_page_to_pfn(head), .tail = mm.struct_page_to_pfn(tail) };
|
|
}
|
|
|
|
/// 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)
|
|
/// before any leaf mappings are installed.
|
|
pub fn preallocate(self: *Self, va: r.VAddr, size: usize, target_level: usize) void {
|
|
var c = self.cursor(va);
|
|
var remain = size;
|
|
|
|
// Step by one table at `target_level + 1` at a time so each
|
|
// `walk_down` call sees a fresh index at that level.
|
|
const step = page_size(target_level + 1);
|
|
|
|
while (remain > 0) {
|
|
c.walk_down(target_level, true) catch @panic("walk_down failed during preallocate");
|
|
const advance_amount = @min(remain, step);
|
|
c.advance(advance_amount);
|
|
remain -= advance_amount;
|
|
}
|
|
}
|
|
|
|
pub const Cursor = struct {
|
|
pmap: *Self,
|
|
va: r.VAddr,
|
|
/// Cached pointers to each level's page table, filled as the cursor
|
|
/// descends. Only indices in `[0, top_level]` are valid.
|
|
tables: [num_levels][*]Impl.Pte,
|
|
/// Index of the highest level for which `tables` currently holds a
|
|
/// valid pointer. Descends toward 0 (the leaf) as `walk_down` runs.
|
|
/// Reset upward in `advance` when the VA crosses a level boundary.
|
|
top_level: usize,
|
|
|
|
fn walk_down(self: *Cursor, target_level: usize, allocate: bool) error{PageNotMapped}!void {
|
|
var current_level = self.top_level;
|
|
|
|
while (current_level > target_level) {
|
|
const idx = index_for_level(self.va, current_level);
|
|
var pte = self.tables[current_level][idx];
|
|
|
|
if (!pte.present) {
|
|
if (!allocate) {
|
|
return error.PageNotMapped;
|
|
}
|
|
|
|
const new_table_pa = mi.phys.alloc();
|
|
const table_ptr: [*]Impl.Pte = @ptrFromInt(mm.p2v(new_table_pa));
|
|
@memset(table_ptr[0..entries_per_table], std.mem.zeroes(Impl.Pte));
|
|
|
|
pte = Impl.make_table_pte(new_table_pa);
|
|
self.tables[current_level][idx] = pte;
|
|
}
|
|
|
|
const child_va = mm.p2v(pte.address());
|
|
self.tables[current_level - 1] = @ptrFromInt(child_va);
|
|
self.top_level -= 1;
|
|
current_level -= 1;
|
|
}
|
|
}
|
|
|
|
pub fn walk_down_resolve(self: *Cursor, target_level: usize) bool {
|
|
self.walk_down(target_level, false) catch return false;
|
|
return true;
|
|
}
|
|
|
|
/// Choose the largest page size that satisfies the alignment and
|
|
/// size constraints of the current VA, PA, and remaining byte count.
|
|
/// Tries large pages first; falls back to 4K.
|
|
fn choose_target_level(self: *Cursor, pa: usize, remain: usize) usize {
|
|
var level: usize = num_levels;
|
|
while (level > 0) {
|
|
level -= 1;
|
|
if (!leaf_level_enabled(level)) continue;
|
|
|
|
const pg_size = page_size(level);
|
|
const align_mask = pg_size - 1;
|
|
|
|
if (remain >= pg_size and
|
|
(self.va & align_mask) == 0 and
|
|
(pa & align_mask) == 0)
|
|
{
|
|
return level;
|
|
}
|
|
}
|
|
|
|
@panic("no leaf level can map current alignment/size");
|
|
}
|
|
|
|
pub fn map_range(self: *Cursor, pa: r.PAddr, size: usize, flags: mm.MapFlags) void {
|
|
var remain = size;
|
|
var current_pa = pa;
|
|
|
|
while (remain > 0) {
|
|
const target_level = self.choose_target_level(current_pa, remain);
|
|
|
|
self.walk_down(target_level, true) catch @panic("walk_down failed during map_range");
|
|
|
|
const table = self.tables[target_level];
|
|
table[index_for_level(self.va, target_level)] =
|
|
Impl.make_leaf_pte(current_pa, flags, target_level);
|
|
|
|
const step = page_size(target_level);
|
|
current_pa += step;
|
|
remain -= step;
|
|
|
|
self.advance(step);
|
|
}
|
|
}
|
|
|
|
/// Advance the cursor's virtual address by `offset` bytes and
|
|
/// invalidate any cached table pointers that no longer apply.
|
|
/// The cursor tracks the highest level whose index is still valid
|
|
/// for the new VA, `walk_down` will re-descend from there on the
|
|
/// next mapping operation.
|
|
pub fn advance(self: *Cursor, offset: usize) void {
|
|
const old_va = self.va;
|
|
self.va +%= offset;
|
|
|
|
var level: usize = num_levels - 1;
|
|
while (level > 0) : (level -= 1) {
|
|
const shift = level_shift(level);
|
|
if ((old_va >> shift) != (self.va >> shift)) {
|
|
self.top_level = level;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
};
|
|
};
|
|
}
|