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

7.7 KiB
Raw Blame History

Page Tables и отображение памяти: paging.rs

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

x86-64 использует 4-уровневую иерархию таблиц страниц:

Виртуальный адрес (48 бит):
┌─────────┬─────────┬─────────┬─────────┬─────────┐
│  PML4   │  PDPT   │   PD    │   PT    │  offset  │
│  bits   │  bits   │  bits   │  bits   │  (12)   │
│  47:39  │  38:30  │  29:21  │  20:12  │  11:0   │
└────┬────┴────┬────┴────┬────┴────┬────┴─────────┘
     │         │         │         │
     ▼         ▼         ▼         ▼
   PML4 ──► PDPT ──►   PD  ──►   PT  ──► Физический фрейм
(512 ent) (512 ent) (512 ent) (512 ent)    (4 KiB)

Каждая запись (PTE) = 8 байт:

Bit 63:    NX (No Execute)
Bits 62:52: Ignored/AVL
Bits 51:12: Physical frame address (4KiB aligned)
Bits 11:9:  Ignored/AVL
Bits 8:     Global
Bits 7:     PS (Page Size = 1 GiB для PDPT, 2 MiB для PD)
Bits 6:     Dirty
Bits 5:     Accessed
Bits 4:     PCD (Cache Disable)
Bits 3:     PWT (Write Through)
Bits 2:     U/S (User/Supervisor)
Bits 1:     R/W (Read/Write)
Bit 0:      Present

PageTable — представление таблицы

#[repr(C, align(4096))]
pub struct PageTable {
    entries: [u64; 512],
}
  • 512 entries × 8 байт = 4096 байт (ровно одна страница).
  • align(4096) — гарантирует, что структура размещается на физической странице, как требует аппаратура.

PageTableFlags — битовые флаги

bitflags! {
    pub struct PageTableFlags: u64 {
        const PRESENT       = 1 << 0;
        const WRITABLE      = 1 << 1;
        const USER          = 1 << 2;
        const WRITE_THROUGH = 1 << 3;
        const NO_CACHE      = 1 << 4;
        const ACCESSED      = 1 << 5;
        const DIRTY         = 1 << 6;
        const HUGE_PAGE     = 1 << 7;
        const GLOBAL        = 1 << 8;
        const COW           = 1 << 9;   // Elyz-specific: Copy-on-Write marker
        const NO_EXECUTE    = 1 << 63;
    }
}

Флаг COW (бит 9) — Elyz-специфичный, используется в AddressSpace для маркировки страниц, которые нужно копировать при записи.

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

map_page(virt, phys, flags) — отображение одной страницы

pub fn map_page(&mut self, virt: VirtAddr, phys: PhysAddr, flags: PageTableFlags) {
    let p1 = self.walk_to_p1_mut(virt, true).expect("...");
    let p1_idx = ((virt.0 >> 12) & 0x1FF) as usize;
    p1.entries[p1_idx] = phys.0 | flags.bits();
    asm!("invlpg [{}]", in(reg) virt.0);
}

Процесс:

  1. walk_to_p1_mut(virt, create=true) проходит 4 уровня (P4→P3→P2→P1), создавая недостающие таблицы через pmm_alloc().
  2. Записывает физический адрес с флагами в P1 entry.
  3. INVLPG — сбрасывает TLB для этой страницы.

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

Вызывает map_page() для каждой страницы в диапазоне. size.div_ceil(4096) — количество страниц.

unmap_page(virt) — удаление отображения

pub fn unmap_page(&mut self, virt: VirtAddr) {
    // walk_to_p1_mut(virt, false) — не создаём, только ищем
    let Some(p1) = self.walk_to_p1_mut(virt, false) else { return; };
    p1.entries[p1_idx] = 0;  // Clear PTE
    asm!("invlpg [{}]", in(reg) virt.0);
}

Важно: не освобождает физический фрейм — это ответственность Caller'а (VMM). Идемпотентна: если PTE нет, ничего не делает.

translate(virt) — трансляция виртуального адреса

Проходит 4 уровня, проверяя PRESENT на каждом уровне. Поддерживает huge pages (1 GiB и 2 MiB):

  • Если P3 entry имеет HUGE_PAGE — 1 GiB страница.
  • Если P2 entry имеет HUGE_PAGE — 2 MiB страница.

get_flags(virt) / update_flags(virt) — чтение/запись флагов

get_flags возвращает флаги PTE для виртуального адреса.

update_flags меняет флаги, сохраняя физический адрес, и вызывает INVLPG.

activate(phys_addr) — загрузка в CR3

pub unsafe fn activate(&self, phys_addr: PhysAddr) {
    asm!("mov cr3, {0}", in(reg) phys_addr.0);
}

Полная замена таблиц страниц. Полный TLB flush (без PCID).

В vmm.rs::AddressSpace::activate() используется улучшенная версия с PCID и битом NOFLUSH.

walk_to_p1_mut — сердце навигации

fn walk_to_p1_mut(&mut self, virt: VirtAddr, create: bool) -> Option<&mut Self> {
    let p4_idx = ((virt.0 >> 39) & 0x1FF) as usize;
    let p3_idx = ((virt.0 >> 30) & 0x1FF) as usize;
    let p2_idx = ((virt.0 >> 21) & 0x1FF) as usize;

    let p3 = self.get_or_create_next_table(p4_idx, create)?;
    let p2 = p3.get_or_create_next_table(p3_idx, create)?;
    p2.get_or_create_next_table(p2_idx, create)
}

get_or_create_next_table(index, create) — следующий уровень

fn get_or_create_next_table(&mut self, index: usize, create: bool) -> Option<&mut Self> {
    if entry & PRESENT == 0 {
        if !create { return None; }
        // Аллоцируем новый фрейм для таблицы
        let pt_phys = pmm_alloc().expect("OOM");
        // Обнуляем (все entry = 0 = not present)
        core::ptr::write_bytes(pt_virt.as_mut_ptr::<u8>(), 0, 4096);
        // Устанавливаем entry: PRESENT | WRITABLE | USER
        self.entries[index] = pt_phys.0 | (PRESENT | WRITABLE | USER);
    }
    // Преобразуем физический адрес entry в ссылку на PageTable
    let next_phys = PhysAddr(self.entries[index] & PTE_ADDR_MASK);
    Some(unsafe { &mut *next_phys.to_virt().as_mut_ptr::<Self>() })
}

pte_addr_mask — маска адреса

const PTE_ADDR_MASK: u64 = 0x000F_FFFF_FFFF_F000;

Биты 51:12 — физический адрес с выравниванием 4KiB. (0x000F_FFFF_FFFF_F000 = 52 бита адреса, очищенные нижние 12 бит).

pmm_alloc — шлюз к PMM

pub fn pmm_alloc() -> Option<PhysAddr> {
    PMM.lock().as_mut()?.alloc_frame()
}

Тонкая обёртка, чтобы избежать циклической зависимости между paging и pmm.

Ключевые инварианты

  1. Каждая PageTable занимает ровно одну физическую страницу (4 KiB).
  2. Физический адрес таблицы всегда 4KiB-выровнен.
  3. Все intermediate таблицы создаются с флагом USER (чтобы и ядро, и пользователь могли их использовать). Разделение на user/kernel контролируется на уровне P1 entry.
  4. INVLPG вызывается после каждого изменения PTE для консистентности TLB.