10 KiB
Virtual Memory Manager: vmm.rs
Концептуальная модель
AddressSpace — это представление виртуального адресного пространства процесса. Оно состоит из:
- Аппаратной части: PML4 (Page Map Level 4) — корневая таблица страниц.
- Программной части: 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 1–4094: пользовательские.
alloc() — Two-pass: от hint до 4094, затем от 1 до hint.
Пропускает полностью занятые 32-битные слова.
Основные операции AddressSpace
new() — создание пустого пространства
- Аллоцирует фрейм из PMM для PML4.
- Обнуляет его (все entry = not present).
- Выделяет ASID из ASID_ALLOC.
- Пустой список 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
- Находит VMA по адресу.
- Удаляет из списка.
- Вызывает
do_unmap(pml4, region):- unmap_page для каждой страницы.
- Если backing owns_frames (Anonymous) — free_frame.
- TLB flush.
handle_fault(fault_addr, write) — Page Fault handler
pub fn handle_fault(&mut self, fault_addr: VirtAddr, write: bool) -> Result<(), VmError>
- Находит VMA, содержащий fault_addr.
- Проверяет права (write + !WRITE = PermissionDenied).
- Проверяет COW:
- Если write + COW: аллоцирует новый фрейм, копирует данные, заменяет PTE, освобождает старый фрейм.
- Проверяет LAZY:
- Если LAZY + page не выделена: alloc_frame, обнуление, map_page.
- Иначе: UnexpectedFault → panic.
clone_for_fork(child_cap_token) — fork адресного пространства
- Создаёт пустое AddressSpace::new().
- Для каждого 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:
- unmap всех регионов (с освобождением owned frames).
- free_frame(pml4_phys).
- free_asid(asid).
VmError — типы ошибок
pub enum VmError {
OutOfMemory, // Нет фреймов в PMM
RegionOverlap, // VMA пересекается с существующим
RegionNotFound, // Нет VMA по адресу
InvalidAlignment, // Адрес/размер не выровнены на 4k
InvalidRange, // Нулевой размер или переполнение
PermissionDenied, // Нет прав на запись
UnexpectedFault, // Fault в не-lazy, не-COW регионе
NonCanonical, // Некорректный адрес
AsidExhausted, // Закончились ASID
}