feat: major memory managment & PMactor
This commit is contained in:
@@ -1,25 +1,491 @@
|
||||
use crate::cap::{Capability, CapObject, Relation, CapRights};
|
||||
use crate::mem::address::PhysAddr;
|
||||
//! # PM Actor (`src/mem/pm_manages.rs`)
|
||||
//!
|
||||
//! Distributed physical-memory actor that owns a fixed physical capital range
|
||||
//! and processes allocation requests asynchronously via a lock-free MPSC queue.
|
||||
//!
|
||||
//! ## Key changes vs. previous version
|
||||
//!
|
||||
//! 1. **Real allocator**: `PMActor` now contains a `BuddyAllocator` instead of a
|
||||
//! bare page counter. Allocations produce real relative frame indices that
|
||||
//! translate to absolute `PhysAddr` values.
|
||||
//!
|
||||
//! 2. **Closed response loop**: `process_messages()` returns `Vec<PMResponse>`.
|
||||
//! The caller (scheduler / boot code) routes each `PMResponse` to the
|
||||
//! requesting entity identified by `channel_id`. `channel_id = 0` means
|
||||
//! "discard" and is safe to ignore.
|
||||
//!
|
||||
//! 3. **Revised message packing** — fits all fields into a single `u64` without
|
||||
//! truncating critical identifiers:
|
||||
//! ```text
|
||||
//! bits 63:56 OPCODE (8 bits) 1=Alloc, 2=Free, 3=Carve
|
||||
//! bits 55:40 CHANNEL_ID (16 bits) response routing; 0 = discard
|
||||
//! bits 39:20 ARG1 (20 bits) size_pages / local_frame_idx / offset_pages
|
||||
//! bits 19: 0 ARG2 (20 bits) token_sig (low 20 bits) / size_pages
|
||||
//! ```
|
||||
//! Maximum ARG size: 2^20 = 1 048 576 pages = 4 GiB per allocation.
|
||||
//! Maximum CHANNEL_ID: 65 535 simultaneous pending requesters.
|
||||
//!
|
||||
//! ## Ownership model
|
||||
//!
|
||||
//! `PMActor` holds a `Strong` capability over its entire managed range.
|
||||
//! Sub-capabilities carved out via `Allocate` or `Carve` are `Strong` children;
|
||||
//! freeing them (via `Free`) coalesces the buddy tree. The actor **never**
|
||||
//! touches the global `BitmapPMM` — that belongs to the kernel boot path.
|
||||
//!
|
||||
//! ## Ballooning hook
|
||||
//!
|
||||
//! When `BuddyAllocator::alloc_pages` returns `None`, the actor emits a
|
||||
//! `PMResponse::OutOfMemory { channel_id }`. The future ballooning subsystem
|
||||
//! will intercept this before it reaches the requester and negotiate memory
|
||||
//! transfer from a neighbouring actor.
|
||||
|
||||
pub struct PMActor {
|
||||
pub root_untyped: Capability,
|
||||
pub managed_range: (PhysAddr, PhysAddr),
|
||||
use alloc::vec::Vec;
|
||||
use core::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
|
||||
|
||||
use crate::cap::{CapObject, CapRights, Capability, Relation};
|
||||
use crate::mem::address::PhysAddr;
|
||||
use crate::mem::buddy::BuddyAllocator;
|
||||
use crate::mem::pmm::PAGE_SIZE;
|
||||
|
||||
extern crate alloc;
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════════════
|
||||
// Message packing constants
|
||||
// ══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
const QUEUE_SIZE: usize = 1024; // power-of-two
|
||||
const QUEUE_MASK: usize = QUEUE_SIZE - 1;
|
||||
|
||||
/// Bit layout of a packed PMRequest u64.
|
||||
mod packing {
|
||||
pub const OPCODE_SHIFT: u32 = 56;
|
||||
pub const CHAN_SHIFT: u32 = 40;
|
||||
pub const ARG1_SHIFT: u32 = 20;
|
||||
// ARG2 sits in bits 19:0 (no shift).
|
||||
|
||||
pub const CHAN_MASK: u64 = 0x0000_FFFF; // 16 bits
|
||||
pub const ARG_MASK: u64 = 0x000F_FFFF; // 20 bits
|
||||
}
|
||||
|
||||
impl PMActor {
|
||||
pub fn carve_region(&self, offset: usize, size: usize) -> Capability {
|
||||
if let CapObject::Memory { phys, .. } = self.root_untyped.object {
|
||||
Capability {
|
||||
object: CapObject::Memory {
|
||||
phys: PhysAddr(phys.0 + offset as u64),
|
||||
size_pages: size / 4096,
|
||||
},
|
||||
rights: CapRights::READ | CapRights::WRITE | CapRights::GRANT,
|
||||
relation: Relation::Strong,
|
||||
token_sig: 0xDEAD_BEEF,
|
||||
// ══════════════════════════════════════════════════════════════════════════════
|
||||
// Request type
|
||||
// ══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/// Asynchronous request submitted to a `PMActor` inbox.
|
||||
///
|
||||
/// `channel_id` identifies where the response should be routed.
|
||||
/// Use `channel_id = 0` if you do not need a reply.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum PMRequest {
|
||||
/// Allocate `size_pages` pages; tag the resulting capability with `token_sig`.
|
||||
Allocate {
|
||||
size_pages: usize,
|
||||
token_sig: u32,
|
||||
channel_id: u16,
|
||||
},
|
||||
/// Free a previously allocated block.
|
||||
///
|
||||
/// `local_frame_idx` is the **relative** page index returned inside the
|
||||
/// `Allocated` capability (i.e. `cap.object.phys - managed_range.start`).
|
||||
/// `order` is the buddy order used at allocation time (stored in the cap).
|
||||
Free {
|
||||
local_frame_idx: usize,
|
||||
order: usize,
|
||||
},
|
||||
/// Carve a fixed sub-region without consulting the buddy allocator.
|
||||
/// Used for static layout decisions made at boot (e.g. framebuffer alias).
|
||||
Carve {
|
||||
offset_pages: usize,
|
||||
size_pages: usize,
|
||||
channel_id: u16,
|
||||
},
|
||||
/// Sentinel — never pushed onto the queue; result of unpack(0).
|
||||
None,
|
||||
}
|
||||
|
||||
impl PMRequest {
|
||||
/// Encode the request into one `u64` atom for the lock-free buffer.
|
||||
///
|
||||
/// Opcode 0 (None) is reserved as the "slot empty" sentinel; `None` returns
|
||||
/// 0 intentionally and the sender rejects it before pushing.
|
||||
#[inline]
|
||||
pub fn pack(self) -> u64 {
|
||||
use packing::*;
|
||||
match self {
|
||||
Self::Allocate { size_pages, token_sig, channel_id } => {
|
||||
(1u64 << OPCODE_SHIFT)
|
||||
| ((channel_id as u64 & CHAN_MASK) << CHAN_SHIFT)
|
||||
| ((size_pages as u64 & ARG_MASK) << ARG1_SHIFT)
|
||||
| (token_sig as u64 & ARG_MASK)
|
||||
}
|
||||
} else {
|
||||
panic!("PM: Root is not memory!");
|
||||
Self::Free { local_frame_idx, order } => {
|
||||
(2u64 << OPCODE_SHIFT)
|
||||
// channel_id not needed — Free has no reply
|
||||
| ((local_frame_idx as u64 & ARG_MASK) << ARG1_SHIFT)
|
||||
| (order as u64 & ARG_MASK)
|
||||
}
|
||||
Self::Carve { offset_pages, size_pages, channel_id } => {
|
||||
(3u64 << OPCODE_SHIFT)
|
||||
| ((channel_id as u64 & CHAN_MASK) << CHAN_SHIFT)
|
||||
| ((offset_pages as u64 & ARG_MASK) << ARG1_SHIFT)
|
||||
| (size_pages as u64 & ARG_MASK)
|
||||
}
|
||||
Self::None => 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Decode a packed `u64` back into a `PMRequest`.
|
||||
#[inline]
|
||||
pub fn unpack(val: u64) -> Self {
|
||||
use packing::*;
|
||||
let opcode = val >> OPCODE_SHIFT;
|
||||
let channel_id = ((val >> CHAN_SHIFT) & CHAN_MASK) as u16;
|
||||
let arg1 = ((val >> ARG1_SHIFT) & ARG_MASK) as usize;
|
||||
let arg2 = (val & ARG_MASK) as usize;
|
||||
|
||||
match opcode {
|
||||
1 => Self::Allocate {
|
||||
size_pages: arg1,
|
||||
token_sig: arg2 as u32,
|
||||
channel_id,
|
||||
},
|
||||
2 => Self::Free {
|
||||
local_frame_idx: arg1,
|
||||
order: arg2,
|
||||
},
|
||||
3 => Self::Carve {
|
||||
offset_pages: arg1,
|
||||
size_pages: arg2,
|
||||
channel_id,
|
||||
},
|
||||
_ => Self::None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════════════
|
||||
// Response type
|
||||
// ══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/// Result returned by `PMActor::process_messages()` for each completed request.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct PMResponse {
|
||||
/// Channel to route this response to. `0` = discard.
|
||||
pub channel_id: u16,
|
||||
/// The actual outcome.
|
||||
pub result: PMResult,
|
||||
}
|
||||
|
||||
/// Outcome of a single PM operation.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum PMResult {
|
||||
/// Memory was allocated. `cap` is the strong capability to the region.
|
||||
/// `order` is the buddy order — **must** be passed back to `Free`.
|
||||
Allocated { cap: Capability, order: usize },
|
||||
|
||||
/// The actor had insufficient free pages.
|
||||
/// Future: ballooning subsystem intercepts this and retries.
|
||||
OutOfMemory { size_pages: usize },
|
||||
|
||||
/// A fixed sub-region was carved successfully.
|
||||
Carved { cap: Capability },
|
||||
|
||||
/// Free completed (no capability issued — memory returned to buddy pool).
|
||||
Freed { pages_returned: usize },
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════════════
|
||||
// Lock-free MPSC inbox
|
||||
// ══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/// MPSC ring buffer for PMRequest values.
|
||||
///
|
||||
/// Producers (any core, any context) call `send`; the owning PMActor calls
|
||||
/// `pop` — exactly one consumer, no locking on the read side.
|
||||
pub struct PMActorQueue {
|
||||
buffer: [AtomicU64; QUEUE_SIZE],
|
||||
/// Pad to separate producer-written `tail` from consumer-read `head`.
|
||||
_pad0: [u8; 64],
|
||||
head: AtomicUsize,
|
||||
_pad1: [u8; 64],
|
||||
tail: AtomicUsize,
|
||||
}
|
||||
|
||||
impl PMActorQueue {
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
#[allow(clippy::declare_interior_mutable_const)]
|
||||
buffer: [const { AtomicU64::new(0) }; QUEUE_SIZE],
|
||||
_pad0: [0u8; 64],
|
||||
head: AtomicUsize::new(0),
|
||||
_pad1: [0u8; 64],
|
||||
tail: AtomicUsize::new(0),
|
||||
}
|
||||
}
|
||||
|
||||
/// Push a request into the inbox (Multi-Producer path).
|
||||
///
|
||||
/// Returns `Err` only if the ring buffer is full — which should trigger
|
||||
/// backpressure or a ballooning request in production.
|
||||
pub fn send(&self, req: PMRequest) -> Result<(), &'static str> {
|
||||
let packed = req.pack();
|
||||
if packed == 0 {
|
||||
return Err("PMActorQueue::send: PMRequest::None is not sendable");
|
||||
}
|
||||
|
||||
let mut tail = self.tail.load(Ordering::Relaxed);
|
||||
loop {
|
||||
let head = self.head.load(Ordering::Acquire);
|
||||
if tail.wrapping_sub(head) >= QUEUE_SIZE {
|
||||
return Err("PMActorQueue overflow — apply backpressure or balloon");
|
||||
}
|
||||
|
||||
match self.tail.compare_exchange_weak(
|
||||
tail,
|
||||
tail.wrapping_add(1),
|
||||
Ordering::AcqRel,
|
||||
Ordering::Relaxed,
|
||||
) {
|
||||
Ok(_) => {
|
||||
self.buffer[tail & QUEUE_MASK].store(packed, Ordering::Release);
|
||||
return Ok(());
|
||||
}
|
||||
Err(actual) => tail = actual,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Pop one request (Single-Consumer path — actor's own context only).
|
||||
pub fn pop(&self) -> Option<PMRequest> {
|
||||
let head = self.head.load(Ordering::Relaxed);
|
||||
let tail = self.tail.load(Ordering::Acquire);
|
||||
if head == tail {
|
||||
return None;
|
||||
}
|
||||
let packed = self.buffer[head & QUEUE_MASK].load(Ordering::Acquire);
|
||||
self.head.store(head.wrapping_add(1), Ordering::Release);
|
||||
Some(PMRequest::unpack(packed))
|
||||
}
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════════════
|
||||
// PM Actor
|
||||
// ══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/// An autonomous physical-memory actor.
|
||||
///
|
||||
/// Owns a `BuddyAllocator` over its capital range and a lock-free MPSC inbox.
|
||||
/// The actor must be driven externally: when the scheduler grants it CPU time,
|
||||
/// it calls `process_messages()` to drain the inbox and produce `PMResponse`s.
|
||||
///
|
||||
/// # Single-consumer guarantee
|
||||
/// Only `process_messages()` ever mutates `buddy` and `queue.head`.
|
||||
/// This is enforced by taking `&mut self` on `process_messages`.
|
||||
pub struct PMActor {
|
||||
/// Unique identity within the actor federation.
|
||||
pub actor_id: u64,
|
||||
|
||||
/// Root strong capability over the entire managed physical range.
|
||||
pub root_untyped: Capability,
|
||||
|
||||
/// `(inclusive_start, exclusive_end)` physical addresses.
|
||||
pub managed_range: (PhysAddr, PhysAddr),
|
||||
|
||||
/// Inbox — producers write here, actor reads here.
|
||||
queue: PMActorQueue,
|
||||
|
||||
/// Local buddy allocator. Only ever touched in `process_messages`.
|
||||
buddy: BuddyAllocator,
|
||||
}
|
||||
|
||||
impl PMActor {
|
||||
/// Construct a new actor managing `size_bytes` bytes starting at `start`.
|
||||
///
|
||||
/// `size_bytes` must be a multiple of `PAGE_SIZE`; non-multiples are
|
||||
/// rounded down silently.
|
||||
pub fn new(
|
||||
actor_id: u64,
|
||||
root_cap: Capability,
|
||||
start: PhysAddr,
|
||||
size_bytes: u64,
|
||||
) -> Self {
|
||||
let total_pages = (size_bytes / PAGE_SIZE) as usize;
|
||||
Self {
|
||||
actor_id,
|
||||
root_untyped: root_cap,
|
||||
managed_range: (start, PhysAddr(start.0 + total_pages as u64 * PAGE_SIZE)),
|
||||
queue: PMActorQueue::new(),
|
||||
buddy: BuddyAllocator::new(total_pages),
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Producer API (callable from any context) ──────────────────────────
|
||||
|
||||
/// Submit a request to this actor's inbox.
|
||||
///
|
||||
/// Thread-safe (MPSC producer side). Returns `Err` if the inbox is full.
|
||||
#[inline]
|
||||
pub fn submit_request(&self, req: PMRequest) -> Result<(), &'static str> {
|
||||
self.queue.send(req)
|
||||
}
|
||||
|
||||
// ─── Consumer API (actor's own scheduled context) ─────────────────────
|
||||
|
||||
/// Drain the inbox and execute all pending requests.
|
||||
///
|
||||
/// Returns a `Vec` of responses that the caller (scheduler) must route to
|
||||
/// the appropriate process or actor mailbox based on `channel_id`.
|
||||
///
|
||||
/// Responses with `channel_id == 0` can be discarded.
|
||||
///
|
||||
/// # No locks held
|
||||
/// `buddy` and `queue.head` are mutated — no other thread touches them.
|
||||
/// The only shared state is the queue's `tail`, which is written by producers
|
||||
/// via `AtomicUsize::compare_exchange_weak`, never by this path.
|
||||
pub fn process_messages(&mut self) -> Vec<PMResponse> {
|
||||
let mut responses = Vec::new();
|
||||
|
||||
while let Some(req) = self.queue.pop() {
|
||||
let resp = match req {
|
||||
PMRequest::Allocate { size_pages, token_sig, channel_id } => {
|
||||
self.handle_allocate(size_pages, token_sig, channel_id)
|
||||
}
|
||||
PMRequest::Free { local_frame_idx, order } => {
|
||||
self.handle_free(local_frame_idx, order)
|
||||
}
|
||||
PMRequest::Carve { offset_pages, size_pages, channel_id } => {
|
||||
self.handle_carve(offset_pages, size_pages, channel_id)
|
||||
}
|
||||
PMRequest::None => continue,
|
||||
};
|
||||
|
||||
// Only push responses that need routing.
|
||||
// Free responses (channel_id == 0) are still pushed so callers can
|
||||
// audit completion if needed; they may simply drop them.
|
||||
if let Some(r) = resp {
|
||||
responses.push(r);
|
||||
}
|
||||
}
|
||||
|
||||
responses
|
||||
}
|
||||
|
||||
// ─── Introspection ────────────────────────────────────────────────────
|
||||
|
||||
/// Free pages remaining in this actor's buddy pool.
|
||||
#[inline]
|
||||
pub fn free_pages(&self) -> usize {
|
||||
self.buddy.free_pages()
|
||||
}
|
||||
|
||||
/// Total pages this actor was initialised with.
|
||||
#[inline]
|
||||
pub fn total_pages(&self) -> usize {
|
||||
self.buddy.total_pages()
|
||||
}
|
||||
}
|
||||
|
||||
// ── Private handler implementations ──────────────────────────────────────────
|
||||
|
||||
impl PMActor {
|
||||
fn handle_allocate(
|
||||
&mut self,
|
||||
size_pages: usize,
|
||||
token_sig: u32,
|
||||
channel_id: u16,
|
||||
) -> Option<PMResponse> {
|
||||
match self.buddy.alloc_pages(size_pages) {
|
||||
Some((rel_idx, order)) => {
|
||||
// Translate relative index → absolute physical address.
|
||||
let phys = PhysAddr(self.managed_range.0.0 + rel_idx as u64 * PAGE_SIZE);
|
||||
|
||||
// Build a strong child capability.
|
||||
// token_sig is widened and XOR'd with actor_id for uniqueness.
|
||||
let cap = Capability {
|
||||
object: CapObject::Memory {
|
||||
phys,
|
||||
// Report actual (rounded-up) page count so the caller
|
||||
// passes the correct `order` back to Free.
|
||||
size_pages: 1 << order,
|
||||
},
|
||||
rights: CapRights::READ | CapRights::WRITE | CapRights::GRANT,
|
||||
relation: Relation::Strong,
|
||||
token_sig: (token_sig as u64) ^ self.actor_id ^ (rel_idx as u64),
|
||||
};
|
||||
|
||||
Some(PMResponse {
|
||||
channel_id,
|
||||
result: PMResult::Allocated { cap, order },
|
||||
})
|
||||
}
|
||||
None => {
|
||||
// TODO: emit a ballooning request to a neighbour actor before
|
||||
// propagating OutOfMemory upward.
|
||||
Some(PMResponse {
|
||||
channel_id,
|
||||
result: PMResult::OutOfMemory { size_pages },
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_free(
|
||||
&mut self,
|
||||
local_frame_idx: usize,
|
||||
order: usize,
|
||||
) -> Option<PMResponse> {
|
||||
// Validate bounds before touching the buddy tree.
|
||||
let block_end = local_frame_idx.saturating_add(1usize << order);
|
||||
if block_end > self.buddy.total_pages() {
|
||||
// Corrupted or spoofed index — ignore silently in release, panic in debug.
|
||||
debug_assert!(
|
||||
false,
|
||||
"PMActor {}: Free out of range (idx={}, order={}, total={})",
|
||||
self.actor_id,
|
||||
local_frame_idx,
|
||||
order,
|
||||
self.buddy.total_pages()
|
||||
);
|
||||
return None;
|
||||
}
|
||||
|
||||
self.buddy.free(local_frame_idx, order);
|
||||
|
||||
// Free never needs routing — return None to skip Vec push, or push with
|
||||
// channel_id=0 for audit purposes. We skip to avoid unnecessary allocation.
|
||||
// Callers that need a Free-complete signal should use a separate mechanism.
|
||||
None
|
||||
}
|
||||
|
||||
fn handle_carve(
|
||||
&mut self,
|
||||
offset_pages: usize,
|
||||
size_pages: usize,
|
||||
channel_id: u16,
|
||||
) -> Option<PMResponse> {
|
||||
// Validate offset + size within range.
|
||||
let range_pages = self.buddy.total_pages();
|
||||
if offset_pages >= range_pages
|
||||
|| size_pages == 0
|
||||
|| offset_pages.saturating_add(size_pages) > range_pages
|
||||
{
|
||||
return Some(PMResponse {
|
||||
channel_id,
|
||||
result: PMResult::OutOfMemory { size_pages },
|
||||
});
|
||||
}
|
||||
|
||||
let phys = PhysAddr(self.managed_range.0.0 + offset_pages as u64 * PAGE_SIZE);
|
||||
let cap = Capability {
|
||||
object: CapObject::Memory { phys, size_pages },
|
||||
rights: CapRights::READ | CapRights::WRITE | CapRights::GRANT,
|
||||
relation: Relation::Strong,
|
||||
token_sig: self.actor_id ^ offset_pages as u64,
|
||||
};
|
||||
|
||||
Some(PMResponse {
|
||||
channel_id,
|
||||
result: PMResult::Carved { cap },
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user