Files
Elyz/kernel/docs/memory/pm-actor.md
2026-07-07 16:40:41 +03:00

11 KiB
Raw Permalink Blame History

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<PMResponse>            │
│                           ▼                            │
│                  Через PMRouter.route_responses()      │
└──────────────────────────────────────────────────────┘

Структура PMActor

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 кольцевой буфер

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

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

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 + 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)

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 — результат

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)

fn handle_allocate(&mut self, size_pages, token_sig, channel_id) -> Option<PMResponse> {
    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)

fn handle_free(&mut self, local_frame_idx, order) -> Option<PMResponse> {
    // Валидация границ
    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).

fn handle_carve(&mut self, offset_pages, size_pages, channel_id) -> Option<PMResponse> {
    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.

Максимальное количество сообщений за вызов

const MAX_MESSAGES_PER_CALL: usize = 64;

Ограничение предотвращает голодание ядра, когда inbox глубок.