# PM Actor: распределённый менеджер физической памяти ## Концептуальная модель **PMActor** (Physical Memory Actor) — это изолированный агент, который владеет фиксированным диапазоном физической памяти и управляет им через BuddyAllocator. Он следует **модели акторов**: - У каждого актора есть **почтовый ящик** (MPSC queue). - Другие компоненты **отправляют ему сообщения** (Allocate, Free, Carve). - Актёр **обрабатывает сообщения**, когда получает CPU time. - Актёр **отправляет ответы** через PM Router. ``` ┌──────────────────────────────────────────────────────┐ │ PMActor #1 │ │ │ │ ┌──────────────────────────────────────────────┐ │ │ │ MPSC Inbox (PMActorQueue) │ │ │ │ [Alloc] [Free] [Carve] [Alloc] [Free] ... │ │ │ └──────────────────────┬───────────────────────┘ │ │ │ pop │ │ ┌──────────────────────▼───────────────────────┐ │ │ │ process_messages() │ │ │ │ ┌────────────┐ ┌──────────┐ ┌──────────┐ │ │ │ │ │handle_alloc│ │handle_free│ │handle_carve│ │ │ │ │ └──────┬─────┘ └────┬─────┘ └─────┬────┘ │ │ │ │ ▼ ▼ ▼ │ │ │ │ ┌────────────────────────────────────────┐ │ │ │ │ │ BuddyAllocator │ │ │ │ │ │ [диапазон: 0x4000_0000 — 0x4000_xxxx] │ │ │ │ │ └────────────────────────────────────────┘ │ │ │ └────────────────────────┬──────────────────────┘ │ │ │ Vec │ │ ▼ │ │ Через PMRouter.route_responses() │ └──────────────────────────────────────────────────────┘ ``` ## Структура PMActor ```rust pub struct PMActor { pub actor_id: u64, // Уникальный ID pub root_untyped: Capability, // Capability на весь диапазон pub managed_range: (PhysAddr, PhysAddr), // [start, end) queue: PMActorQueue, // MPSC inbox buddy: BuddyAllocator, // Buddy-аллокатор } ``` - `actor_id` — используется в `token_sig` для уникальной идентификации. - `root_untyped` — Strong capability на весь диапазон (корень дерева). - `managed_range` — физические границы (начало, конец). - `queue` — lock-free MPSC кольцевой буфер. - `buddy` — аллокатор для этого диапазона. ## PMActorQueue — lock-free MPSC кольцевой буфер ```rust pub struct PMActorQueue { buffer: [AtomicU64; QUEUE_SIZE], // 1024 entry _pad0: [u8; 64], head: AtomicUsize, // читатель (актор) _pad1: [u8; 64], tail: AtomicUsize, // писатели (производители) } ``` ### Зачем padding? `_pad0` и `_pad1` размером 64 байта — это размер кэш-линии x86-64. Разделение head и tail по разным кэш-линиям предотвращает **false sharing** (когда два ядра пишут в разные переменные, но они на одной кэш-линии). ### send(req) — Multi-Producer ```rust pub fn send(&self, req: PMRequest) -> Result<(), &'static str> { let packed = req.pack(); 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("Queue overflow"); } // CAS: пытаемся захватить слот match self.tail.compare_exchange_weak(tail, tail+1, AcqRel, Relaxed) { Ok(_) => { self.buffer[tail & QUEUE_MASK].store(packed, Release); return Ok(()); } Err(actual) => tail = actual, } } } ``` - `wrapping_sub`: кольцевой буфер, переполнение при разнице >= QUEUE_SIZE. - `compare_exchange_weak`: позволяет нескольким продюсерам соревноваться. - Store с `Release`: гарантирует, что данные видны потребителю. ### pop() — Single-Consumer ```rust pub fn pop(&self) -> Option { 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 + 1, Ordering::Release); Some(PMRequest::unpack(packed)) } ``` - Только актор вызывает pop — никакой конкуренции. - Acquire на tail: видим последнюю запись продюсера. ## Message Packing Каждое сообщение упаковывается в одно `u64`: ``` Bit 63:56 OPCODE (8 bits) 1=Alloc, 2=Free, 3=Carve Bit 55:40 CHANNEL_ID (16 bits) маршрутизация ответа; 0 = discard Bit 39:20 ARG1 (20 bits) size_pages/local_frame_idx/offset_pages Bit 19: 0 ARG2 (20 bits) token_sig (low 20 bits) / size_pages / order ``` Максимум: 2^20 = 1 048 576 страниц = 4 GiB на одну аллокацию. ## Типы сообщений (PMRequest) ```rust pub enum PMRequest { Allocate { size_pages: usize, // сколько страниц token_sig: u32, // тег capability channel_id: u16, // куда отправить ответ }, Free { local_frame_idx: usize, // относительный индекс order: usize, // buddy order }, Carve { offset_pages: usize, // смещение от начала size_pages: usize, // размер channel_id: u16, // куда отправить ответ }, None, } ``` ## PMResponse — результат ```rust pub struct PMResponse { pub channel_id: u16, pub result: PMResult, } pub enum PMResult { Allocated { cap: Capability, order: usize }, OutOfMemory { size_pages: usize }, Carved { cap: Capability }, Freed { pages_returned: usize }, } ``` ## Обработка сообщений ### handle_allocate(size_pages, token_sig, channel_id) ```rust fn handle_allocate(&mut self, size_pages, token_sig, channel_id) -> Option { match self.buddy.alloc_pages(size_pages) { Some((rel_idx, order)) => { let phys = PhysAddr(self.managed_range.0.0 + rel_idx * PAGE_SIZE); let cap = Capability { object: CapObject::Memory { phys, size_pages: 1 << order }, rights: CapRights::READ | WRITE | GRANT, relation: Relation::Strong, token_sig: (token_sig as u64) ^ self.actor_id ^ (rel_idx as u64), }; Some(PMResponse { channel_id, result: Allocated { cap, order } }) } None => { Some(PMResponse { channel_id, result: OutOfMemory { size_pages } }) } } } ``` **Детали:** - `rel_idx` — относительный индекс в buddy (от 0 до total_pages). - Физический адрес: `managed_range.start + rel_idx * 4096`. - `token_sig` — XOR от token_sig запроса, actor_id и rel_idx для уникальности. - Размер в capability: `1 << order` (округляется до степени двойки). ### handle_free(local_frame_idx, order) ```rust fn handle_free(&mut self, local_frame_idx, order) -> Option { // Валидация границ let block_end = local_frame_idx.saturating_add(1usize << order); if block_end > self.buddy.total_pages() { debug_assert!(false, "Out of range"); return None; } self.buddy.free(local_frame_idx, order); None // Free не требует ответа } ``` ### handle_carve(offset_pages, size_pages, channel_id) Carve — выделение под-диапазона без участия buddy-аллокатора. Используется для статических размещений (framebuffer alias). ```rust fn handle_carve(&mut self, offset_pages, size_pages, channel_id) -> Option { let range_pages = self.buddy.total_pages(); // Проверка границ if offset_pages >= range_pages || size_pages == 0 || ... { return Some(PMResponse { channel_id, result: OutOfMemory { size_pages } }); } let phys = PhysAddr(self.managed_range.0.0 + offset_pages * PAGE_SIZE); let cap = Capability { object: CapObject::Memory { phys, size_pages }, rights: CapRights::READ | WRITE | GRANT, relation: Relation::Strong, token_sig: self.actor_id ^ offset_pages, }; Some(PMResponse { channel_id, result: Carved { cap } }) } ``` ## Ballooning hook (TODO) Когда `BuddyAllocator::alloc_pages` возвращает `None`, актор генерирует `OutOfMemory`. Будущая подсистема ballooning будет перехватывать этот ответ до того, как он дойдёт до запросившего, и договариваться о передаче памяти от соседнего актора. ## Владение и capabilities - PMActor держит Strong capability на весь свой диапазон. - Все выделенные или вырезанные под-диапазоны — тоже Strong (дочерние). - При освобождении через Free buddy-дерево объединяет блоки. - Актёр **никогда** не обращается к глобальному BitmapPMM. ## Максимальное количество сообщений за вызов ```rust const MAX_MESSAGES_PER_CALL: usize = 64; ``` Ограничение предотвращает голодание ядра, когда inbox глубок.