feat: major memory managment & PMactor

This commit is contained in:
Faynot
2026-06-26 14:33:29 +03:00
parent ceedd21aee
commit 3f87a93d52
14 changed files with 2347 additions and 162 deletions

298
kernel/src/mem/buddy.rs Normal file
View File

@@ -0,0 +1,298 @@
//! # Per-Actor Buddy Allocator (`src/mem/buddy.rs`)
//!
//! A power-of-two page-block allocator for a fixed, pre-committed physical
//! region. Designed for **single-consumer** use inside a [`crate::mem::pm_manages::PMActor`]:
//! the owning actor is the only entity that ever mutates its buddy allocator, so
//! no locking is needed — the entire subsystem is inherently lock-free from the
//! consumer's perspective.
//!
//! ## Block Layout
//!
//! The region is subdivided into blocks of `2^order` pages (order 0 … MAX_ORDER).
//! Every block at order *k* is aligned to `2^k` pages within the region.
//!
//! ```text
//! order 0 → 1 page = 4 KiB
//! order 1 → 2 pages = 8 KiB
//! …
//! order 11 → 2 048 pages = 8 MiB (MAX_ORDER)
//! ```
//!
//! ## Complexity
//!
//! | Operation | Amortised | Worst-case |
//! |-----------|-----------|------------|
//! | `alloc` | O(log N) | O(MAX_ORDER · Lₖ) |
//! | `free` | O(log N) | O(MAX_ORDER · Lₖ) |
//!
//! where Lₖ = `free_lists[k].len()` ≤ `total_pages / 2^k`.
//!
//! ## Production note
//!
//! For managed ranges > 1 GiB (> 256 K pages), replace the per-order `Vec<usize>`
//! free lists with a radix tree or interval tree to bound Lₖ. For current
//! PMActor capital sizes (typically ≤ 128 MiB = 32 K pages at order 0), the
//! `Vec`-based implementation is fully adequate.
extern crate alloc;
use alloc::vec::Vec;
// ══════════════════════════════════════════════════════════════════════════════
// Constants & helpers
// ══════════════════════════════════════════════════════════════════════════════
/// Maximum allocation order.
/// `2^11 × 4 096 bytes = 8 MiB` per single allocation.
pub const MAX_ORDER: usize = 11;
/// ⌈log₂(n)⌉ — the minimum order whose block size covers `page_count` pages.
///
/// ```text
/// order_for(1) = 0 (2^0 = 1)
/// order_for(2) = 1 (2^1 = 2)
/// order_for(3) = 2 (2^2 = 4 ≥ 3)
/// order_for(2048) = 11
/// ```
#[inline]
pub fn order_for(page_count: usize) -> usize {
if page_count <= 1 {
0
} else {
// Number of bits needed to represent (page_count - 1).
usize::BITS as usize - (page_count - 1).leading_zeros() as usize
}
}
// ══════════════════════════════════════════════════════════════════════════════
// BuddyAllocator
// ══════════════════════════════════════════════════════════════════════════════
/// Buddy allocator over a contiguous, pre-committed range of physical pages.
///
/// All page indices stored in free lists are **relative to the start of the
/// managed range**. The owner (`PMActor`) translates to absolute `PhysAddr`
/// by adding `managed_range.0`.
///
/// # Invariants
/// - `free_pages ≤ total_pages` at all times.
/// - Every block in `free_lists[k]` is aligned to `2^k` pages (i.e.
/// `idx % (1 << k) == 0`).
/// - No block appears in more than one order's free list simultaneously.
pub struct BuddyAllocator {
/// `free_lists[k]` = relative page indices of free 2^k-page blocks.
/// Ordering within each list is irrelevant; `pop()` / `swap_remove()` are used.
free_lists: [Vec<usize>; MAX_ORDER + 1],
/// Total pages in the managed range (need not be a power of two).
total_pages: usize,
/// Running count of free pages. Always equals `Σ (2^k × free_lists[k].len())`.
free_pages: usize,
}
impl BuddyAllocator {
// ─── Construction ─────────────────────────────────────────────────────────
/// Create a new allocator over `total_pages` pages, **all initially free**.
///
/// Uses a greedy largest-first decomposition to build the initial free lists
/// in O(total_pages / 2^MAX_ORDER) iterations — essentially O(1) for
/// power-of-two sizes.
///
/// Example: 7 pages → blocks [4, 2, 1] → free_lists[2]=[0], [1]=[4], [0]=[6].
pub fn new(total_pages: usize) -> Self {
// core::array::from_fn is the idiomatic way to init a non-Copy array.
let free_lists: [Vec<usize>; MAX_ORDER + 1] =
core::array::from_fn(|_| Vec::new());
let mut this = Self {
free_lists,
total_pages,
free_pages: 0,
};
if total_pages == 0 {
return this;
}
let mut idx = 0usize;
while idx < total_pages {
let remaining = total_pages - idx;
// Alignment constraint: block at `idx` must be aligned to 2^order.
// trailing_zeros(0) is u32::MAX, so we clamp to MAX_ORDER.
let align_order = if idx == 0 {
MAX_ORDER
} else {
(idx.trailing_zeros() as usize).min(MAX_ORDER)
};
// Size constraint: 2^order ≤ remaining → order ≤ ⌊log₂(remaining)⌋.
let size_order = (usize::BITS as usize - 1)
- remaining.leading_zeros() as usize; // ⌊log₂(remaining)⌋
let order = MAX_ORDER.min(align_order).min(size_order);
let block_size = 1usize << order;
this.free_lists[order].push(idx);
this.free_pages += block_size;
idx += block_size;
}
this
}
// ─── Allocation ───────────────────────────────────────────────────────────
/// Allocate a 2^`order`-page block.
///
/// Returns the **relative** page index of the block's first page, or `None`
/// if insufficient contiguous memory remains.
///
/// The returned index can be converted to a physical address:
/// ```text
/// phys = actor.managed_range.0.0 + (idx as u64) * PAGE_SIZE
/// ```
pub fn alloc(&mut self, order: usize) -> Option<usize> {
if order > MAX_ORDER {
return None;
}
// Find the smallest available order ≥ requested.
let found_order = (order..=MAX_ORDER)
.find(|&o| !self.free_lists[o].is_empty())?;
// Consume one block from the found order.
let block_idx = self.free_lists[found_order]
.pop()
.expect("buddy: free_list non-empty but pop() returned None");
// Debit: we removed a 2^found_order block from free.
self.free_pages -= 1 << found_order;
// Split down to the requested order, putting buddies back into free lists.
//
// Invariant at each iteration:
// `block_idx` is the lower half of a 2^cur_order block.
// The upper half (= `block_idx + 2^(cur_order-1)`) is returned to the
// free list as an order-(cur_order-1) block.
let mut cur_order = found_order;
while cur_order > order {
cur_order -= 1;
let buddy_idx = block_idx + (1 << cur_order);
self.free_lists[cur_order].push(buddy_idx);
self.free_pages += 1 << cur_order;
}
// Net accounting: removed 2^found_order, added (2^found_order 2^order).
// Result: free_pages decreased by exactly 2^order. ✓
Some(block_idx)
}
/// Allocate `page_count` pages, rounding up to the nearest power of two.
///
/// Returns `(relative_page_idx, actual_order)`. The caller **must** pass the
/// same `order` to [`BuddyAllocator::free`] — mismatched orders corrupt the
/// allocator.
///
/// The wasted "rounding up" pages remain unusable until the block is freed.
/// For tightly-packed allocations, callers should use `alloc(order)` directly.
pub fn alloc_pages(&mut self, page_count: usize) -> Option<(usize, usize)> {
if page_count == 0 {
return None;
}
let order = order_for(page_count);
if order > MAX_ORDER {
return None;
}
self.alloc(order).map(|idx| (idx, order))
}
// ─── Deallocation ─────────────────────────────────────────────────────────
/// Return a 2^`order`-page block at **relative** index `block_idx` to the
/// free pool, coalescing with free buddies up the order chain.
///
/// # Panics (debug builds only)
/// - `order > MAX_ORDER`
/// - `block_idx + 2^order > total_pages`
///
/// In release builds the checks are elided for performance; passing incorrect
/// arguments causes undefined bookkeeping, not UB (no unsafe indexing).
pub fn free(&mut self, mut block_idx: usize, mut order: usize) {
debug_assert!(
order <= MAX_ORDER,
"buddy::free: order {} > MAX_ORDER {}",
order,
MAX_ORDER
);
debug_assert!(
block_idx.checked_add(1 << order).map_or(false, |e| e <= self.total_pages),
"buddy::free: block [{}, +{}] out of range (total_pages={})",
block_idx,
1usize << order,
self.total_pages
);
// Try to coalesce with our buddy at each order.
//
// The buddy of a block at relative index `i` of order `k` is at:
// buddy_idx = i XOR 2^k
// This works because aligned buddy pairs always differ in exactly bit k.
while order < MAX_ORDER {
let buddy_idx = block_idx ^ (1 << order);
// Buddy must lie entirely within the managed range.
let buddy_end = match buddy_idx.checked_add(1 << order) {
Some(e) => e,
None => break,
};
if buddy_end > self.total_pages {
break;
}
// Search for the buddy in the free list for this order.
// `swap_remove` is O(1) and order-preserving is not required.
if let Some(pos) = self.free_lists[order].iter().position(|&b| b == buddy_idx) {
self.free_lists[order].swap_remove(pos);
self.free_pages -= 1 << order; // buddy was in free count; remove it
// Merged block starts at the lower address of the two.
block_idx = block_idx.min(buddy_idx);
order += 1;
// Continue trying to merge at the next level.
} else {
break; // buddy is allocated or out-of-range; stop coalescing
}
}
// Push the (possibly merged) block onto the appropriate free list.
self.free_pages += 1 << order;
self.free_lists[order].push(block_idx);
}
// ─── Introspection ────────────────────────────────────────────────────────
/// Number of pages currently available for allocation.
#[inline]
pub fn free_pages(&self) -> usize {
self.free_pages
}
/// Total pages in the managed range.
#[inline]
pub fn total_pages(&self) -> usize {
self.total_pages
}
/// `true` if the allocator has no free pages.
#[inline]
pub fn is_exhausted(&self) -> bool {
self.free_pages == 0
}
/// Dump free-list statistics for each order (useful in panic handlers).
///
/// Returns an array `[(order, free_block_count); MAX_ORDER + 1]`.
pub fn stats(&self) -> [(usize, usize); MAX_ORDER + 1] {
core::array::from_fn(|o| (o, self.free_lists[o].len()))
}
}