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

10 KiB
Raw Blame History

Virtual Memory Manager: vmm.rs

Концептуальная модель

AddressSpace — это представление виртуального адресного пространства процесса. Оно состоит из:

  1. Аппаратной части: PML4 (Page Map Level 4) — корневая таблица страниц.
  2. Программной части: VMA (Virtual Memory Area) список — отсортированный перечень отображённых регионов с метаданными.
AddressSpace
┌─────────────────────────────────────────────┐
│ asid: u16 (PCID = Process Context ID)        │
│ pml4_phys: PhysAddr (адрес PML4)             │
│ regions: Vec<VmaRegion> (сортирован по адресу)│
└─────────────────────────────────────────────┘
                     │
                     ▼
             ┌───────────────┐
             │    PML4 (P4)  │ ◄── CR3
             ├───────────────┤
             │   PDE (P3)    │
             ├───────────────┤
             │   PTE (P2)    │
             ├───────────────┤
             │   PTE (P1)    │ ◄── 4 KiB страницы
             └───────────────┘

VmaRegion — регион виртуальной памяти

pub struct VmaRegion {
    pub virt_start: VirtAddr,    // начало (выровнено на 4k)
    pub virt_end:   VirtAddr,    // конец (эксклюзивный)
    pub flags:      VmaFlags,    // READ, WRITE, EXEC, USER, LAZY, SHARED, COW...
    pub cap_token:  u64,         // токен capability (для revoke)
    pub backing:    VmaBacking,  // откуда берутся физические страницы
}

VmaBacking — источник физических страниц

pub enum VmaBacking {
    Anonymous(Vec<Option<PhysAddr>>),
    // ^ Owned: каждая страница выделена из PMM под этот VMA.
    //   Option: None = lazy (страница ещё не выделена).

    Physical(PhysAddr),
    // ^ Не-owned: всё отображение — непрерывный физический диапазон.
    //   Frame не освобождаются при unmap.

    Shared { owner_cap: u64, phys_base: PhysAddr },
    // ^ Zero-copy shared mapping: чужие физические страницы.
    //   Frame не освобождаются при unmap.
    //   При revoke owner_cap — все Shared VMA с этим токеном удаляются.
}

VmaFlags

pub struct VmaFlags: u32 {
    const READ    = 1 << 0;
    const WRITE   = 1 << 1;
    const EXEC    = 1 << 2;
    const USER    = 1 << 3;
    const LAZY    = 1 << 4;   // Demand-paging: page fault = alloc frame
    const SHARED  = 1 << 5;   // Zero-copy shared mapping
    const PINNED  = 1 << 6;   // Защищён от revoke
    const NOCACHE = 1 << 7;   // Cache disabled (для MMIO)
    const MMIO    = 1 << 8;   // Memory-mapped I/O
    const COW     = 1 << 9;   // Copy-on-Write active
}

to_page_flags() транслирует VmaFlags в PageTableFlags:

  • PRESENT всегда
  • WRITE → WRITABLE
  • EXEC → NO_EXECUTE (инвертировано!)
  • LAZY → PRESENT без фрейма (fault отлавливается)
  • COW → COW (кастомный бит 9)

ASID Allocator — выделение PCID

x86-64 PCID (Process Context Identifier) — 12-битный идентификатор (0-4095).

struct AsidAllocator {
    bitmap: [u32; 128],  // 4096 бит
    next_hint: u16,       // хинт для амортизированного O(1)
}
  • ASID 0: kernel (no PCID tagging) — всегда занят.
  • ASID 4095: reserved by Intel spec — всегда занят.
  • ASID 14094: пользовательские.

alloc() — Two-pass: от hint до 4094, затем от 1 до hint. Пропускает полностью занятые 32-битные слова.

Основные операции AddressSpace

new() — создание пустого пространства

  1. Аллоцирует фрейм из PMM для PML4.
  2. Обнуляет его (все entry = not present).
  3. Выделяет ASID из ASID_ALLOC.
  4. Пустой список regions.

from_active(pml4_phys, asid) — обёртка существующего PML4

Используется для kernel space (ASID 0).

map_region(virt, phys, size, flags, cap_token) — отображение

pub fn map_region(&mut self, virt: VirtAddr, phys: Option<PhysAddr>,
                  size: u64, flags: VmaFlags, cap_token: u64) -> Result<VirtAddr, VmError>
  • phys = Some(base): Physical mapping — непрерывный диапазон.
  • phys = None + LAZY: Demand-paging — фреймы выделяются при fault'е.
  • phys = None + !LAZY: Eager alloc — все фреймы выделяются сейчас.

Создаёт VmaRegion, вставляет в отсортированный список.

map_shared(virt, phys_base, page_count, flags, owner_cap) — zero-copy

Как map_region, но:

  • Не копирует фреймы (не владеет ими).
  • Всегда eagerly отображает (адреса известны).
  • Записывает cap_token = owner_cap для revoke.

unmap_region(virt) — удаление VMA

  1. Находит VMA по адресу.
  2. Удаляет из списка.
  3. Вызывает do_unmap(pml4, region):
    • unmap_page для каждой страницы.
    • Если backing owns_frames (Anonymous) — free_frame.
  4. TLB flush.

handle_fault(fault_addr, write) — Page Fault handler

pub fn handle_fault(&mut self, fault_addr: VirtAddr, write: bool) -> Result<(), VmError>
  1. Находит VMA, содержащий fault_addr.
  2. Проверяет права (write + !WRITE = PermissionDenied).
  3. Проверяет COW:
    • Если write + COW: аллоцирует новый фрейм, копирует данные, заменяет PTE, освобождает старый фрейм.
  4. Проверяет LAZY:
    • Если LAZY + page не выделена: alloc_frame, обнуление, map_page.
  5. Иначе: UnexpectedFault → panic.

clone_for_fork(child_cap_token) — fork адресного пространства

  1. Создаёт пустое AddressSpace::new().
  2. Для каждого VMA родителя:
    • Physical/Shared: просто копирует PTE (та же физическая память).
    • Anonymous (owned): COW.
      • Устанавливает COW флаг на родительские PTE.
      • Убирает WRITABLE с родительских PTE.
      • Отображает те же фреймы в дочернее пространство (без WRITABLE).
      • При записи → handle_fault → copy-on-write.

revoke_by_token(cap_token) / do_revoke_by_token(cap_token) — отзыв

Удаляет все нем-PINNED VMA с заданным cap_token.

  • Собирает индексы регионов, соответствующих токену.
  • Удаляет в обратном порядке (чтобы индексы не сдвигались).
  • TLB flush.

process_pending_revocations() — обработка очереди отзыва

Дренирует глобальную MMU_REVOCATION_QUEUE и revoke-ит каждый токен. Вызывается перед handle_fault при page fault'е, чтобы избежать deadlock с capability subsystem.

TLB Management

local_tlb_flush_asid(asid)сброс TLB для одного ASID

fn local_tlb_flush_asid(asid: u16) {
    if INVPCID_SUPPORTED {
        // INVPCID type 1 (single-context): сбрасывает только
        // TLB entries, помеченные данным PCID.
        asm!("invpcid {ty}, [{desc}]", ...);
    } else {
        // Fallback: полный сброс TLB (все контексты)
        tlb_flush_all();
    }
}

tlb_flush_asid(asid) — TLB shootdown (SMP)

pub fn tlb_flush_asid(asid: u16) {
    local_tlb_flush_asid(asid);  // всегда сбрасываем локальный

    let target_mask = active_cpus & !(1 << current_core);
    if target_mask == 0 { return; }

    // Lock SHOOTDOWN_LOCK → записываем ASID → broadcast IPI → wait ACK
    SHOOTDOWN_ASID.store(asid);
    SHOOTDOWN_ACK.store(0);
    lapic::broadcast_ipi_exclude_self(TLB_SHOOTDOWN_VECTOR);

    while SHOOTDOWN_ACK.load() & target_mask != target_mask {
        spin_loop();
    }
}

tlb_flush_all() — полный сброс

Перезапись CR3 без PCID NOFLUSH бита.

AddressSpace::activate() — контекстный переключатель

pub unsafe fn activate(&self) {
    let cr3 = self.pml4_phys.0 | u64::from(self.asid) | (1u64 << 63);
    // ^ Биты 11:0 = ASID, бит 63 = NOFLUSH (не сбрасывать TLB)
    asm!("mov cr3, {0}", in(reg) cr3);
}

Глобальный kernel space

pub static KERNEL_SPACE: Locked<Option<AddressSpace>> = Locked::new(None);

Инициализируется в init_kernel_space(pml4_phys) после создания первичных page tables.

Drop для AddressSpace

При Drop:

  1. unmap всех регионов (с освобождением owned frames).
  2. free_frame(pml4_phys).
  3. free_asid(asid).

VmError — типы ошибок

pub enum VmError {
    OutOfMemory,             // Нет фреймов в PMM
    RegionOverlap,           // VMA пересекается с существующим
    RegionNotFound,          // Нет VMA по адресу
    InvalidAlignment,        // Адрес/размер не выровнены на 4k
    InvalidRange,            // Нулевой размер или переполнение
    PermissionDenied,        // Нет прав на запись
    UnexpectedFault,         // Fault в не-lazy, не-COW регионе
    NonCanonical,            // Некорректный адрес
    AsidExhausted,           // Закончились ASID
}