diff --git a/.PLAN.md.swp b/.PLAN.md.swp deleted file mode 100644 index 68d93f4..0000000 Binary files a/.PLAN.md.swp and /dev/null differ diff --git a/kernel/docs/README.md b/kernel/docs/README.md deleted file mode 100644 index 46e8804..0000000 --- a/kernel/docs/README.md +++ /dev/null @@ -1,19 +0,0 @@ -# Документация ядра LISA - -LISA v0.1.0 — capability-based микроядро для x86-64, написанное на Rust (nightly). - -## Разделы - -| Раздел | Описание | -|---|---| -| [Архитектура](architecture.md) | Общая архитектура и взаимосвязи подсистем | -| [Точка входа](main.md) | `kmain()`, инициализация, FramebufferDisplay, тесты | -| [Физическая память](memory-management.md) | BitmapPMM, BuddyAllocator, Page Tables, адресация | -| [Виртуальная память](vmm.md) | AddressSpace, VMA, COW, Lazy, TLB shootdown, ASID/PCID | -| [Акторы памяти](pmactor.md) | PMActor, PMRouter, BuddyAllocator, асинхронные запросы | -| [Capability system](capabilities.md) | CNode, Capability, mint/revoke, KernelObject | -| [Прерывания](interrupts.md) | IDT, обработчики page fault / TLB shootdown, LAPIC | -| [Консоль и отладка](console.md) | TTY, Serial, макросы логирования | -| [Аллокатор кучи](allocator.md) | SlabAllocator, Locked, GlobalAlloc | -| [Очередь отзыва](events.md) | RevocationQueue, MMU уведомления | -| [Сборочная система](build-system.md) | Cargo, linker scripts, GNUmakefile, toolchain | diff --git a/kernel/docs/allocator.md b/kernel/docs/allocator.md deleted file mode 100644 index 9e722d1..0000000 --- a/kernel/docs/allocator.md +++ /dev/null @@ -1,105 +0,0 @@ -# Аллокатор кучи - -**Файл**: `src/mem/allocator.rs` - -Slab-аллокатор со spinlock-синхронизацией и fallback bump-аллокацией для больших блоков. - ---- - -## Примитив синхронизации: `Locked` - -Самодельный spinlock (альтернатива `spin::Mutex`). - -```rust -pub struct Locked { - inner: UnsafeCell, - lock: AtomicBool, -} -``` - -- `new(inner)` — создаёт с unlocked состоянием -- `lock() -> LockedGuard<'_, A>` — CAS-цикл на `AtomicBool` (спин-ожидание) - -### LockedGuard - -- `Deref`/`DerefMut` — доступ к внутренним данным -- `Drop` — `store(false, Release)` — освобождение блокировки - -`unsafe impl Sync for Locked` — разработчик гарантирует корректность. - ---- - -## SlabAllocator - -```rust -pub struct SlabAllocator { - list_heads: [Option<&'static mut ListNode>; 9], // slab free list - large_block_free: Option<&'static mut LargeBlockNode>, // free list для >2048 - heap_start: usize, - heap_end: usize, - next_bump: usize, -} -``` - -### Slab классы - -```rust -const BLOCK_SIZES: &[usize] = &[8, 16, 32, 64, 128, 256, 512, 1024, 2048]; -``` - -9 классов — степени двойки от 8 до 2048. - -### ListNode / LargeBlockNode - -```rust -struct ListNode { - next: Option<&'static mut ListNode>, -} - -struct LargeBlockNode { - size: usize, - next: Option<&'static mut LargeBlockNode>, -} -``` - -Односвязные списки свободных блоков. - -### `init(&mut self, start, size)` - -Устанавливает границы кучи: `heap_start = start`, `heap_end = start + size`, `next_bump = start`. - -### `list_index(layout) -> Option` - -Находит индекс наименьшего slab-класса, покрывающего `max(size, align)`. - -Пример: layout size=20, align=8 → блок 20 > 16, следующий 32 → index 3 (32 байта). - -### `fallback_alloc(&mut self, layout) -> *mut u8` - -1. Проверяет `large_block_free` — если есть подходящий блок, отдаёт его -2. Иначе bump-аллокация: выравнивание, проверка `next_bump + size <= heap_end`, возврат `next_bump` -3. При OOM возвращает null - ---- - -## Глобальный аллокатор - -```rust -#[global_allocator] -pub static ALLOCATOR: Locked; -``` - -### `GlobalAlloc::alloc(&self, layout) -> *mut u8` - -1. Lock allocator -2. Если `layout.size()` влезает в slab класс: - - Находит `list_index` - - Если free list не пуст: pop голову, вернуть указатель - - Если пуст: `fallback_alloc(layout)` но с размером slab-блока (не `layout.size()`) -3. Для блоков > 2048: сразу `fallback_alloc(layout)` - -### `GlobalAlloc::dealloc(&self, ptr, layout)` - -1. Lock allocator -2. Для slab-sized: push обратно в slab free list как ListNode -3. Для больших: push в large_block_free как LargeBlockNode diff --git a/kernel/docs/architecture.md b/kernel/docs/architecture.md deleted file mode 100644 index 37630ad..0000000 --- a/kernel/docs/architecture.md +++ /dev/null @@ -1,56 +0,0 @@ -# Архитектура ядра LISA - -LISA — **capability-based микроядро**. Вся физическая память управляется через систему capability (прав доступа к объектам). Ядро использует загрузчик **Limine** (протокол Limine boot protocol). - -## Общая схема - -``` -kmain() - ├── BitmapPMM.init() ── глобальный менеджер фреймов - ├── LAPIC.init() ── доступ к Local APIC - ├── PageTable::activate() ── включение страничной памяти - ├── SlabAllocator.init() ── куча ядра - ├── VMM::init_kernel_space() ── адресное пространство ядра - ├── IDT::load() ── обработчики прерываний - ├── PMRouter::init() ── 65536 каналов - ├── CNode (cap тест) ── insert → mint → revoke - │ └── RevocationQueue.push() ── уведомление VMM - └── PMActor тест - ├── PMRouter.alloc_channel() - ├── PMActor.submit_request() - ├── PMActor.process_messages() - └── BuddyAllocator (внутри PMActor) -``` - -## Поток ревокации - -``` -CNode::revoke() - → RevocationQueue.push(token_sig) - → (page fault handler) - → process_deferred_mmu_events() - → AddressSpace::process_pending_revocations() - → RevocationQueue.pop() - → AddressSpace::revoke_by_token(token) // анмаппинг VMA - → TLB flush -``` - -## Поток PMActor запроса - -``` -1. PMRouter::alloc_channel() → получаем channel_id -2. PMActor::submit_request(Allocate { channel_id, ... }) -3. (актор обрабатывает) PMActor::process_messages() -4. PMActor::handle_allocate() → BuddyAllocator::alloc_pages() -5. PMRouter::route_responses([PMResponse { channel_id, result }]) -6. PMRouter::wait_for_response(channel_id) → spin до READY -7. Возврат результата клиенту -``` - -## Ключевые принципы - -- **Lock-free очереди** — MPSC кольцевые буферы везде, где возможен контеншен (PMActorQueue, RevocationQueue, PMRouter channels) -- **Double-write logging** — каждое сообщение дублируется в фреймбуфер (консоль) и последовательный порт -- **HHDM** — Higher Half Direct Map: весь физическая память отображена 1:1 в верхней половине адресного пространства через HHDM offset -- **ASID/PCID** — аппаратная изоляция TLB между адресными пространствами -- **Распределённое управление памятью** — PMM выделяет только сырые фреймы, PMActor управляет диапазонами через BuddyAllocator diff --git a/kernel/docs/boot/introduction.md b/kernel/docs/boot/introduction.md new file mode 100644 index 0000000..ef11509 --- /dev/null +++ b/kernel/docs/boot/introduction.md @@ -0,0 +1,152 @@ +# Процесс загрузки: концептуальная модель + +## Цепочка загрузки + +``` +Питание включено + │ + ▼ +CPU reset vector (0xFFFFFFF0) + │ + ▼ +UEFI firmware / BIOS + │ + ▼ +Limine bootloader ─────────────────────────────┐ + │ │ + │ 1. Переводит CPU в 64-bit long mode │ + │ 2. Настраивает page tables (identity map) │ + │ 3. Загружает ядро по физическому адресу │ + │ 4. Настраивает HHDM │ + │ 5. Заполняет Limine requests │ + │ 6. Передаёт управление на kmain │ + │ │ + ▼ │ +kmain() (точка входа) │ + │ │ + ... инициализация ... │ + │ │ + ▼ │ +HCF (останов CPU) ─────────────────────────────┘ +``` + +## Limine Boot Protocol + +Elyz использует [Limine](https://github.com/limine-bootloader/limine) — +современный bootloader с открытым исходным кодом. + +Статические `Limine requests` сообщают bootloader'у, что нужно ядру: + +| Request | Назначение | +|---------|------------| +| `FramebufferRequest` | Получить framebuffer для графического вывода | +| `MemoryMapRequest` | Получить карту физической памяти | +| `HhdmRequest` | Получить HHDM offset | +| `ExecutableAddressRequest` | Получить физический/виртуальный адрес ядра | +| `BaseRevision` | Проверить совместимость с Limine | + +### Размещение в секциях + +Requests размещаются между специальными маркерами в секции `.data`: + +```rust +#[used] +#[unsafe(link_section = ".requests_start_marker")] +static _START_MARKER: RequestsStartMarker = RequestsStartMarker::new(); + +// ... все requests ... + +#[used] +#[unsafe(link_section = ".requests_end_marker")] +static _END_MARKER: RequestsEndMarker = RequestsEndMarker::new(); +``` + +Линкер скрипт сохраняет эти секции: +```ld +.data : { + *(.data .data.*) + KEEP(*(.requests_start_marker)) + KEEP(*(.requests)) + KEEP(*(.requests_end_marker)) +} :data +``` + +## Точка входа — kmain + +Линкер скрипт: `ENTRY(kmain)`. + +```rust +#[unsafe(no_mangle)] +unsafe extern "C" fn kmain() -> ! { ... } +``` + +Атрибуты: +- `no_mangle` — сохраняет имя `kmain` (линкер ищет именно его). +- `extern "C"` — C ABI (Linux x86-64 calling convention: RDI, RSI, ...). +- `unsafe` — на этапе инициализации все операции потенциально опасны. +- `-> !` — kmain никогда не возвращается (HALT). + +## Build system + +### GNUmakefile + +```makefile +KARCH ?= x86_64 +RUST_TARGET ?= $(KARCH)-unknown-none +RUST_PROFILE ?= dev + +all: + RUSTFLAGS="-C relocation-model=static" \ + cargo build --target $(RUST_TARGET) --profile $(RUST_PROFILE) + cp target/$(RUST_TARGET)/$(RUST_PROFILE_SUBDIR)/kernel . +``` + +- `relocation-model=static`: ядро не использует динамическую перелокацию. +- `--target x86_64-unknown-none`: bare-metal target (без ОС). +- Результат копируется в `kernel/` (корень). + +### build.rs — linker script + +```rust +fn main() { + let arch = std::env::var("CARGO_CFG_TARGET_ARCH").unwrap(); + println!("cargo:rustc-link-arg=-Tlinker-{arch}.ld"); + println!("cargo:rerun-if-changed=linker-{arch}.ld"); +} +``` + +Подставляет правильный linker script для архитектуры. + +### rust-toolchain.toml + +```toml +[toolchain] +channel = "nightly" +targets = ["x86_64-unknown-none"] +``` + +Требуется nightly Rust из-за: +- `#![no_std]`, `#![no_main]`. +- `core::arch::global_asm!`, `core::arch::asm!`. +- `const { ... }` в инициализации констант. + +## Память: расположение после загрузки + +``` +Физическая память: +┌───────────────────────┐ 0x0 +│ Reserved │ +├───────────────────────┤ +│ Usable (free) │ ← входит в mmap entries +├───────────────────────┤ +│ Kernel image │ ← загружен Limine +├───────────────────────┤ +│ PMM metadata │ ← bitmap + ref_counts + L1 +├───────────────────────┤ +│ ... (usable) │ +└───────────────────────┘ max_addr + +Виртуальная память (Higher Half): +0xFFFF_8000_0000_0000 ─── HHDM (вся физическая память 1:1) +0xFFFF_9000_0000_0000 ─── Kernel heap (8 MiB) +``` diff --git a/kernel/docs/boot/linker.md b/kernel/docs/boot/linker.md new file mode 100644 index 0000000..052317e --- /dev/null +++ b/kernel/docs/boot/linker.md @@ -0,0 +1,151 @@ +# Линкер-скрипты: архитектурные детали + +## Назначение + +Линкер-скрипты управляют расположением секций ELF-образа ядра. +Для каждой архитектуры — свой скрипт, но все они следуют одной схеме. + +## Общая структура + +``` +OUTPUT_FORMAT(...) ← формат ELF (зависит от архитектуры) +ENTRY(kmain) ← точка входа + +PHDRS ← сегменты (program headers) +{ + text PT_LOAD; + rodata PT_LOAD; + data PT_LOAD; +} + +SECTIONS ← расположение секций +{ + . = 0xffffffff80000000; ← база higher half + + .text : { *(.text .text.*) } :text + + . = ALIGN(MAXPAGESIZE); + .rodata : { *(.rodata .rodata.*) } :rodata + + . = ALIGN(MAXPAGESIZE); + .data : { + *(.data .data.*) + KEEP(*(.requests_start_marker)) + KEEP(*(.requests)) + KEEP(*(.requests_end_marker)) + } :data + + .bss : { *(.bss .bss.*) *(COMMON) } :data + + /DISCARD/ : { *(.eh_frame*) *(.note .note.*) } +} +``` + +## Детали + +### OUTPUT_FORMAT + +| Архитектура | Формат | +|-------------|--------| +| x86_64 | `elf64-x86-64` | +| aarch64 | `elf64-littleaarch64` | +| riscv64 | `elf64-littleriscv` | +| loongarch64 | `elf64-loongarch` | + +### Базовый адрес: 0xFFFFFFFF80000000 + +Ядро размещается в **higher half** — верхней 2 GiB виртуального +адресного пространства. Это стандартная практика для x86-64: + +``` +0x0000_0000_0000_00000 ─── user space (не используется ядром) +0xFFFF_8000_0000_00000 ─── kernel space (higher half) +0xFFFF_FFFF_FFFF_FFFF ─── конец +``` + +Любой адрес в области 0xFFFF800000000000 — 0xFFFFFFFFFFFFFFFF корректен; +0xFFFFFFFF80000000 выбран как начало typical higher half региона. + +### PHDRS: PT_LOAD сегменты + +Bootloader загружает только PT_LOAD сегменты. Их три: + +1. **text**: код + inline-константы. +2. **rodata**: неизменяемые данные (строки, таблицы). +3. **data**: изменяемые данные + BSS. + +### Section alignment + +```ld +. = ALIGN(CONSTANT(MAXPAGESIZE)); +``` + +MAXPAGESIZE = 0x1000 (4 KiB). Каждая секция начинается с новой страницы, +что даёт bootloader'у правильные MMU permissionы: +- text = read + execute (no write) +- rodata = read (no write, no execute) +- data = read + write (no execute) + +### Limine Requests в .data + +```ld +KEEP(*(.requests_start_marker)) +KEEP(*(.requests)) +KEEP(*(.requests_end_marker)) +``` + +- `KEEP` — запрещает линкеру выбрасывать эти секции (dead code elimination). +- `.requests_start_marker` и `.requests_end_marker` — маркеры границ. +- Bootloader сканирует память между ними, чтобы найти requests. + +### BSS + +```ld +.bss : { + *(.bss .bss.*) + *(COMMON) +} :data +``` + +- BSS — неинициализированные глобальные переменные. +- Занимает место в виртуальной памяти, но не в ELF-файле. +- Bootloader обнуляет BSS перед передачей управления. + +### DISCARD + +```ld +/DISCARD/ : { + *(.eh_frame*) + *(.note .note.*) +} +``` + +- `.eh_frame*` — исключительные фреймы C++/Rust unwinding. +- `.note.*` — ELF notes. +- Не нужны bare-metal ядру, могут вызвать проблемы. + +## Архитектурные различия + +### RISC-V + +```ld +.data : { + *(.data .data.*) + KEEP(*(.requests_start_marker)) + KEEP(*(.requests)) + KEEP(*(.requests_end_marker)) + *(.sdata .sdata.*) ← RISC-V: small data +} :data + +.bss : { + *(.sbss .sbss.*) ← RISC-V: small BSS + *(.bss .bss.*) + *(COMMON) +} :data +``` + +RISC-V имеет `.sdata`/`.sbss` секции для small data (GP-relative addressing). + +### AArch64 и LoongArch64 + +Идентичны x86_64, за исключением OUTPUT_FORMAT. diff --git a/kernel/docs/build-system.md b/kernel/docs/build-system.md deleted file mode 100644 index 069522e..0000000 --- a/kernel/docs/build-system.md +++ /dev/null @@ -1,134 +0,0 @@ -# Сборочная система - ---- - -## Cargo.toml - -**Файл**: `Cargo.toml` - -```toml -[package] -name = "LISA" -version = "0.1.0" -edition = "2024" - -[lib] -# н/д — только бинарный крейт - -[[bin]] -name = "kernel" -path = "src/main.rs" - -[dependencies] -limine = "0.5" # Limine boot protocol -embedded-graphics = "0.8" # 2D фреймбуфер рисование -bitflags = "2.11.0" # bitflags макрос - -[profile.dev] -panic = "abort" # без раскрутки стека - -[profile.release] -panic = "abort" -``` - ---- - -## build.rs - -**Файл**: `build.rs` - -```rust -fn main() { - let arch = std::env::var("CARGO_CFG_TARGET_ARCH").unwrap(); - println!("cargo:rustc-link-arg=-Tlinker-{}.ld", arch); - println!("cargo:rerun-if-changed=linker-{}.ld", arch); -} -``` - -Передаёт линкер-скрипт в зависимости от архитектуры: `-Tlinker-x86_64.ld`, `-Tlinker-aarch64.ld`, etc. - ---- - -## Linker скрипты - -### Общая структура (все 4 архитектуры) - -- **Entry**: `kmain` -- **Base address**: `0xFFFFFFFF80000000` — высшие 2 GiB, по спецификации Limine -- **Program headers**: `PT_LOAD` для `.text`, `.rodata`, `.data` - -### Секции - -``` -SECTIONS { - .text : { *(.text .text.*) } → код - .rodata : { *(.rodata .rodata.*) } → только чтение (page-aligned) - .data : { - *(.requests_start_marker) → Limine requests - *(.requests) - *(.requests_end_marker) - *(.data .data.*) - } - .bss : { *(.bss .bss.*) } → нули - /DISCARD/ : { *(.eh_frame*) *(.note.*) } -} -``` - -### Архитектурные различия - -| Архитектура | OUTPUT_FORMAT | Особенности | -|---|---|---| -| x86-64 | `elf64-x86-64` | Стандартный | -| aarch64 | `elf64-littleaarch64` | Стандартный | -| riscv64 | `elf64-littleriscv` | `.data` + `.sdata`, `.bss` + `.sbss` | -| loongarch64 | `elf64-loongarch` | Стандартный | - ---- - -## GNUmakefile - -**Файл**: `GNUmakefile` - -### Переменные - -```makefile -OUTPUT := kernel -KARCH ?= x86_64 -RUST_TARGET := $(KARCH)-unknown-none -# riscv64 -> riscv64gc-unknown-none-elf -RUST_PROFILE ?= dev -``` - -### Цели - -| Цель | Действие | -|---|---| -| `all` | `RUSTFLAGS="-C relocation-model=static" cargo build --target ...` + копирование `target/.../kernel` → `./kernel` | -| `clean` | `cargo clean` + rm `./kernel` | -| `distclean` | То же, что clean | - -`relocation-model=static` — запрещает позиционно-независимый код (ядро загружается по фиксированному адресу). - ---- - -## rust-toolchain.toml - -**Файл**: `rust-toolchain.toml` - -```toml -[toolchain] -channel = "nightly" -targets = ["x86_64-unknown-none"] -# aarch64-unknown-none, riscv64gc-unknown-none-elf, loongarch64-unknown-none (для будущего) -``` - ---- - -## .gitignore - -**Файл**: `.gitignore` - -``` -/kernel # бинарный файл ядра -/target # артефакты сборки Cargo -``` diff --git a/kernel/docs/capabilities.md b/kernel/docs/capabilities.md deleted file mode 100644 index 987dffd..0000000 --- a/kernel/docs/capabilities.md +++ /dev/null @@ -1,147 +0,0 @@ -# Система Capability - ---- - -## Дескрипторы Capability - -**Файл**: `src/cap/descriptor.rs` - -### Relation - -```rust -pub enum Relation { - Strong, // владение (исключительный доступ) - Borrow, // заимствование (временный доступ) - Transfer, // передача (владение перешло) -} -``` - -### CapObject - -```rust -pub enum CapObject { - Empty, // null (слот свободен) - Memory { phys: PhysAddr, size_pages: usize }, // регион физической памяти - CNode { phys: PhysAddr, slots: usize }, // узел capability (таблица) - PMActor { id: u64 }, // ссылка на PMActor -} -``` - -### CapRights (bitflags, u8) - -```rust -READ = 1 << 0 -WRITE = 1 << 1 -EXECUTE = 1 << 2 -GRANT = 1 << 3 // разрешение на mint (порождение потомков) -``` - -### Capability - -```rust -pub struct Capability { - pub object: CapObject, // ссылка на объект - pub rights: CapRights, // права доступа - pub relation: Relation, // тип отношений - pub token_sig: u64, // уникальный токен для отзыва (MMU) -} -``` - -- `empty()` — возвращает Capability с `CapObject::Empty` -- `is_valid() -> bool` — `object != Empty` - ---- - -## Объекты ядра - -**Файл**: `src/cap/object.rs` - -### ObjectType - -```rust -pub enum ObjectType { - Untyped, // сырая нетипизированная память - Frame, // выделенный физический фрейм - CNode, // узел capability - ThreadBlock, // блок управления потоком - PageTable, // страница таблицы -} -``` - -### KernelObject - -```rust -pub struct KernelObject { - pub phys_addr: PhysAddr, - pub size_bits: u8, // размер как степень двойки - pub obj_type: ObjectType, - pub ref_count: AtomicUsize, // атомарный счётчик ссылок - pub owner_id: u64, // ID владельца -} -``` - -- `add_ref()` — atomic increment (Relaxed ordering) -- `release() -> bool` — atomic decrement (Release). Возвращает true, если счётчик достиг 0 - ---- - -## CNode - -**Файл**: `src/cap/mod.rs` - -Узел capability, хранит массив слотов с индивидуальной блокировкой. - -### CNodeSlot - -```rust -struct CNodeSlot { - cap: Capability, - parent_idx: Option, // индекс родителя (для дерева) — None у корневых -} -``` - -### CNode - -```rust -pub struct CNode { - slots: Vec>, -} -``` - -### `new(size) -> Self` - -Создаёт `size` пустых слотов с `parent_idx = None`. - -### `insert(slot, cap) -> Result<(), &str>` - -Проверка границ, сохранение Capability в слоте. - -### `mint(src, dest, relation, rights) -> Result<(), &str>` - -**Порождение дочерней capability с урезанными правами:** - -1. **Lock ordering**: блокировка слотов по возрастанию индекса (deadlock prevention) -2. Source-слот не пуст, имеет GRANT право -3. `child_rights = parent_rights & requested_rights` — дочерние права не могут превышать родительские -4. Копирует Capability, устанавливает новые права, relation, `parent_idx` -5. Ошибка при `src == dest` - -### `revoke(slot_idx) -> Result<(), &str>` - -**Каскадный отзыв с уведомлением VMM:** - -1. `revoke_internal(slot_idx)` — рекурсивно находит и уничтожает всех потомков -2. Уничтожает саму Capability в слоте (Empty) -3. Пушит `token_sig` в `MMU_REVOCATION_QUEUE` (кроме 0 и `0xDEAD_BEEF`) -4. **Паника** при переполнении очереди - -### `revoke_internal(slot_idx)` - -Приватная рекурсия: -1. Сканирует все слоты в поисках `parent_idx == slot_idx` -2. Для каждого потомка: рекурсивный вызов -3. Затем уничтожает свой слот (Empty) - -### `get_cap(slot) -> Option` - -Возвращает копию Capability. diff --git a/kernel/docs/capability/cnode.md b/kernel/docs/capability/cnode.md new file mode 100644 index 0000000..765aabb --- /dev/null +++ b/kernel/docs/capability/cnode.md @@ -0,0 +1,146 @@ +# CNode: таблица capability: `mod.rs` + +## Концептуальная модель + +**CNode** (Capability Node) — это массив слотов, каждый из которых +может хранить один capability. Это аналог файловой таблицы в Unix, +но для capabilities. + +``` +CNode { + slots: Vec> +} + +CNodeSlot { + cap: Capability, + parent_idx: Option, // индекс родительского слота +} +``` + +## Инициализация + +```rust +pub fn new(size: usize) -> Self { + let mut slots = Vec::with_capacity(size); + for _ in 0..size { + slots.push(Locked::new(CNodeSlot { + cap: Capability::empty(), + parent_idx: None, + })); + } + Self { slots } +} +``` + +Все слоты изначально пустые (`Capability::empty()`). + +## Операции + +### insert(slot, cap) — вставка + +```rust +pub fn insert(&self, slot: usize, cap: Capability) -> Result<(), &'static str> { + if slot >= self.slots.len() { return Err("Index out of bounds"); } + let mut s = self.slots[slot].lock(); + s.cap = cap; + Ok(()) +} +``` + +Простая вставка без проверки (перезаписывает существующий). + +### mint(src, dest, relation, rights) — создание потомка + +```rust +pub fn mint(&self, src: usize, dest: usize, relation: Relation, rights: CapRights) -> Result<(), &'static str> +``` + +**Валидация:** +- src и dest в пределах массива. +- src != dest (дедлок не имеет смысла, но блокировка была бы корректна). +- src содержит валидный capability. +- src имеет GRANT. + +**Процесс:** +1. Захват блокировок в порядке возрастания индекса (lock ranking). +2. Вычисление `final_rights = src.rights & rights`. +3. Копирование Capability в dest с новыми правами и relation. +4. Установка `parent_idx = Some(src)`. + +### revoke(slot_idx) — отзыв + +```rust +pub fn revoke(&self, slot_idx: usize) -> Result<(), &'static str> +``` + +Каскадное удаление: + +``` +revoke_internal(slot_idx): + │ + ├── 1. Поиск потомков: + │ for i in 0..slots.len(): + │ if slots[i].parent_idx == Some(slot_idx): + │ revoke_internal(i) ← рекурсивно! + │ + ├── 2. Уничтожение себя: + │ cap = Capability::empty() + │ parent_idx = None + │ token = old_token_sig + │ + └── 3. Отправка token в очередь: + if token != 0 && token != 0xDEAD_BEEF: + MMU_REVOCATION_QUEUE.push(token) +``` + +**Почему нет блокировок при рекурсии?** +- На каждом шаге проверка `is_child` захватывает и отпускает блокировку. +- Рекурсивный вызов происходит **после** освобождения блокировки. +- Это предотвращает взаимоблокировки. + +**Фильтр токенов:** +- `token == 0`: пустой/невалидный capability. +- `token == 0xDEAD_BEEF`: сырой Untyped (для тестов/отладки). +- Эти токены не отправляются в очередь (бессмысленно). + +### get_cap(slot) — чтение + +```rust +pub fn get_cap(&self, slot: usize) -> Option { + self.slots.get(slot).map(|s| s.lock().cap) +} +``` + +## Lock Ranking — детали + +```rust +let (src_slot, dest_slot) = if src < dest { + _guard_low = self.slots[src].lock(); // меньший → первый + _guard_high = self.slots[dest].lock(); // больший → второй +} else { + _guard_low = self.slots[dest].lock(); + _guard_high = self.slots[src].lock(); +}; +``` + +**Причина:** если поток A делает mint(5, 10), а поток B делает mint(10, 5), +без lock ranking они могут взаимно заблокироваться: +- A: lock(5) → ждёт lock(10) +- B: lock(10) → ждёт lock(5) + +С lock ranking: +- A: lock(5) → lock(10) +- B: lock(5) → ждёт... → дождался → lock(10) + +## Интеграция с событиями + +После `revoke()`, токен попадает в `MMU_REVOCATION_QUEUE` (см. +`events.rs`). VMM обрабатывает очередь в `process_pending_revocations()`. + +## Особенности реализации + +1. **Vec>**: каждый слот — отдельная spinlock-ячейка. + Это позволяет параллельно читать разные слоты. +2. **`pub slots`**: прямой доступ к слоту возможен (для тестов). +3. **panic на overflow**: если очередь отзыва переполнена — паника. + Это критический сбой подсистемы ресурсов. diff --git a/kernel/docs/capability/descriptors.md b/kernel/docs/capability/descriptors.md new file mode 100644 index 0000000..17fed8e --- /dev/null +++ b/kernel/docs/capability/descriptors.md @@ -0,0 +1,115 @@ +# Дескрипторы capability: `descriptor.rs` + +## Назначение + +Файл определяет базовые типы capability-системы: +что такое capability, какие бывают объекты, права и отношения. + +## CapObject — что представляет capability + +```rust +pub enum CapObject { + Empty, // Пустой слот + Memory { phys: PhysAddr, size_pages: usize }, // Фрейм памяти + CNode { phys: PhysAddr, slots: usize }, // Другой CNode + PMActor { id: u64 }, // PM Actor +} +``` + +### Memory + +Capability на физическую память: +- `phys` — физический адрес начала. +- `size_pages` — размер в страницах. + +### CNode + +Capability на другой CNode: +- `phys` — физический адрес CNode. +- `slots` — количество слотов. + +### PMActor + +Capability на PMActor: +- `id` — уникальный идентификатор актора. + +### Empty + +Слот пуст. `is_valid()` возвращает `false`. + +## CapRights — права доступа + +```rust +bitflags! { + pub struct CapRights: u8 { + const READ = 1 << 0; // 0x01 — чтение + const WRITE = 1 << 1; // 0x02 — запись + const EXECUTE = 1 << 2; // 0x04 — исполнение + const GRANT = 1 << 3; // 0x08 — разрешение на mint + } +} +``` + +**Правила:** +- Права дочернего capability = `родительские_права & запрошенные_права`. +- Нельзя расширить права: если родитель не имеет GRANT, mint невозможен. +- `CapRights::all()` = R | W | X | G = 0x0F. + +## Relation — тип связи + +```rust +pub enum Relation { + Strong, // Владелец — сильная ссылка (объект жив, пока есть Strong) + Borrow, // Заёмщик — временный доступ + Transfer, // Передача — владение переходит без возможности отзыва +} +``` + +- **Strong**: capability владеет объектом. При revoke, объект может + быть уничтожен или возвращён пулу. +- **Borrow**: capability предоставляет временный доступ. + При revoke родителя, borrow-потомки тоже отзываются. +- **Transfer**: полная передача владения. Используется при IPC. + +## Capability — полный дескриптор + +```rust +#[derive(Debug, Clone, Copy)] +pub struct Capability { + pub object: CapObject, // Целевой объект + pub rights: CapRights, // Права доступа + pub relation: Relation, // Тип связи + pub token_sig: u64, // Уникальный подписывающий токен +} +``` + +### token_sig — назначение + +- Уникальный 64-битный идентификатор capability. +- Используется для: + 1. **Отзыва**: при revoke, token_sig отправляется в MMU_REVOCATION_QUEUE. + 2. **Идентификации в VMM**: VMA хранят `cap_token == token_sig`. + 3. **Отладки**: каждый capability можно однозначно отследить. + +### Методы + +```rust +impl Capability { + pub const fn empty() -> Self { + Self { + object: CapObject::Empty, + rights: CapRights::empty(), + relation: Relation::Borrow, + token_sig: 0, + } + } + + pub fn is_valid(&self) -> bool { + !matches!(self.object, CapObject::Empty) + } +} +``` + +`token_sig = 0` зарезервирован для пустых/невалидных capability. +`token_sig = 0xDEAD_BEEF` используется для сырых Untyped +(не отправляется в MMU_REVOCATION_QUEUE, см. `cap/mod.rs`). diff --git a/kernel/docs/capability/introduction.md b/kernel/docs/capability/introduction.md new file mode 100644 index 0000000..a307a19 --- /dev/null +++ b/kernel/docs/capability/introduction.md @@ -0,0 +1,126 @@ +# Capability-система: концептуальная модель + +## Философия + +**Capability** (дескриптор возможности) — это **неподделываемый токен**, +дающий право выполнить определённую операцию над определённым объектом. + +В традиционных ОС (Linux, Windows) доступ контролируется через: +- PID + UID/GID + проверка при каждом системном вызове. +- MMU: page tables определяют, что отображено, но не кто отобразил. + +В модели capabilities: +- **Если у вас нет capability — у вас нет доступа.** +- Capability хранятся в CNode — защищённой таблице, доступной только ядру. +- Capability можно создавать только от родительского capability + (иерархия наследования). +- Права можно только **урезать** (mint), но не расширить. +- Capability можно **отозвать** (revoke), что уничтожает его + и всех его потомков. + +## Основные понятия + +``` +Capability { + object: CapObject, // на что указывает (Memory, CNode, PMActor...) + rights: CapRights, // права (R, W, X, G) + relation: Relation, // Strong (владеет), Borrow (заём), Transfer + token_sig: u64, // уникальный идентификатор (для revoke) +} + +Relation { + Strong: владеет объектом (capability владеет памятью) + Borrow: заём — временный доступ без права распоряжаться + Transfer: передача — владение переходит получателю +} + +CapRights { + READ = 0x1, + WRITE = 0x2, + EXECUTE = 0x4, + GRANT = 0x8, // разрешение создавать дочерние capability +} +``` + +## Иерархия и отзыв + +``` +CNode (массив слотов) +┌────────┬────────┬────────┬────────┐ +│ slot 0 │ slot 1 │ slot 2 │ slot 3 │ ... +├────────┼────────┼────────┼────────┤ +│ cap │ cap │ cap │ cap │ +│ parent:│ parent:│ parent:│ parent:│ +│ None │ Some(0)│ Some(0)│ Some(1)│ +└────────┴────────┴────────┴────────┘ + │ + ┌────────┴────────┐ + ▼ ▼ + slot 1 slot 2 + (mint from 0) (mint from 0) + +revoke(0) → slot 0 уничтожается + → рекурсивно: slot 1, slot 2 тоже уничтожаются + → token_sig slot 0 отправляется в MMU_REVOCATION_QUEUE + → VMM обработает отзыв при следующем page fault +``` + +### Mint — создание дочернего capability + +```rust +cnode.mint(src, dest, relation, rights) +``` + +- `src` — исходный слот (должен иметь GRANT). +- `dest` — целевой слот (должен быть пустым). +- `relation` — как наследник связан с родителем. +- `rights` — права наследника (∩ с правами родителя). + +### Revoke — отзыв capability + +```rust +cnode.revoke(slot_idx) +``` + +1. Рекурсивно находит всех потомков и уничтожает их. +2. Уничтожает сам capability. +3. Отправляет `token_sig` в глобальную `MMU_REVOCATION_QUEUE`. +4. VMM при следующем page fault обрабатывает все накопленные отзывы. + +## Lock Ranking — предотвращение дедлоков + +В `mint()` захватываются две блокировки (src и dest). Чтобы избежать +инверсии блокировок, используется строгий порядок: + +```rust +let (src_slot, dest_slot) = if src < dest { + // Захватываем меньший индекс первым + _guard_low = self.slots[src].lock(); + _guard_high = self.slots[dest].lock(); +} else { + _guard_low = self.slots[dest].lock(); + _guard_high = self.slots[src].lock(); +}; +``` + +## Интеграция с VMM + +При отзыве capability, VMM должна аннулировать все VMA, связанные +с отозванным токеном. Для этого: + +1. `revoke()` пушит `token_sig` в lock-free очередь. +2. При page fault: `process_pending_revocations()` дренирует очередь. +3. VMA с `cap_token == token_sig` удаляются из AddressSpace. +4. TLB flush для синхронизации MMU. + +## Использование в kmain() + +```rust +let root_cnode = CNode::new(256); +// Вставка capability на фрейм +root_cnode.insert(0, mem_cap).unwrap(); +// Mint с урезанными правами +root_cnode.mint(0, 10, Relation::Borrow, R | W).unwrap(); +// Revoke — отзыв всех потомков +root_cnode.revoke(0); +``` diff --git a/kernel/docs/capability/objects.md b/kernel/docs/capability/objects.md new file mode 100644 index 0000000..d24696b --- /dev/null +++ b/kernel/docs/capability/objects.md @@ -0,0 +1,73 @@ +# Ядерные объекты: `object.rs` + +## Концептуальная модель + +`KernelObject` — это ref-counted представление объекта ядра. +В отличие от `Capability` (которая указывает на объект), `KernelObject` +— это сам объект с подсчётом ссылок. + +## Структура + +```rust +pub struct KernelObject { + pub phys_addr: PhysAddr, // физический адрес объекта + pub size_bits: u8, // размер в битах (2^size_bits) + pub obj_type: ObjectType, // тип объекта + pub ref_count: AtomicUsize, // счётчик ссылок + pub owner_id: u64, // ID владельца +} +``` + +## ObjectType — классификация объектов + +```rust +pub enum ObjectType { + Untyped, // Сырая память без типа + Frame, // Фрейм (4KiB страница) + CNode, // Capability Node + ThreadBlock, // Блок управления потоком (TCB) + PageTable, // Таблица страниц +} +``` + +## Ref-counting + +```rust +impl KernelObject { + pub fn add_ref(&self) { + self.ref_count.fetch_add(1, Ordering::Relaxed); + // ^ Relaxed: нас не волнует порядок других операций + // при увеличении счётчика. Только атомарность. + } + + pub fn release(&self) -> bool { + self.ref_count.fetch_sub(1, Ordering::Release) == 1 + // ^ Release: все операции до release видны тому, + // кто Acquire-читает ref_count. + // Возвращает true, если это была последняя ссылка + // (объект должен быть уничтожен). + } +} +``` + +## Текущее состояние + +`KernelObject` определён, но пока не используется активно. +Capability-система в текущей версии работает напрямую с `CapObject` +и `PhysAddr`, без обёртки в `KernelObject`. + +Планируемое использование: +- При выделении памяти через PMActor: создаётся KernelObject. +- Capability ссылается на KernelObject через ID/индекс. +- Когда последняя Strong capability удалена → KernelObject + уничтожается → память возвращается. + +## Отличие CapObject vs KernelObject + +| Характеристика | CapObject | KernelObject | +|---------------|-----------|--------------| +| Роль | Что capability представляет | Сам объект в памяти ядра | +| Ref-count | Нет | AtomicUsize | +| Хранение | В CNode slot | В отдельной таблице | +| Типы | Memory, CNode, PMActor | Untyped, Frame, CNode, ThreadBlock, PageTable | +| Связь | CapObject.Memory.phys = KernelObject.phys_addr | — | diff --git a/kernel/docs/console.md b/kernel/docs/console.md deleted file mode 100644 index 06c4ec6..0000000 --- a/kernel/docs/console.md +++ /dev/null @@ -1,132 +0,0 @@ -# Консоль и отладка - ---- - -## TTY — PSF2 терминал - -**Файл**: `src/tty.rs` - -Шрифтовой терминал на основе PSF2 (PC Screen Font v2). Это **реально используемая консоль** в `kmain`. - -### Psf2Header — `#[repr(C, packed)]` - -```rust -struct Psf2Header { - magic: u32, // магическое число PSF2 - version: u32, - header_size: u32, - flags: u32, - num_glyphs: u32, // количество глифов - bytes_per_glyph: u32, - height: u32, // высота шрифта в пикселях - width: u32, // ширина шрифта в пикселях -} -``` - -### Console - -```rust -pub struct Console<'a> { - framebuffer: &'a Framebuffer<'a>, - font: &'static [u8], // сырые данные PSF2 - x: usize, // курсор X (в пикселях) - y: usize, // курсор Y (в пикселях) - fg_color: u32, // цвет текста (0xFFFFFF = белый) - bg_color: u32, // цвет фона (0x000000 = чёрный) -} -``` - -### Методы - -| Метод | Описание | -|---|---| -| `new(framebuffer, font)` | Создаёт консоль, курсор в (0,0) | -| `set_color(fg)` | Устанавливает цвет текста | -| `clear()` | Зануляет фреймбуфер, сбрасывает курсор | -| `header()` | Возвращает ссылку на Psf2Header | -| `scroll()` | Сдвигает фреймбуфер на высоту шрифта, зануляет низ | -| `draw_glyph(glyph_index, x, y)` | Рисует глиф: сканирует bitmap шрифта, пишет `fg_color` для установленных битов | -| `write_char(c)` | Обрабатывает `\n`, word wrap, поиск глифа (0 для отсутствующих), скролл | -| `write_str(s)` | Реализация `fmt::Write` — итерация по символам | - -`draw_glyph` — ключевой метод: строки шрифта — это побитовое представление, каждый бит — один пиксель. Если бит установлен → пишется `fg_color`, иначе пропускается. - ---- - -## Serial — последовательный порт - -**Файл**: `src/debug/serial.rs` - -Драйвер UART 16550 на COM1. - -### SerialPort - -```rust -pub struct SerialPort(pub u16); // номер порта -``` - -### Инициализация UART (`init()`) - -```rust -port + 1 = 0x00 // отключение прерываний -port + 3 = 0x80 // DLAB = 1 (доступ к делителю) -port + 0 = 0x03 // делитель младший байт (~38400 бод) -port + 1 = 0x00 // делитель старший байт -port + 3 = 0x03 // 8N1: 8 бит, no parity, 1 stop bit -port + 2 = 0xC7 // FIFO enable, clear, 14-byte threshold -port + 4 = 0x0B // DTR + RTS (data terminal ready) -``` - -### Методы - -| Метод | Описание | -|---|---| -| `is_transmit_empty() -> bool` | Проверка бита 5 Line Status Register | -| `send(data: u8)` | Ждёт `is_transmit_empty()`, затем пишет в порт | -| `write_str(s)` (fmt::Write) | Поcимвольная отправка | - -### Функции - -| Функция | Описание | -|---|---| -| `init_global()` | Инициализирует COM1, сохраняет в `SERIAL_PORT` | -| `write_global(args: Arguments)` | Пишет форматированную строку в serial | -| `outb(port, val)` | `unsafe`: инструкция `out` (write I/O port) | -| `inb(port) -> u8` | `unsafe`: инструкция `in` (read I/O port) | - ---- - -## Логирование - -**Файл**: `src/debug.rs` - -### LogLevel - -```rust -pub enum LogLevel { - Info, // зелёный: ANSI \x1b[32m, RGB 0x00FF00 - Warn, // жёлтый: ANSI \x1b[33m, RGB 0xFFFF00 - Error, // красный: ANSI \x1b[31m, RGB 0xFF0000 -} -``` - -### Макросы - -#### `log!(console, level, module, $($arg)*)` - -Формат: `[ LOG ] | \n` - -Двойной вывод: -- **Console**: цветной `[ LOG ]` (по LogLevel) + белый ` | ` -- **Serial**: ANSI-цветной `[ LOG ]` + ` | ` + ANSI reset - -#### `info!(console, module, ...)` -Обёртка над `log!` с `LogLevel::Info`. Зелёный `[ LOG ]`. - -#### `warn!(console, module, ...)` -Обёртка над `log!` с `LogLevel::Warn`. Жёлтый `[ LOG ]`. - -#### `error!(console, module, ...)` -Обёртка над `log!` с `LogLevel::Error`. Красный `[ LOG ]`. - -Все макросы экспортируются (`#[macro_export]`) и доступны из любого модуля. diff --git a/kernel/docs/cpu/idt.md b/kernel/docs/cpu/idt.md new file mode 100644 index 0000000..d486ec2 --- /dev/null +++ b/kernel/docs/cpu/idt.md @@ -0,0 +1,115 @@ +# Interrupt Descriptor Table: `idt.rs` + +## Аппаратная модель + +IDT (Interrupt Descriptor Table) — это таблица из 256 entry (по 16 байт каждая), +которая сообщает CPU, куда передавать управление при прерываниях и исключениях. + +``` +IDT: +┌──────┬──────────────────────────────────────────────────────┐ +│ 0 │ #DE — Divide Error │ +│ 1 │ #DB — Debug │ +│ 2 │ #NMI — Non-Maskable Interrupt │ +│ 3 │ #BP — Breakpoint │ +│ 4 │ #OF — Overflow │ +│ 5 │ #BR — Bound Range Exceeded │ +│ 6 │ #UD — Undefined Opcode │ +│ 7 │ #NM — Device Not Available │ +│ 8 │ #DF — Double Fault │ +│ 9 │ #MF — Coprocessor Segment Overrun │ +│ 10 │ #TS — Invalid TSS │ +│ 11 │ #NP — Segment Not Present │ +│ 12 │ #SS — Stack-Segment Fault │ +│ 13 │ #GP — General Protection Fault │ +│ 14 │ #PF — Page Fault │ +│ 15-31│ Reserved / CPU exceptions │ +│ 32-255│ User-defined (hardware interrupts) │ +└──────┴──────────────────────────────────────────────────────┘ +``` + +Загрузка IDT: инструкция `lidt [idtr_ptr]`, где `idtr_ptr` — это +6-байтовая структура `IdtPtr`: +``` +IdtPtr: +┌──────────┬──────────┐ +│ limit:16 │ base:48 │ +└──────────┴──────────┘ +``` + +## IdtEntry — 16-байтовая запись + +```rust +#[repr(C, packed)] +pub struct IdtEntry { + offset_low: u16, // Бит 0:15 адреса обработчика + selector: u16, // Селектор сегмента кода (0x28 для ядра) + ist: u8, // Interrupt Stack Table + type_attr: u8, // Тип вентиля + флаги + offset_mid: u16, // Бит 16:31 адреса обработчика + offset_high: u32, // Бит 32:63 адреса обработчика + ignore: u32, // Зарезервировано +} +``` + +**type_attr:** +- Бит 7: Present (должен быть 1) +- Бит 6-5: DPL (Descriptor Privilege Level) +- Бит 4: Reserved (0) +- Бит 3-0: Gate Type (0xE = Interrupt Gate, 0xF = Trap Gate) + +`set_handler(handler, selector, flags)`: +```rust +pub fn set_handler(&mut self, handler: u64, selector: u16, flags: u8) { + self.offset_low = handler as u16; + self.selector = selector; + self.ist = 0; + self.type_attr = flags | 0x80; // Present bit forced on + self.offset_mid = (handler >> 16) as u16; + self.offset_high = (handler >> 32) as u32; +} +``` + +## InterruptDescriptorTable — 256 entry + +```rust +pub struct InterruptDescriptorTable { + entries: [IdtEntry; 256], // 256 × 16 = 4096 байт +} +``` + +**Методы:** +- `set_handler(vector, handler)` — устанавливает обработчик с + селектором 0x28 (GDT code segment) и type_attr 0x8E + (Interrupt Gate, Ring 0, Present). +- `load()` — `lidt` инструкция. + +```rust +pub unsafe fn load(&'static self) { + let ptr = IdtPtr { + limit: (size_of::() - 1) as u16, // 4095 + base: self as *const _ as u64, + }; + asm!("lidt [{}]", in(reg) &ptr); +} +``` + +## Глобальная IDT + +```rust +pub static mut IDT: InterruptDescriptorTable = InterruptDescriptorTable::new(); +``` + +`static mut` — потому что IDT модифицируется в ранней инициализации, +до включения прерываний. Потенциально может быть заменён на `static` +с `UnsafeCell`. + +## Детали конфигурации + +- **Selector**: `0x28` — это GDT entry для ring 0 code segment + (дескриптор 5, 5 × 8 = 0x28). Селектор сегмента кода в long mode. +- **Type 0x8E**: `1000_1110` = бит 7 (Present) + бит 3:0 = 1110 + (Interrupt Gate, 32-bit). В 64-bit режиме все вентили — 64-bit, + флаг 0xE остаётся корректным. +- **IST**: 0 — не используем Interrupt Stack Table (один стек для + всех обработчиков). diff --git a/kernel/docs/cpu/interrupts.md b/kernel/docs/cpu/interrupts.md new file mode 100644 index 0000000..9d500d8 --- /dev/null +++ b/kernel/docs/cpu/interrupts.md @@ -0,0 +1,200 @@ +# Обработчики прерываний и исключений: `interrupts.rs` + +## Концептуальная модель + +Файл объединяет **обработчики исключений** (таблицу IDT) и +**межпроцессорные прерывания** (TLB shootdown). Это связующий слой +между аппаратурой (CPU exceptions, APIC) и программными подсистемами +(VMM, Memory). + +## Структура обработчика (stub + Rust handler) + +Каждый обработчик состоит из двух частей: + +1. **Сборочный stub** (global_asm): сохраняет контекст, вызывает + Rust-функцию, восстанавливает контекст, iretq. +2. **Rust handler**: собственно обработка. + +``` +Пример: Page Fault + [stack] +page_fault_stub: error_code ← CPU пушет + push rax ...регистры... + push rcx + ... + mov rdi, [rsp + 15*8] ← error_code как аргумент + call rust_page_fault_handler ← вызов Rust + pop r15 восстановление + ... + pop rax + add rsp, 8 ← убираем error_code + iretq ← возврат +``` + +## Exception stubs (макрос) + +```rust +macro_rules! exception_stub { + ($name:ident, $handler:ident) => { + concat!( + ".global ", stringify!($name), "\n", + stringify!($name), ":\n", + "push rax\npush rcx\n...push r15\n", // сохранение + "mov rdi, [rsp + 15*8]\n", // error_code + "call ", stringify!($handler), "\n", + "pop r15\n...pop rax\n", // восстановление + "add rsp, 8\n", // очистка error_code + "iretq\n", + ) + }; +} +``` + +Используется для: +- `page_fault_stub` → `rust_page_fault_handler` (вектор 14) +- `gpf_stub` → `rust_gpf_handler` (вектор 13) +- `double_fault_stub` → `rust_double_fault_handler` (вектор 8) + +## TLB shootdown stub + +```rust +global_asm!( + "tlb_shootdown_stub:", + "push rax\npush rcx\n...", // сохранение + "call rust_tlb_shootdown_handler", + "pop r15\n...pop rax\n", // восстановление + "iretq" +); +``` + +## Early handlers — для начальной загрузки + +До того как VMM и slab allocator готовы, ядро не может обрабатывать +сложные исключения. Для векторов 0-31 генерируются ранние заглушки. + +### Генерация (макрос `.altmacro`) + +```asm +.macro early_stub vec + .globl early_handler_\vec + .balign 16 + early_handler_\vec: + push 0 /* dummy error code (если нет аппаратного) */ + push \vec /* номер вектора */ + jmp early_common +.endm +``` + +`early_stub 0`..`early_stub 31` генерирует 32 обработчика. + +### `early_common` + +```asm +early_common: + push rax\npush rcx\n... // сохранение всех GP-регистров + mov rdi, [rsp + 15*8] // vector number + mov rsi, [rsp + 16*8] // error code (или dummy 0) + call rust_early_exception_handler + // never returns +``` + +### `rust_early_exception_handler(vector, error_code)` + +```rust +pub extern "C" fn rust_early_exception_handler(vector: u64, _error_code: u64) -> ! { + serial::write_global(format_args!( + "\n!!! EARLY EXCEPTION !!! vector={} error_code={:#x}\nCPU halted.\n", + vector, _error_code + )); + loop { asm!("cli; hlt"); } +} +``` + +Всегда HALT — раннее исключение фатально. + +## init_early_exceptions() — настройка IDT для ранней загрузки + +```rust +pub fn init_early_exceptions() { + let idt = addr_of_mut!(IDT); + // Устанавливаем early_handler_N для векторов 0-31 + for (v, &handler) in early_handlers.iter().enumerate() { + (*idt).set_handler(v as u8, handler); + } + // Переопределяем критические: + (*idt).set_handler(8, double_fault_stub); // #DF + (*idt).set_handler(13, gpf_stub); // #GPF + (*idt).set_handler(14, page_fault_stub); // #PF + (*idt).set_handler(TLB_SHOOTDOWN_VECTOR, tlb_shootdown_stub); + + let ptr: &'static IDT = &*addr_of!(IDT); + ptr.load(); // lidt +} +``` + +## init_idt() — перезагрузка после полной инициализации + +```rust +pub fn init_idt() { + // Переустанавливаем только Page Fault и TLB Shootdown + // (их обработчики уже переключились на VMM-aware версии) + let idt = addr_of_mut!(IDT); + (*idt).set_handler(14, page_fault_stub); + (*idt).set_handler(TLB_SHOOTDOWN_VECTOR, tlb_shootdown_stub); + let ptr: &'static IDT = &*addr_of!(IDT); + ptr.load(); +} +``` + +## Обработчики исключений + +### `rust_page_fault_handler(error_code)` + +```rust +pub extern "C" fn rust_page_fault_handler(error_code: u64) { + let fault_addr: u64; + asm!("mov {}, cr2", out(reg) fault_addr); // читаем CR2 + + let write = (error_code & 0x2) != 0; // fault на запись? + let present = (error_code & 0x1) != 0; // PTE был PRESENT? + + let mut vmm_guard = KERNEL_SPACE.lock(); + if let Some(space) = vmm_guard.as_mut() { + space.process_pending_revocations(); // обрабатываем отзывы + match space.handle_fault(fault_addr, write) { + Ok(_) => {} // обработано: iretq retry + Err(e) => panic!(...), // необработанный fault + } + } else { + panic!("Page fault before KERNEL_SPACE!"); + } +} +``` + +### `rust_gpf_handler(error_code)` и `rust_double_fault_handler(error_code)` + +Оба — HALT с сообщением. + +## TLB Shootdown обработчик + +```rust +pub extern "C" fn rust_tlb_shootdown_handler() { + crate::mem::vmm::handle_tlb_shootdown_ipi(); + crate::cpu::lapic::send_eoi(); // подтверждаем LAPIC прерывание +} +``` + +### Тайминги и безопасность + +- TLB shootdown — IPI, требует минимальной задержки. +- Все операции в `handle_tlb_shootdown_ipi` — простые и быстрые. +- LAPIC EOI отправляется сразу после локального TLB flush. + +## Константы + +```rust +pub const TLB_SHOOTDOWN_VECTOR: u8 = 0xFD; +``` + +Вектор 0xFD (253) — в диапазоне пользовательских прерываний (32-255), +намеренно далеко от системных векторов 0-31. diff --git a/kernel/docs/cpu/introduction.md b/kernel/docs/cpu/introduction.md new file mode 100644 index 0000000..5072b8c --- /dev/null +++ b/kernel/docs/cpu/introduction.md @@ -0,0 +1,106 @@ +# CPU подсистема: концептуальная модель + +## Состав и ответственность + +CPU подсистема отвечает за: + +1. **Обработку прерываний и исключений** — IDT, обработчики. +2. **Исключения ранней загрузки** — пока ядро ещё не полностью инициализировано. +3. **TLB Shootdown** — межпроцессорное прерывание для синхронизации TLB. +4. **Local APIC** — программируемый контроллер прерываний. + +``` +CPU Subsystem +┌─────────────────────────────────────────────────────────┐ +│ CPU Core #0 │ +│ │ +│ ┌──────────────┐ ┌─────────────────────────┐ │ +│ │ LAPIC │ │ IDT │ │ +│ │ │ │ │ │ +│ │ ICR ────────┼────────► 0: #DE (Divide Error) │ │ +│ │ EOI │ │ 1: #DB (Debug) │ │ +│ │ (timer) │ │ ... │ │ +│ │ │ │ 8: #DF (Double Fault) │ │ +│ │ │ │ 13: #GP (GPF) │ │ +│ │ │ │ 14: #PF (Page Fault) │ │ +│ │ │ │ ... │ │ +│ │ │ │ 0xFD: TLB Shootdown │ │ +│ └──────────────┘ └─────────────────────────┘ │ +└─────────────────────────────────────────────────────────┘ +``` + +## Модули + +| Файл | Компонент | Функция | +|------|-----------|---------| +| `idt.rs` | IDT структуры | Определение IdtEntry, IdtPtr, InterruptDescriptorTable | +| `interrupts.rs` | Обработчики | Early handlers, Page Fault, Double Fault, GPF, TLB shootdown | +| `lapic.rs` | Local APIC | Инициализация LAPIC, EOI, broadcast IPI | + +## Порядок инициализации + +``` +kmain() + │ + ├── cpu::interrupts::init_early_exceptions() + │ └── IDT для векторов 0-31 + TLB shootdown (0xFD) + │ + ├── ... (PMM, LAPIC, Page tables) ... + │ + ├── cpu::interrupts::init_idt() + │ └── Перезагрузка IDT с полными обработчиками + │ + ├── STI (разрешение прерываний) + │ + └── ... (работа с прерываниями) +``` + +## Обработка исключений: два этапа + +### Этап 1: Early (до VMM) + +На раннем этапе загрузки (до настройки page tables и heap) ядро +не может обрабатывать сложные исключения. Для векторов 0-31 +устанавливаются `early_handler_N`, которые: + +1. Пушат вектор и (возможно) dummy error code. +2. Переходят в `early_common`. +3. Сохраняют все регистры. +4. Вызывают `rust_early_exception_handler(vector, error_code)`. +5. Паникуют (HALT). + +### Этап 2: Полный (после VMM) + +После инициализации VMM три критических исключения получают +полноценные обработчики: + +- **Page Fault (#PF, вектор 14)**: попытка обработать (COW, lazy). +- **Double Fault (#DF, вектор 8)**: HALT с сообщением. +- **General Protection Fault (#GPF, вектор 13)**: HALT с сообщением. +- **TLB Shootdown (вектор 0xFD)**: межпроцессорный TLB сброс. + +## TLB Shootdown: модель + +При изменении page tables на одном ядре, TLB других ядер устаревает. +Протокол: + +``` +CPU 0 (инициатор) CPU 1 (мишень) + │ │ + ├── local_tlb_flush_asid() │ + ├── SHOOTDOWN_LOCK.lock() │ + ├── SHOOTDOWN_ASID = asid │ + ├── SHOOTDOWN_ACK = 0 │ + ├── LAPIC: broadcast IPI │ + │ (вектор TLB_SHOOTDOWN_VECTOR) ───► прерывание! + │ ├── handle_tlb_shootdown_ipi() + │ ├── local_tlb_flush_asid() + │ ├── SHOOTDOWN_ACK |= 1 << core + │ ├── LAPIC::send_eoi() + │ └── iretq + │ │ + ├── spin_loop() ◄────────────────────┤ + │ (ждёт ACK от всех ядер) │ + ├── SHOOTDOWN_LOCK.unlock() │ + └── continue │ +``` diff --git a/kernel/docs/cpu/lapic.md b/kernel/docs/cpu/lapic.md new file mode 100644 index 0000000..82ff79d --- /dev/null +++ b/kernel/docs/cpu/lapic.md @@ -0,0 +1,115 @@ +# Local APIC: `lapic.rs` + +## Аппаратная модель + +**Local APIC** (Advanced Programmable Interrupt Controller) — это +встроенный в каждое ядро x86-64 контроллер прерываний. + +``` +Local APIC (MMIO, начиная с 0xFEE00_000) +┌──────────────────────────────┐ +│ 0x020: IRR (In-Service Reg) │ +│ 0x030: TMR (Trigger Mode) │ +│ 0x080: EOI │ ← запись 0 = подтверждение прерывания +│ 0x0B0: LINT0/LINT1 │ +│ 0x0E0: Timer │ +│ 0x200: LVT Error │ +│ 0x300: ICR (Interrupt Cmd) │ ← отправка межпроцессорного прерывания +│ 0x310: ICR_HIGH (APIC ID) │ +└──────────────────────────────┘ +``` + +## Регистры + +| Смещение | Регистр | Назначение | +|----------|---------|------------| +| 0x0B0 | EOI | End Of Interrupt — подтверждение обработки | +| 0x300 | ICR (low) | Interrupt Command Register — отправка IPI | +| 0x310 | ICR (high) | Destination APIC ID | + +## Инициализация + +```rust +static LAPIC_VIRT_BASE: AtomicU64 = AtomicU64::new(0); + +pub fn init() { + // LAPIC отображён bootloader'ом в HHDM по адресу 0xFEE00_000 + LAPIC_VIRT_BASE.store(LAPIC_DEFAULT_BASE + get_hhdm(), Ordering::SeqCst); +} +``` + +LAPIC расположен по физическому адресу `0xFEE0_0000`. Ядро получает +виртуальный базовый адрес, добавляя HHDM offset. + +## Доступ к регистрам + +```rust +#[inline(always)] +fn write_lapic_reg(offset: u64, value: u32) { + let base = LAPIC_VIRT_BASE.load(Ordering::Relaxed); + if base == 0 { return; } // LAPIC ещё не инициализирован + unsafe { core::ptr::write_volatile((base + offset) as *mut u32, value) } +} +``` + +- `write_volatile` — запрещает компилятору оптимизировать обращение + (регистры MMIO). +- `Relaxed` ordering — для отладки/инициализации достаточно. + +## Операции + +### send_eoi() — конец прерывания + +```rust +pub fn send_eoi() { + write_lapic_reg(LAPIC_EOI, 0); // 0x0B0 +} +``` + +### broadcast_ipi_exclude_self(vector) — IPI всем, кроме себя + +```rust +pub fn broadcast_ipi_exclude_self(vector: u8) { + // ICR[19:18] = 10b (All Excluding Self) + // ICR[14] = 1 (Assert) + // ICR[7:0] = vector + let icr_low = (2 << 18) | (1 << 14) | (vector as u32); + write_lapic_reg(LAPIC_ICR_LOW, icr_low); +} +``` + +Используется для TLB shootdown — необходимо разослать всем ядрам +(кроме текущего) IPI с вектором `TLB_SHOOTDOWN_VECTOR`. + +### current_core_id() — определение текущего ядра + +```rust +pub fn current_core_id() -> u32 { + // CPUID leaf 1: EBX[31:24] = Local APIC ID + let mut ebx: u32; + asm!( + "mov {tmp:r}, rbx", // спрятать rbx (резерв LLVM) + "mov eax, 1", "cpuid", + "mov {out:e}, ebx", // сохранить EBX + "mov rbx, {tmp:r}", // восстановить rbx + tmp = out(reg) _, + out = out(reg) ebx, + ... + ); + ebx >> 24 +} +``` + +**RBX проблема:** LLVM резервирует RBX, поэтому его нужно сохранять +и восстанавливать вручную вокруг CPUID инструкции. + +## Тонкости + +1. **MMIO vs MSR**: LAPIC можно программировать через MSR (IA32_APIC_BASE) + и через MMIO. Bootloader (Limine) настраивает MMIO mapping в HHDM. +2. **Инициализация**: LAPIC уже включён bootloader'ом. `init()` просто + сохраняет виртуальный адрес. +3. **x2APIC**: не используется (в текущей версии — MMIO xAPIC). +4. **EOI**: обязателен после каждого прерывания от LAPIC (включая IPI). +5. **ICR запись**: после записи в ICR Low, шина APIC доставляет + прерывание. Запись блокирующая (ждёт готовности шины). diff --git a/kernel/docs/debug/introduction.md b/kernel/docs/debug/introduction.md new file mode 100644 index 0000000..5ef4992 --- /dev/null +++ b/kernel/docs/debug/introduction.md @@ -0,0 +1,88 @@ +# Подсистема отладки: концептуальная модель + +## Два канала вывода + +Ядро имеет два параллельных канала для отладки: + +1. **Экранный (framebuffer console)** — через `tty::Console`. + - Использует PSF2-шрифты. + - Цветной вывод (зелёный/жёлтый/красный для Info/Warn/Error). + - Медленнее, но визуально нагляднее. + +2. **Serial port (COM1)** — через `debug::serial`. + - Текстовый вывод с ANSI escape codes. + - Работает через QEMU/KVM serial console. + - Быстрее, может быть перенаправлен в файл. + +## LogLevel — уровни логирования + +```rust +pub enum LogLevel { + Info, // Зелёный на экране, зелёный в serial + Warn, // Жёлтый + Error, // Красный +} +``` + +Каждый уровень имеет: +- `serial_color_code()` — ANSI escape code для serial. +- `console_color()` — RGB значение для framebuffer. + +## Макросы + +```rust +// Основной макрос +log!(console, level, module, format_args...) + +// Специализированные +info!(console, module, format_args...) +warn!(console, module, format_args...) +error!(console, module, format_args...) +``` + +**Формат вывода на экран:** +``` +[ LOG ] | +``` + +**Формат в serial:** +``` +GREEN[ LOG] RESET | +``` + +## Цветовое кодирование + +| Уровень | Экран (RGB) | Serial (ANSI) | +|---------|-------------|---------------| +| Info | 0x00FF00 | `\x1b[32m` (green) | +| Warn | 0xFFFF00 | `\x1b[33m` (yellow) | +| Error | 0xFF0000 | `\x1b[31m` (red) | +| Текст | 0xFFFFFF (white) | `\x1b[0m` (reset) | + +## Использование в kmain() + +```rust +// После инициализации serial +debug::serial::init_global(); + +// После инициализации console +info!(console, "BOOT", "LIS4 Kernel Starting..."); +info!(console, "MEM", "BitmapPMM initialized."); +info!(console, "LAPIC", "Local APIC initialized."); +``` + +## Архитектура + +``` +┌──────────────┐ ┌───────────────────┐ +│ kmain() │────►│ log!() macro │ +└──────────────┘ └────────┬──────────┘ + │ + ┌──────────────┼──────────────┐ + ▼ ▼ ▼ + ┌──────────┐ ┌──────────┐ ┌──────────┐ + │ Экран │ │ Serial │ │ Паника │ + │ Console │ │ COM1 │ │ Handler │ + │ (tty.rs) │ │(serial.rs)│ │(main.rs) │ + └──────────┘ └──────────┘ └──────────┘ +``` diff --git a/kernel/docs/debug/serial.md b/kernel/docs/debug/serial.md new file mode 100644 index 0000000..c0d2da7 --- /dev/null +++ b/kernel/docs/debug/serial.md @@ -0,0 +1,110 @@ +# Serial Port драйвер: `serial.rs` + +## Назначение + +Драйвер последовательного порта (UART 16550, COM1) для отладочного +вывода. Позволяет видеть сообщения ядра через QEMU serial console, +minicom, screen и т.д. + +## Аппаратная модель + +COM1 расположен по портам ввода-вывода `0x3F8`-`0x3FF`: + +| Порт | Регистр | Назначение | +|------|---------|------------| +| 0x3F8 | DATA | Чтение/запись данных | +| 0x3F9 | IER | Interrupt Enable | +| 0x3FA | IIR/FCR | Interrupt ID / FIFO Control | +| 0x3FB | LCR | Line Control | +| 0x3FC | MCR | Modem Control | +| 0x3FD | LSR | Line Status | +| 0x3FE | MSR | Modem Status | + +## Инициализация + +```rust +pub unsafe fn init() -> Self { + let port = Self::COM1; // 0x3F8 + outb(port + 1, 0x00); // IER = 0 (disable interrupts) + outb(port + 3, 0x80); // LCR DLAB=1 (enable baud rate programming) + outb(port + 0, 0x03); // Divisor LSB = 3 (38400 baud) + outb(port + 1, 0x00); // Divisor MSB = 0 + outb(port + 3, 0x03); // LCR = 8N1 (8 bits, No parity, 1 stop) + outb(port + 2, 0xC7); // FCR = enable FIFO, clear, 14-byte threshold + outb(port + 4, 0x0B); // MCR = DTR+RTS+OUT2 (enable IRQ + handshake) + SerialPort(port) +} +``` + +### Детали конфигурации + +1. **IER = 0**: отключаем прерывания UART (TODO: включить для RX). +2. **DLAB = 1**: разрешаем программирование делителя бода. +3. **Divisor = 3**: при тактовой 1.8432 MHz → 115200 / 3 = 38400 бод. +4. **LCR = 0x03**: 8N1 — 8 бит данных, нет чётности, 1 стоп-бит. +5. **FCR = 0xC7**: enable FIFO, clear both FIFOs, trigger at 14 bytes. +6. **MCR = 0x0B**: DTR=1, RTS=1, OUT2=1 (необходимо для IRQ на ISA шине). + +## Отправка байта + +```rust +fn is_transmit_empty(&self) -> bool { + unsafe { (inb(self.0 + 5) & 0x20) != 0 } // LSR bit 5 = Transmitter Holding Register Empty +} + +pub fn send(&self, data: u8) { + while !self.is_transmit_empty() {} // Ждём, пока UART готов + unsafe { outb(self.0, data); } +} +``` + +## fmt::Write реализация + +```rust +impl core::fmt::Write for SerialPort { + fn write_str(&mut self, s: &str) -> core::fmt::Result { + for byte in s.bytes() { self.send(byte); } + Ok(()) + } +} +``` + +## Глобальный экземпляр + +```rust +static SERIAL_PORT: Locked> = Locked::new(None); + +pub fn init_global() { + let mut guard = SERIAL_PORT.lock(); + *guard = Some(unsafe { SerialPort::init() }); +} + +pub fn write_global(args: core::fmt::Arguments) { + let mut guard = SERIAL_PORT.lock(); + if let Some(ref mut sp) = *guard { + let _ = sp.write_fmt(args); + } +} +``` + +`init_global()` вызывается из `kmain()` после инициализации TTY. +`write_global()` используется: +- В `log!()` макросе (через debug.rs). +- В `rust_panic()` обработчике. +- В обработчиках исключений (GPF, Double Fault, Early Exception). + +## Низкоуровневый I/O + +```rust +unsafe fn outb(port: u16, val: u8) { + asm!("out dx, al", in("dx") port, in("al") val, + options(nomem, nostack, preserves_flags)); +} + +unsafe fn inb(port: u16) -> u8 { + let res: u8; + asm!("in al, dx", out("al") res, in("dx") port, + options(nomem, nostack, preserves_flags)); + res +} +``` diff --git a/kernel/docs/events.md b/kernel/docs/events.md deleted file mode 100644 index 581cb79..0000000 --- a/kernel/docs/events.md +++ /dev/null @@ -1,85 +0,0 @@ -# Очередь отзыва (RevocationQueue) - -**Файл**: `src/events.rs` - -Lock-free MPSC (Multi-Producer Single-Consumer) очередь для уведомления VMM об отзыве capability. - ---- - -## RevocationQueue - -```rust -pub struct RevocationQueue { - buffer: [AtomicU64; QUEUE_SIZE], // 1024 слота - _pad0: [u8; 64], // cache-line padding - head: AtomicUsize, // индекс потребителя (VMM) - _pad1: [u8; 64], // ещё padding - tail: AtomicUsize, // индекс производителя (CNode) -} -``` - -Cache-line padding (64 байта) предотвращает **false sharing** между head и tail на разных кэш-линиях. - -### Константы - -- `QUEUE_SIZE = 1024` -- `QUEUE_MASK = 1023` - -### `new() -> Self` - -Все слоты = 0, head = 0, tail = 0. - -### `push(token_sig: u64) -> Result<(), &'static str>` - -1. Читает `tail` (Relaxed) -2. CAS-цикл: пытается захватить следующий слот -3. Если буфер полон (`tail - head == QUEUE_SIZE`): ошибка -4. Записывает `token_sig` в `buffer[tail & QUEUE_MASK]` (Release ordering) -5. Обновляет `tail` (Release) - -**Производители**: CNode::revoke() — при каждом отзыве пушит token_sig. - -### `pop() -> Option` - -1. Читает `tail` (Acquire) и `head` (Relaxed) -2. Если `head == tail`: буфер пуст, None -3. Читает `buffer[head & QUEUE_MASK]` (Acquire) -4. Обновляет `head` (Release) -5. Возвращает token - -**Потребитель**: VMM — `AddressSpace::process_pending_revocations()`. - ---- - -## Глобальный экземпляр - -```rust -pub static MMU_REVOCATION_QUEUE: RevocationQueue = RevocationQueue::new(); -``` - ---- - -## Полный цикл ревокации - -``` -CNode::revoke(slot_idx) - → revoke_internal(slot_idx) // рекурсивное уничтожение потомков - → slot = Empty - → MMU_REVOCATION_QUEUE.push(token_sig) - -... позже, при page fault ... - -rust_page_fault_handler() - → process_deferred_mmu_events() - → KERNEL_SPACE.lock() - → process_pending_revocations() - → while let Some(token) = MMU_REVOCATION_QUEUE.pop() - → revoke_by_token(token) - → unmap matching VMAs (кроме PINNED) - → TLB flush - - → KERNEL_SPACE.lock() - → handle_fault(fault_addr, write) // обработка исходного fault -``` - -**Почему такая очерёдность?** Ревокации обрабатываются **до** захвата VMM для page fault, чтобы избежать deadlock'а: и revoke, и handle_fault хотят lock на `KERNEL_SPACE`. diff --git a/kernel/docs/events/introduction.md b/kernel/docs/events/introduction.md new file mode 100644 index 0000000..6bc7787 --- /dev/null +++ b/kernel/docs/events/introduction.md @@ -0,0 +1,128 @@ +# Очередь отзыва RevocationQueue: `events.rs` + +## Концептуальная модель + +**RevocationQueue** — это глобальная lock-free кольцевая очередь, +которая соединяет capability-систему (CNode) с виртуальной памятью (VMM). + +### Назначение + +Когда capability отзывается, VMM должна аннулировать все VMA, +связанные с этим capability. Но: +- CNode.revoke() не может напрямую обратиться к VMM (разные подсистемы, + разное время жизни блокировок). +- VMM может быть в обработке page fault'а. +- Отзыв может быть каскадным и рекурсивным. + +Решение: **асинхронная очередь**. + +``` +CNode.revoke(token_1) + │ + └──→ push(token_1) ───→ MMU_REVOCATION_QUEUE ──→ pop() ──→ VMM + │ +CNode.revoke(token_2) │ + │ │ + └──→ push(token_2) ─────────────────────────────────────────┤ + │ +Page Fault handler │ + │ │ + └──→ process_pending_revocations() ←────────────────────────┘ +``` + +## RevocationQueue — детали + +```rust +pub struct RevocationQueue { + buffer: [AtomicU64; QUEUE_SIZE], // 1024 entry + _pad0: [u8; 64], // padding: кэш-линия + head: AtomicUsize, // читатель (VMM) + _pad1: [u8; 64], // padding: кэш-линия + tail: AtomicUsize, // писатели (CNode revoke) +} +``` + +Тот же паттерн, что и в `PMActorQueue`: разделение head и tail +по разным кэш-линиям предотвращает false sharing. + +### push(token_sig) + +```rust +pub fn push(&self, token_sig: u64) -> Result<(), &'static str> { + 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("Revocation queue overflow"); + } + match self.tail.compare_exchange_weak(tail, tail+1, AcqRel, Relaxed) { + Ok(_) => { + self.buffer[tail & QUEUE_MASK].store(token_sig, Release); + return Ok(()); + } + Err(actual) => tail = actual, + } + } +} +``` + +### pop() + +```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 token = self.buffer[head & QUEUE_MASK].load(Ordering::Acquire); + self.head.store(head.wrapping_add(1), Ordering::Release); + Some(token) +} +``` + +## Глобальный экземпляр + +```rust +pub static MMU_REVOCATION_QUEUE: RevocationQueue = RevocationQueue::new(); +``` + +## VMM: обработка отзывов + +```rust +// В interrupts.rs: rust_page_fault_handler() +let mut vmm_guard = KERNEL_SPACE.lock(); +if let Some(space) = vmm_guard.as_mut() { + space.process_pending_revocations(); // ← дренирует очередь + match space.handle_fault(fault_addr, write) { ... } +} +``` + +```rust +// В vmm.rs: AddressSpace +pub fn process_pending_revocations(&mut self) { + let mut needs_flush = false; + while let Some(token) = MMU_REVOCATION_QUEUE.pop() { + self.do_revoke_by_token(token); + needs_flush = true; + } + if needs_flush { + tlb_flush_asid(self.asid); + } +} +``` + +## Почему именно так? + +1. **Lock-free**: отзыв capability не блокируется VMM. +2. **Batch processing**: VMM обрабатывает сразу все накопленные отзывы + при следующем page fault, экономя TLB flush'ы. +3. **Deadlock prevention**: process_pending_revocations вызывается + **до** handle_fault, избегая циклических зависимостей между + блокировками capability и VMM. +4. **Простота**: кольцевой буфер с CAS — проверенная временем структура. + +## Ограничения + +- Размер: 1024 entry. Если очередь переполняется — **паника**. + Это проектный выбор: переполнение означает, что VMM не успевает + обрабатывать отзывы быстрее, чем CNode генерирует новые. +- Однопоточный pop (только page fault handler дренирует очередь). diff --git a/kernel/docs/glossary/terms.md b/kernel/docs/glossary/terms.md new file mode 100644 index 0000000..dab4afa --- /dev/null +++ b/kernel/docs/glossary/terms.md @@ -0,0 +1,1055 @@ +# Глоссарий: термины операционных систем и ядра Elyz + +> Эта статья — **минимальная база** для понимания всей документации ядра. +> Если вы не знаете, что такое MMU, TLB или Page Fault — начните здесь. +> Термины сгруппированы по темам: от фундаментальных к специфическим. + +--- + +## 1. Фундаментальные концепции + +### Процессор и память: базовая модель + +Представьте, что процессор (CPU) — это человек, который выполняет инструкции +из блокнота (оперативная память, RAM). У него есть две проблемы: + +1. **Скорость**: RAM медленная. Процессор в 100+ раз быстрее. +2. **Изоляция**: программы не должны видеть память друг друга. + +Для решения этих проблем существуют MMU и TLB. + +--- + +### MMU (Memory Management Unit) + +**Что это?** MMU — это аппаратный блок внутри процессора, который +автоматически преобразует **виртуальные адреса** (которыми оперирует +программа) в **физические адреса** (реальная ячейка RAM). + +**Зачем?** Без MMU каждая программа работала бы с реальными физическими +адресами. Программа А может случайно прочитать данные программы Б. +Одна программа может испортить данные другой. С MMU каждая программа +думает, что она одна в памяти, и работает со своими «виртуальными» адресами. + +**Как это работает упрощённо:** +``` +Программа: "запиши 42 по адресу 0x1000" + │ + ▼ +MMU: "адрес 0x1000 у этой программы → реальный адрес 0x7F00" + │ + ▼ +RAM: запись по адресу 0x7F00 +``` + +MMU использует **таблицы страниц** (page tables) для этого преобразования. +В x86-64 это 4-уровневая структура: PML4 → PDPT → PD → PT (см. Paging). + +**Где в коде:** `src/mem/paging.rs` — `PageTable`, которая представляет +одну таблицу (512 entry). `walk_to_p1_mut()` проходит все 4 уровня. + +--- + +### TLB (Translation Lookaside Buffer) + +**Что это?** TLB — это крошечный, но очень быстрый кэш внутри процессора, +который запоминает результаты трансляции адресов. Если MMU — это +переводчик, то TLB — его записная книжка. + +**Зачем?** Каждый раз, когда программа читает или пишет память, MMU должен +пройти через 4 уровня таблиц страниц, чтобы перевести виртуальный адрес +в физический. Это **дорого** (4 обращения к RAM вместо 1). TLB запоминает +пары «виртуальный адрес → физический адрес», поэтому для часто +используемых страниц трансляция занимает 1 такт, а не 100+. + +**Проблема:** Когда ядро меняет таблицы страниц (например, отображает +новую страницу), TLB содержит **устаревшие** записи. Их нужно сбросить +(flush). Для этого есть инструкции: +- `INVLPG [addr]` — сбросить одну запись для addr. +- Запись в CR3 — сбросить **весь** TLB. +- `INVPCID` — сбросить TLB для конкретного PCID (более тонко). + +**Где в коде:** `src/mem/paging.rs` — `INVLPG` после map/unmap. +`src/mem/vmm.rs` — `tlb_flush_asid()`, `local_tlb_flush_asid()`, +`tlb_flush_all()`, `handle_tlb_shootdown_ipi()`. + +--- + +### Page Fault (исключение #PF, вектор 14) + +**Что это?** Это аппаратное исключение, которое происходит, когда +процессор не может преобразовать виртуальный адрес через MMU. +Причины: страница не отображена, нарушение прав, Copy-on-Write и т.д. + +**Как работает:** +1. Программа пытается обратиться к адресу. +2. MMU проверяет таблицы — записи нет (или нет прав). +3. MMU сохраняет адрес сбоя в регистр **CR2**. +4. Процессор пушит error code и номер вектора (14) на стек. +5. Процессор переходит в обработчик (через IDT). +6. Обработчик решает: выделить страницу? COW? Или убить процесс? + +**Зачем?** Page Fault — это **инструмент**, а не только ошибка: +- **Demand paging**: не выделяем страницу, пока она не понадобится. + Первый доступ → Page Fault → аллоцируем. Экономит память. +- **Copy-on-Write**: при fork страницы разделяются. При записи → fault → + копируем. Экономит время и память. +- **Lazy mapping**: регион зарезервирован (VMA есть), страниц нет. + Fault → alloc + map. + +**Error code (что в нём):** +``` +Bit 0 (P): 0 = страница не присутствует, 1 = защита (была, но нельзя) +Bit 1 (W): 0 = чтение, 1 = запись +Bit 2 (U): 0 = supervisor (ядро), 1 = user +Bit 3 (RSVD): нарушение зарезервированных битов +Bit 4 (ID): 0 = обычный fault, 1 = instruction fetch +``` + +**Где в коде:** `src/cpu/interrupts.rs` — `rust_page_fault_handler()`. +Читает CR2, проверяет error code, вызывает VMM. + +--- + +### CR2 (Control Register 2) + +**Что это?** Регистр процессора x86-64, который автоматически заполняется +**адресом, вызвавшим Page Fault**. При каждом #PF процессор сохраняет +виртуальный адрес, к которому пыталась обратиться программа. + +**Зачем?** Обработчик Page Fault должен знать, какой адрес вызвал fault, +чтобы выделить правильную страницу. + +**Как читается в коде:** +```rust +asm!("mov {}, cr2", out(reg) fault_addr); +``` +`src/cpu/interrupts.rs:265-266` + +> **Примечание:** Только чтение. Запись в CR2 возможна, но не нужна. +> CR2 — это read-only диагностический регистр. + +--- + +### STI (Set Interrupt Flag) / CLI (Clear Interrupt Flag) + +**Что это?** Инструкции процессора x86-64, которые управляют флагом +прерываний `IF` в регистре RFLAGS. + +- `STI` (STart Interrupts) — **разрешает** маскируемые прерывания. + Процессор начинает реагировать на внешние прерывания (таймер, клавиатура). +- `CLI` (CLear Interrupts) — **запрещает** маскируемые прерывания. + Процессор игнорирует внешние прерывания. + +**Зачем в ядре?** +- На этапе инициализации ядро работает с `CLI` (без прерываний), чтобы + никто не помешал последовательности настройки. +- Когда всё готово — `STI`, и ядро начинает обрабатывать прерывания. + +**Где в коде:** `src/main.rs:150` — `asm!("sti")` после инициализации +IDT, VMM, PM Router. `hcf()` — `asm!("cli")`. + +--- + +### HCF (Halt and Catch Fire) — остановка CPU + +**Что это?** Состояние, в котором процессор перестаёт выполнять +программу, ожидая внешнего прерывания. Реализуется инструкцией `HLT`. + +**Зачем?** +- Когда ядру больше нечего делать (нет процессов). +- При панике (panic): `CLI + HLT` — процессор остановлен навсегда. +- Ожидание в цикле: вместо busy-wait (сжигает 100% CPU) используем HLT. + +**Где в коде:** +```rust +fn hcf() -> ! { + unsafe { asm!("cli"); } // запретить прерывания + loop { + unsafe { asm!("hlt"); } // остановить CPU + } +} +``` +`src/main.rs:253-257` + +--- + +## 2. Процессор и прерывания + +### CPU Subsystem (подсистема процессора) + +**Что это?** Совокупность кода ядра, который управляет процессором: +- IDT — таблица обработчиков прерываний. +- Interrupt Handlers — сами обработчики. +- LAPIC — контроллер прерываний. +- Системные регистры (CR0, CR2, CR3, CR4). + +**Зачем?** Процессор — это не просто «вычислитель». Он генерирует +события (исключения, прерывания), и ядро должно на них реагировать. +Подсистема CPU — это прослойка между аппаратурой и остальным ядром. + +**Где в коде:** `src/cpu/` — модули `idt.rs`, `interrupts.rs`, `lapic.rs`. + +--- + +### IDT (Interrupt Descriptor Table) + +**Что это?** Таблица в памяти, которая говорит процессору: +«Когда случится событие с номером N, перейди по адресу A_N». +Аналог — оглавление книги: «Если вам нужна глава 14, откройте стр. 100». + +**Размер:** 256 entry × 16 байт = 4096 байт (ровно 1 страница). + +**Каждая entry содержит:** +- Адрес обработчика (64 бита, разбит на 3 части из-за совместимости). +- Селектор сегмента кода (0x28 для ring 0 ядра). +- Тип вентиля: Interrupt Gate (0xE) или Trap Gate (0xF). + Разница: Interrupt Gate автоматически запрещает прерывания (CLI) при входе. + +**Загрузка:** инструкция `LIDT` (Load IDT) с указателем на `IdtPtr` +(6 байт: 2 — размер, 4 — адрес). + +**Где в коде:** `src/cpu/idt.rs` — `IdtEntry`, `InterruptDescriptorTable`, +`IdtPtr`. `src/cpu/interrupts.rs` — глобальная `static mut IDT`. + +--- + +### Interrupt Handlers (обработчики прерываний) + +**Что это?** Функции, которые вызываются процессором при прерываниях +и исключениях. Каждый обработчик — это ассемблерный «мостик» (stub), +который сохраняет контекст, вызывает Rust-функцию и возвращается. + +**Анатомия обработчика:** +```asm +; Ассемблерный stub +vector_14_handler: + push rax ; сохранить регистры (контекст) + push rcx + push rdx + ... + mov rdi, [rsp+15*8] ; error code как аргумент 1 + call rust_handler ; вызов Rust функции + add rsp, 8 ; очистить error code + pop ... ; восстановить регистры + iretq ; возврат в прерванный код +``` + +**Почему два этапа (asm + Rust)?** +- Rust не умеет напрямую работать с сохранением/восстановлением регистров. +- Соглашение C (и Rust) требует определённого состояния стека. +- Ассемблер — единственный способ правильно войти и выйти. + +**Типы обработчиков в Elyz:** +- **Early** (вектора 0-31): HALT с сообщением — при ранних сбоях до VMM. +- **Page Fault** (14): полноценный — COW, lazy, или паника. +- **Double Fault** (8): всегда паника. +- **GPF** (13): всегда паника. +- **TLB Shootdown** (0xFD): межпроцессорный TLB flush. + +**Где в коде:** `src/cpu/interrupts.rs` — `exception_stub!()` макрос, +`early_stub` макрос, `rust_page_fault_handler`, `rust_gpf_handler`, +`rust_double_fault_handler`. + +--- + +### LAPIC (Local Advanced Programmable Interrupt Controller) + +**Что это?** Встроенный в каждое ядро процессора контроллер прерываний. +Он: +- Принимает прерывания от устройств (через системный контроллер APIC). +- Отправляет межпроцессорные прерывания (IPI). +- Имеет таймер (не используется в текущей версии). +- Обрабатывает EOI (End Of Interrupt). + +**Зачем?** Без LAPIC каждое прерывание шло бы на единственный CPU. +С LAPIC можно распределять прерывания между ядрами. + +**MMIO:** LAPIC программируется через Memory-Mapped I/O по физическому +адресу `0xFEE0_0000` + HHDM offset. Обращение через `write_volatile`. + +**Регистры:** +- `0x0B0` — EOI: запись 0 подтверждает завершение обработки. +- `0x300` — ICR (Interrupt Command Register): для отправки IPI. +- `0x020` — IRR (In-Service Register): какие прерывания обрабатываются. + +**Где в коде:** `src/cpu/lapic.rs`. + +--- + +### IPI (Inter-Processor Interrupt) + +**Что это?** Прерывание, которое одно ядро отправляет другому (или всем). + +**Зачем?** Используется для **TLB shootdown**: когда одно ядро меняет +page tables, оно должно сообщить другим ядрам, чтобы они сбросили TLB. + +**Как работает в Elyz:** +1. Ядро A хочет flush TLB для ASID 5. +2. Ядро A пишет в LAPIC ICR: вектор TLB_SHOOTDOWN (0xFD), + destination = All Excluding Self. +3. LAPIC отправляет IPI всем ядрам, кроме A. +4. Каждое ядро B получает прерывание → вызывает + `rust_tlb_shootdown_handler()` → `local_tlb_flush_asid(5)`. +5. Каждое ядро B пишет ACK и отправляет EOI. +6. Ядро A ждёт ACK от всех ядер, затем продолжает. + +**Где в коде:** `src/cpu/lapic.rs` — `broadcast_ipi_exclude_self()`. +`src/mem/vmm.rs` — `tlb_flush_asid()`, `handle_tlb_shootdown_ipi()`. + +--- + +## 3. Управление памятью: фундамент + +### Физическая vs Виртуальная память + +**Физическая память** — это реальные микросхемы RAM. У неё есть адреса +начиная с 0 и до максимума (например, 16 GiB). + +**Виртуальная память** — это иллюзия, которую создаёт MMU для каждой +программы. Программа видит «свой» адрес с 0 до 2^48 (256 TiB), но +за каждым виртуальным адресом стоит какой-то физический. + +``` +Виртуальные адреса программы A: + 0x0000 ─────────────────► физический 0x1000 (через MMU) + 0x1000 ─────────────────► физический 0x5000 + 0xFFFF_8000_0000_0000 ─► физический 0x0000 (HHDM — отображение ядра) + +Виртуальные адреса программы B: + 0x0000 ─────────────────► физический 0x3000 (другой!) + 0x1000 ─────────────────► физический 0x7000 +``` + +**Преимущества виртуальной памяти:** +1. **Изоляция**: программа A не видит программу B. +2. **Безопасность**: ядро живёт в higher half, пользователь не может + к нему обратиться (из-за флага USER/SUPERVISOR в PTE). +3. **Эффективность**: можно отобразить только часть программы, остальное + подкачивать по требованию (demand paging). +4. **Упрощение**: каждая программа думает, что у неё вся память с 0. + +--- + +### Paging / 4-уровневые таблицы (Page Tables) + +**Что это?** Структура данных, которую использует MMU для трансляции +адресов. В x86-64 это 4 уровня: + +``` +Виртуальный адрес (64 бита, используются 48): +┌─────────┬─────────┬─────────┬─────────┬─────────┐ +│ PML4 │ PDPT │ PD │ PT │ offset │ +│ 9 бит │ 9 бит │ 9 бит │ 9 бит │ 12 бит │ +└────┬────┴────┬────┴────┬────┴────┬────┴─────────┘ + │ │ │ │ + ▼ ▼ ▼ ▼ + PML4 ──► PDPT ──► PD ──► PT ──► Фрейм (4 KiB) + (P4) (P3) (P2) (P1) +``` + +- **Каждая таблица**: 512 entry × 8 байт = 4096 байт (1 страница). +- **9 бит на уровень**: 512 вариантов, 2^9 = 512. +- **12 бит offset**: 4096 байт внутри страницы. +- **Итого**: 48 бит = 256 TiB адресуемого пространства. + +**Entry (PTE, Page Table Entry):** +``` +Bit 63: NX (No Exec) — защита от исполнения +Bits 51:12: Физический адрес следующей таблицы (или фрейма) +Bits 11:0: Флаги (PRESENT, WRITABLE, USER, ACCESSED, DIRTY...) +``` + +**Huge Pages:** +- 2 MiB: если P2 entry имеет PS (Page Size) = 1. +- 1 GiB: если P3 entry имеет PS = 1. +- Используются для отображения больших областей (HHDM). + +**Где в коде:** `src/mem/paging.rs` — `PageTable`, `PageTableFlags`, +`walk_to_p1_mut()`, `translate()`, `map_page()`, `unmap_page()`. + +--- + +### PML4 (Page Map Level 4) + +**Что это?** Самый верхний (корневой) уровень таблиц страниц. +Физический адрес PML4 хранится в регистре **CR3**. + +**Зачем?** Каждое адресное пространство имеет свой PML4. Когда +процессор переключается между задачами, он загружает новый CR3, +и MMU начинает транслировать адреса по-новому. + +**PCID (Process Context Identifier):** В младшие 12 бит CR3 можно +записать номер контекста (ASID). Это позволяет TLB хранить записи +для нескольких процессов одновременно, не сбрасывая TLB при каждом +переключении. Elyz использует PCID для kernel (ASID=0) и процессов +(ASID 1-4094). + +**Где в коде:** `src/mem/vmm.rs:708-716` — `AddressSpace::activate()`. + +--- + +### HHDM (Higher-Half Direct Map) + +**Что это?** Область виртуальной памяти (обычно начинается с +`0xFFFF_8000_0000_0000`), где **вся физическая память отображена 1:1 +с фиксированным смещением**. + +**Формула:** `virt_addr = phys_addr + HHDM_OFFSET` + +**Зачем?** Когда ядру нужно прочитать или записать физический фрейм +(например, обнулить страницу, прочитать Page Table), оно не может +использовать физический адрес напрямую — процессор работает только +с виртуальными адресами. HHDM даёт простой способ получить доступ +к любому физическому адресу: просто добавь HHDM_OFFSET. + +**Где используется:** +- `PhysAddr::to_virt()` → `VirtAddr(self.0 + get_hhdm())` +- `PageTable` доступ: `PhysAddr(pte & PTE_ADDR_MASK).to_virt().as_mut_ptr()` +- `BitmapPMM`: обнуление фреймов. +- `BuddyAllocator`: intrusive list (next/prev на странице). + +**Где в коде:** `src/mem/address.rs` — `HHDM_OFFSET`, `init_hhdm()`, +`get_hhdm()`, `PhysAddr::to_virt()`. + +--- + +### Address Space (адресное пространство) + +**Что это?** Полное описание всех виртуальных адресов, доступных +программе (или ядру). Включает: +- **PML4** (CR3) — аппаратная структура для трансляции. +- **VMA list** — список регионов (программное описание, что где отображено). + +**В Elyz:** +```rust +pub struct AddressSpace { + pub asid: u16, // PCID для TLB + pub pml4_phys: PhysAddr, // адрес корневой таблицы в CR3 + regions: Vec, // отсортированный список VMA +} +``` + +**Зачем?** Переключение между процессами = переключение между +AddressSpace → загрузка нового CR3 + TLB flush. + +**Где в коде:** `src/mem/vmm.rs` — `AddressSpace` (методы: `new()`, +`from_active()`, `map_region()`, `unmap_region()`, `handle_fault()`, +`clone_for_fork()`, `activate()`, `revoke_by_token()`). + +--- + +### VMA (Virtual Memory Area) и VMA-деревья + +**Что это?** Регион непрерывного виртуального адресного пространства +с едиными правами доступа. Аналог — «отрезок» памяти. + +**Пример:** программа просит 1 MiB памяти. Ядро создаёт VMA: +``` +VmaRegion { + virt_start: 0x7F00_0000, + virt_end: 0x7F10_0000, // 1 MiB позже + flags: READ | WRITE | USER, + cap_token: 0x1234, + backing: Anonymous(Vec>), +} +``` + +**Типы VMA (backing):** +- **Anonymous**: own-фреймы, выделенные из PMM. При unmap → free. +- **Physical**: непрерывный физический диапазон (например, framebuffer). +- **Shared**: zero-copy mapping чужих фреймов (capability-based). + +**VMA-деревья:** VMA хранятся в сортированном списке (Vec), по +virt_start. Поиск: binary search (`partition_point`). В будущем +может быть заменено на дерево для O(log N) вставки/удаления. + +**Где в коде:** `src/mem/vmm.rs` — `VmaRegion`, `VmaBacking`. +Управление: `find_idx()`, `insert_sorted()`, `map_region()`. + +--- + +### KERNEL SPACE (пространство ядра) + +**Что это?** Глобальное адресное пространство ядра. В Elyz: +```rust +pub static KERNEL_SPACE: Locked> = Locked::new(None); +``` + +**Особенности:** +- Использует ASID = 0 (PCID 0, kernel). +- Живёт постоянно (не освобождается). +- Page Fault handler работает в контексте KERNEL_SPACE. +- Содержит: + - HHDM (вся физическая память отображена). + - Код ядра (текст). + - Кучу (heap, slab allocator). + - Page tables ядра. + +**Зачем отдельно?** Чтобы ядро могло обрабатывать Page Fault'ы, +оно должно знать, какие VMA у его собственного AddressSpace. +Kernel space — это AddressSpace ядра для себя. + +**Где в коде:** `src/mem/vmm.rs:809`. Инициализация `init_kernel_space()`. + +--- + +## 4. Менеджеры физической памяти + +### PMM (Physical Memory Manager) + +**Что это?** Компонент ядра, который отслеживает, какие физические +фреймы (страницы по 4 KiB) свободны, а какие заняты. + +**Основные операции:** +- `alloc_frame()` — «дай мне один свободный фрейм». +- `free_frame(addr)` — «этот фрейм больше не нужен». +- `alloc_contiguous(n)` — «дай n последовательных фреймов» (для DMA). + +**Зачем?** Без PMM каждое выделение памяти было бы вручную. PMM +автоматизирует и централизует этот процесс. + +--- + +### BitmapPMM (Bitmap Physical Memory Manager) + +**Что это?** Реализация PMM через битовую карту (bitmap). Каждый бит +в bitmap представляет один фрейм (4 KiB): +- `1` = фрейм занят. +- `0` = фрейм свободен. + +**Сколько нужно памяти для bitmap:** +Если у нас 16 GiB RAM → 16 GiB / 4 KiB = 4 млн фреймов. +4 млн бит = 512 KiB для bitmat. + +**Трёхуровневое ускорение:** + +``` +L1 (уровень 1): массив u64 + Каждый бит = одно слово из L0 + 1 = в этом слове есть хотя бы один свободный фрейм + +L0 (уровень 0): массив u8 (основной bitmap) + Каждый байт = 8 фреймов, каждый бит = 1 фрейм + +Ref-counts: массив u16 + Счётчик ссылок на каждый фрейм (для COW: несколько + виртуальных отображений могут ссылаться на один фрейм) +``` + +**Аллокация (alloc_frame):** +1. Ищем L1-слово с ненулевым битом (есть свободные фреймы). +2. Вычисляем, какое L0-слово соответствует этому биту. +3. Если L0-слово == !0 (все заняты) — очищаем бит L1, ищем дальше. +4. `(!word).trailing_zeros()` — находим свободный бит в L0-слове. +5. Возвращаем физический адрес: `page_idx * 4096`. + +**Почему не отдельный L0-слово?** L1 позволяет пропускать целые +группы по 64 слова (4096 фреймов), где нет ни одного свободного. + +**Ref-counts:** +- `free_frame()` уменьшает счётчик. Если стал 0 — очищает бит. +- `inc_ref_frame()` увеличивает счётчик (для COW). +- `lock_frame()` — принудительное занятие (для метаданных PMM). + +**Где в коде:** `src/mem/pmm.rs` — `BitmapPMM`. + +--- + +### PM Actor (Physical Memory Actor) + +**Что это?** Распределённый менеджер памяти. В отличие от глобального +BitmapPMM, у каждого PMActor есть свой **диапазон** физической памяти, +которым он управляет самостоятельно. + +**Зачем?** +1. **Изоляция**: актор A не может истощить память актора B. +2. **Масштабирование**: каждый актор может работать на своём ядре. +3. **Предсказуемость**: каждый актор знает свой лимит. +4. **Модель акторов**: запросы приходят асинхронно через очередь. + +**Как работает:** +1. PMActor владеет диапазоном: `managed_range = (start_phys, end_phys)`. +2. Внутри использует `BuddyAllocator` для своего диапазона. +3. Получает запросы через lock-free MPSC очередь (PMActorQueue). +4. Обрабатывает (`process_messages()`): Alloc, Free, Carve. +5. Ответы отправляет через PMRouter. + +**Где в коде:** `src/mem/pm_manages.rs` — `PMActor`. + +--- + +### PM Router (Physical Memory Router) + +**Что это?** Центральный коммутатор для асинхронной маршрутизации +ответов от PMActor'ов к запросившим потокам. + +**Как работает:** +- 65536 предварительно выделенных **каналов**. +- `alloc_channel()`: CAS на стеке свободных каналов (lock-free). +- `route_responses()`: запись результата в канал. +- `wait_for_response()`: HLT-ожидание + чтение результата. + +| Операция | Что происходит | +|----------|---------------| +| Выделить канал | `alloc_channel()` → получаем u16 ID | +| Отправить запрос | `actor.submit_request(req)` с channel_id | +| Актёр обрабатывает | `process_messages()` → buddy.alloc() | +| Отправить ответ | `router.route_responses([response])` | +| Получить ответ | `router.wait_for_response(ch)` | + +**Где в коде:** `src/mem/pm_router.rs` — `PMRouter`, `Channel`. + +--- + +### Buddy Allocator (аллокатор пар) + +**Что это?** Алгоритм управления памятью, который делит память на +блоки размером `2^order` страниц. Название от английского «buddy» +(товарищ, партнёр) — потому что блоки объединяются со своим +«напарником» (buddy) при освобождении. + +**Как работает:** + +``` +Изначально: один блок 16 страниц (order 4) + +alloc(1 страница, order 0): + 16 → [8][8] → [4][4][8] → [2][2][4][8] → [1][1][2][4][8] + Выдан порядок 0, блок в начале. + +free(первый блок, order 0): + [свободный][свободный][2][4][8] + → объединяем buddy → [2][2][4][8] → [4][4][8] → [8][8] → [16] + Восстановлен полный блок order 4. +``` + +**Формула buddy:** `buddy_idx = block_idx XOR (1 << order)` + +**Почему эффективно?** +1. **Нет фрагментации**: блоки объединяются обратно. +2. **Быстро**: alloc и free — O(log N) в худшем, O(1) в среднем. +3. **Intrusive list**: next/prev хранятся прямо на страницах. + +**Intrusive List в Buddy:** + +Вместо отдельного массива указателей, свободные блоки хранят +указатели next/prev прямо на своей физической странице: + +``` +Физическая страница 0x4000 (свободна): +┌──────────────┬──────────────┬────────────────────┐ +│ next = 0x5000│ prev = NULL │ (остаток страницы) │ +└──────────────┴──────────────┴────────────────────┘ +``` + +Это требует только HHDM-доступа к странице, никаких дополнительных +аллокаций для структур данных. + +**Где в коде:** `src/mem/buddy.rs` — `BuddyAllocator`. + +--- + +### Intrusive Linked List + +**Что это?** Связный список, где указатели next/prev хранятся **внутри** +самих элементов, а не в отдельной структуре. + +**Сравнение:** + +``` +Обычный список: + Node { data, next, prev } — Node выделяется отдельно + list: {... Node A ... Node B ...} — узлы в куче + +Интрузивный список: + Page { ... next, prev, ...} — поле next/prev прямо на странице + list: [Page A] ↔ [Page B] ↔ [Page C] — страницы сами себе узлы +``` + +**Преимущество:** никаких дополнительных аллокаций. Список — это +сами управляемые объекты (страницы). + +**Использование в Elyz:** +- `BuddyAllocator`: free_heads[order] — intrusive list свободных блоков. +- `flist_push`, `flist_remove`, `flist_pop` управляют списком. + +**Где в коде:** `src/mem/buddy.rs:45-82`. + +--- + +### Slab Allocator (плитный аллокатор) + +**Что это?** Аллокатор для **мелких объектов** (8 — 2048 байт) — +куча ядра. Работает так: + +1. Есть фиксированные размеры блоков: 8, 16, 32, 64, 128, 256, 512, 1024, 2048. +2. Для каждого размера — список свободных блоков (slab list). +3. Аллокация: берём блок из списка. +4. Деаллокация: возвращаем блок в список. + +**Пример:** `alloc(37 байт)`: +- `list_index(37)` → размер 64 (ближайший ≥ 37). +- Если есть блок 64 в list_heads — отдаём. +- Если нет — берём новый блок (bump alloc) размером 64. + +**Для больших блоков (> 2048):** используется отдельный freelist. +Если freelist пуст — bump alloc (последовательная раздача из heap). + +**Bump alloc:** просто двигаем указатель `next_bump` вперёд. +Быстро, но не переиспользует память (только slab возвращает). + +**Отличие от Buddy Allocator:** +| Характеристика | Slab | Buddy | +|---------------|------|-------| +| Размер блоков | Фиксированный (8-2048) | Степени двойки (1,2,4,8… страниц) | +| Для чего | Ядерная куча (Vec, Box) | Физическая память (PMActor) | +| Где память | Виртуальная (heap 0xFFFF_9000...) | Физическая | +| Переиспользование | Да (slab list) | Да (coalesce) | + +**Где в коде:** `src/mem/allocator.rs` — `SlabAllocator`, `ALLOCATOR` (global_allocator). + +--- + +### Heap Init (инициализация кучи) + +**Что это?** Выделение и настройка области памяти для кучи ядра. + +**В Elyz:** +```rust +let heap_start = 0xFFFF_9000_0000_0000; // виртуальный адрес +let heap_size = 8 * 1024 * 1024; // 8 MiB +// Сначала map'им все страницы кучи (alloc_frame для каждой) +for i in (0..heap_size).step_by(4096) { + let frame = pmm::alloc_frame().expect("OOM"); + p4.map_page(VirtAddr(heap_start + i), frame, flags); +} +// Затем инициализируем аллокатор +allocator::ALLOCATOR.lock().init(heap_start, heap_size); +``` + +**Почему в 2 шага?** +1. Сначала подготавливаем page tables: куча должна быть отображена + физически, иначе при первом `alloc` будет Page Fault. +2. Затем говорим SlabAllocator'у: «вот твоя область, раздавай». + +--- + +## 5. Виртуальная память: продвинутые концепции + +### VMM (Virtual Memory Manager) + +**Что это?** Компонент ядра, который управляет виртуальными адресными +пространствами (AddressSpace). Его работа: +- Создание/уничтожение AddressSpace. +- Отображение (map) и удаление (unmap) регионов. +- Обработка Page Fault (COW, lazy). +- Fork с Copy-on-Write. +- TLB shootdown при SMP. + +**Где в коде:** `src/mem/vmm.rs` — `AddressSpace`, `VmaRegion`, `VmaBacking`. + +--- + +### Demand Paging (ленивое отображение, lazy mapping) + +**Что это?** Стратегия, при которой физическая страница выделяется +только когда программа действительно к ней обращается, а не когда +она запрашивает память. + +**Как работает:** +1. Программа: `malloc(1 MiB)` или ядро: `map_region(LAZY)`. +2. Ядро создаёт VMA с флагом LAZY. +3. VMA есть, но Page Table entries = NOT PRESENT. +4. При первом чтении/записи → **Page Fault**. +5. Обработчик: alloc_frame → zero → map_page → return. +6. Программа продолжает, даже не зная, что был fault. + +**Зачем?** +- **Экономия памяти**: программа может зарезервировать 1 GiB, + но использовать 1 MiB. Остальное не занимает физическую память. +- **Скорость запуска**: не нужно allocating всё сразу. + +**Где в коде:** `src/mem/vmm.rs` — `VmaFlags::LAZY`, `handle_fault()` +проверяет LAZY и выделяет страницу. + +--- + +### Copy-on-Write (COW, копирование при записи) + +**Что это?** Техника, при которой ресурс (страница) разделяется между +несколькими потребителями, пока один из них не попытается изменить +(записать) его. Тогда делается копия. + +**Как работает (на примере fork):** +1. Родитель и потомок имеют одинаковые страницы. +2. Вместо копирования всех страниц (дорого!), ядро отображает + **те же** фреймы в AddressSpace потомка. +3. Все PTE помечаются флагом **COW** и WRITABLE снимается. +4. Когда кто-то пишет → Page Fault. +5. Обработчик: alloc_frame → copy(old → new) → map(new) → free(old). +6. Каждый теперь имеет свою копию. + +**Зачем?** +- **fork() становится O(1)**: не нужно копировать всё адресное + пространство. Только page tables. +- **Экономия**: если потомок только читает (или exec'ится сразу), + копирования не происходит вообще. + +**COW-bit:** Elyz использует бит 9 PTE как кастомный флаг COW +(аппаратура его игнорирует). + +**Где в коде:** `src/mem/vmm.rs`: +- `clone_for_fork()`: устанавливает COW на родительские страницы. +- `handle_fault()`: при write + COW → copy. +- `src/mem/paging.rs` — `PageTableFlags::COW`. + +--- + +### INVPCID (Invalidate PCID) + +**Что это?** Инструкция x86-64 для выборочного сброса TLB по PCID +(Process Context Identifier). Более тонкая, чем полная перезапись CR3. + +**Типы INVPCID:** +- **Тип 0**: сброс одной страницы в конкретном контексте. +- **Тип 1**: сброс всех записей для одного контекста (используется в Elyz). +- **Тип 2**: сброс всех записей для всех контекстов. +- **Тип 3**: сброс всех записей для всех контекстов, кроме текущего. + +**Зачем?** На старых CPU (до Broadwell) INVPCID может отсутствовать. +Elyz проверяет CPUID.07H:EBX[10] и падает на CR3 reload если нет. + +**Где в коде:** `src/mem/vmm.rs` — `INVPCID_SUPPORTED`, `init_cpu_features()`, +`local_tlb_flush_asid()`. + +--- + +### Lazy Region (ленивый регион) + +**Что это?** VMA с флагом LAZY. Страницы не выделены заранее, +аллоцируются по требованию при Page Fault. + +**Отличие от обычного региона:** +- Обычный: alloc сразу выделяет все фреймы (eager). +- Ленивый: alloc создаёт Vec. Фреймы при fault. + +**Где используется:** большие выделения памяти, стеки потоков, +mapped файлы. + +--- + +## 6. Система Capability + +### Capability (возможность, дескриптор доступа) + +**Что это?** Неподделываемый токен, дающий право выполнить операцию +над объектом. Аналог: ключ от номера в гостинице. + +**В традиционных ОС (Linux):** +- У вас есть UID (0 = root, 1000 = user). +- Система проверяет: «есть ли у user 1000 права на этот файл?» +- Это называется **Access Control List (ACL)**. + +**В capability-системе:** +- У вас есть capability (структура в памяти ядра). +- Capability говорит: «я даю право ЧИТАТЬ эту страницу». +- Если у вас нет capability — у вас нет доступа. Точка. + +**Преимущества:** +- **Принцип минимальных привилегий**: вы можете передать только READ, + без WRITE. С ACL это сложнее. +- **Иерархия**: из capability можно сделать «дочерний» с урезанными правами. +- **Отзыв**: capability можно отозвать (revoke), включая всех «детей». + +**Capability в Elyz:** +```rust +pub struct Capability { + pub object: CapObject, // Memory / CNode / PMActor + pub rights: CapRights, // READ / WRITE / EXECUTE / GRANT + pub relation: Relation, // Strong / Borrow / Transfer + pub token_sig: u64, // уникальный ID +} +``` + +**Где в коде:** `src/cap/descriptor.rs`. + +--- + +### Mint (создание capability-потомка) + +**Что это?** Операция создания дочернего capability с урезанными правами. + +**Аналогия:** у вас есть ключ-карта от номера (можете открыть дверь, +минибар, сейф). Вы делаете копию, которая открывает только дверь. + +**Правила:** +- `final_rights = parent.rights & requested_rights` +- Нельзя расширить права (только сузить). +- Нужен флаг GRANT у родителя. + +**Пример:** +```rust +// Есть capability с R|W|X|G на страницу +// Создаём потомка только с R|W +cnode.mint(src=0, dest=10, Relation::Borrow, rights=R|W)?; +``` + +**Где в коде:** `src/cap/mod.rs:40-80`. + +--- + +### CNode (Capability Node) + +**Что это?** Таблица (массив) слотов, каждый слот хранит один capability. +Аналог: таблица открытых файлов в ядре Linux (fd table). + +```rust +pub struct CNode { + pub slots: Vec>, +} +pub struct CNodeSlot { + pub cap: Capability, + pub parent_idx: Option, +} +``` + +**Операции:** +- `insert(slot, cap)` — поместить в слот. +- `mint(src, dest, relation, rights)` — создать потомка. +- `revoke(slot)` — отозвать (каскадно). +- `get_cap(slot)` — прочитать. + +**Отзыв (revoke):** +1. Найти всех потомков (parent_idx == slot). +2. Рекурсивно revoke каждого потомка. +3. Уничтожить сам capability. +4. Отправить token_sig в MMU_REVOCATION_QUEUE. + +**Lock Ranking:** при mint захватываются две блокировки. Чтобы избежать +дедлока — захватываем всегда в порядке возрастания индекса. + +**Где в коде:** `src/cap/mod.rs`. + +--- + +## 7. Конкурентность и синхронизация + +### Lock-free (без блокировок) + +**Что это?** Техника синхронизации, где несколько потоков могут +читать/писать общие данные без мьютексов. Используются атомарные +операции: CAS (Compare-And-Swap), fetch_add, store/load. + +**CAS (Compare-And-Swap) в Rust:** +```rust +// Атомарно: если *addr == old, то *addr = new, вернуть Ok +// Иначе: вернуть Err(actual_value) +match atom.compare_exchange_weak(old, new, Ordering::AcqRel, Ordering::Relaxed) { + Ok(_) => {}, // успешно обновили + Err(actual) => {}, // кто-то другой изменил +} +``` + +**Где используется в Elyz:** +- `PMActorQueue::send()` — несколько продюсеров конкурируют за слот. +- `PM Router::alloc_channel()` — CAS на free_head. +- `RevocationQueue::push()` — несколько CNode могут одновременно пушить. +- `Locked::lock()` — CAS на AtomicBool. + +**Преимущества:** +- Нет блокировок → нет deadlock'ов. +- Нет переключения контекста при ожидании. +- Работает в обработчиках прерываний. + +--- + +### MPSC (Multiple Producer, Single Consumer) + +**Что это?** Паттерн очереди, где **много** потоков могут писать +(producer), но только **один** поток читает (consumer). + +**Где в Elyz:** +- `PMActorQueue`: produce = любой поток/ядро, consume = только PMActor. +- `RevocationQueue`: produce = любой CNode revoke, consume = VMM (page fault). + +**Реализация:** +- Producer: CAS на tail (конкуренция). +- Consumer: просто читает head (без CAS, Single Consumer гарантия). + +**Почему Single Consumer?** +- Проще: не нужна блокировка на чтение. +- Быстрее: pop() без атомики. +- Соответствует модели: один актор потребляет свои сообщения. + +--- + +## 8. Вопрос архитектуры + +### Нужно ли переносить LAPIC и serial в userspace для микроядра? + +**Краткий ответ:** +- **Serial (COM1)**: да, должен быть в userspace. Драйвер UART + не требует привилегированного доступа, только порты I/O. + В микроядре он был бы userspace-процессом, получающим символы + через IPC и отправляющим их в порт. +- **LAPIC**: частично — да, частично — нет. + +**Подробнее про LAPIC:** + +| Функция LAPIC | Микроядро | Почему | +|---------------|-----------|--------| +| EOI (End Of Interrupt) | **Ядро** | EOI — это acknowledging прерывания. Если отдать userspace, процесс может «забыть» отправить EOI, и прерывания зависнут. | +| ICR (IPI отправка) | **Ядро** | IPI может сбить TLB других ядер. Только ядро должно контролировать, когда и кому отправлять IPI. | +| APIC ID | Userspace | Чтение ID — безопасно, полезно для affinity. | +| Timer | Userspace | Таймер LAPIC может быть отдан userspace-процессу (например, драйверу таймера). | +| LVT (остальное) | **Ядро** | LVT настраивает векторы прерываний. Нельзя отдавать userspace. | + +**Вывод: LAPIC не может быть полностью в userspace**, но может быть +split-драйвером: часть в ядре (безопасность), часть в userspace. + +**Текущее состояние Elyz (гибридное/монолитное):** +На данном этапе Elyz не является микроядром. И LAPIC, и serial +находятся в ядре. Это нормально для early стадии. При переходе +к микроядру: +1. Serial → userspace процесс (драйвер UART). +2. LAPIC → split: EOI/ICR в ядре, timer в userspace. + +**Где в коде:** `src/cpu/lapic.rs`, `src/debug/serial.rs`. + +--- + +## 9. Краткий справочник: для чего всё? + +| Термин | Для чего | +|--------|----------| +| **MMU** | Преобразует виртуальные адреса в физические. Даёт изоляцию процессов. | +| **TLB** | Кэш трансляции адресов. Ускоряет работу MMU в ~100x. | +| **Page Fault** | Ловит момент, когда страницы нет в памяти. Используется для demand paging и COW. | +| **IDT** | Таблица, которая говорит CPU, какой код вызвать при исключении. | +| **LAPIC** | Контроллер прерываний. Нужен для IPI (TLB shootdown). | +| **HHDM** | Отображение всей физической памяти в higher half. Упрощает доступ к фреймам. | +| **PMM** | Кто даёт и забирает физические фреймы. | +| **BitmapPMM** | Реализация PMM через битовую карту. Быстрый поиск свободных фреймов. | +| **BuddyAllocator** | Аллокатор физической памяти. Объединяет блоки при освобождении — нет фрагментации. | +| **SlabAllocator** | Аллокатор кучи ядра для мелких объектов. Vec, Box, String — через него. | +| **PMActor** | Распределённый менеджер физической памяти. Изоляция — актор A не ест память B. | +| **PM Router** | Асинхронная доставка результатов от акторов к потокам. | +| **AddressSpace** | Виртуальное адресное пространство процесса. PML4 + VMA. | +| **VMA** | Регион памяти с едиными правами. | +| **COW** | Копирование страницы только при записи. Fork без копирования всего. | +| **Demand paging** | Выделение страницы по требованию, а не заранее. Экономит память. | +| **VMM** | Управляет AddressSpace. Создаёт, удаляет, обрабатывает fault. | +| **INVPCID** | Выборочный сброс TLB для одного контекста. Ускоряет context switch. | +| **TLB shootdown** | Рассылка TLB flush на все ядра при изменении page tables. | +| **Capability** | Неподделываемый токен доступа. «Если нет ключа — нет доступа». | +| **Mint** | Создание дочернего capability с урезанными правами. | +| **Revoke** | Отзыв capability и всех его потомков. | +| **CNode** | Таблица capability. Хранит их в слотах. | +| **Lock-free** | Синхронизация без мьютексов, через CAS. Нет дедлоков. | +| **MPSC** | Очередь: много пишут, один читает. Паттерн для акторов. | +| **Intrusive list** | Список, где next/prev в самих элементах. Не нужно доп. аллокаций. | +| **KERNEL_SPACE** | AddressSpace ядра. Page Fault handler работает в нём. | +| **HCF** | Останов CPU (CLI + HLT). Конечная точка после паники. | +| **STI/CLI** | Включить/выключить прерывания. | +| **CR2** | Регистр с адресом, вызвавшим Page Fault. | diff --git a/kernel/docs/index.md b/kernel/docs/index.md new file mode 100644 index 0000000..df21061 --- /dev/null +++ b/kernel/docs/index.md @@ -0,0 +1,70 @@ +# Документация ядра Elyz (LISA) + +## Структура документации + +``` +kernel/docs/ +├── index.md ← этот файл: оглавление +│ +├── overview/ +│ ├── introduction.md ← Концептуальная модель (7 принципов) +│ └── architecture.md ← Архитектура, схема, порядок инициализации +│ +├── memory/ +│ ├── introduction.md ← Трёхуровневая модель памяти +│ ├── address-translation.md ← PhysAddr, VirtAddr, HHDM +│ ├── physical-memory.md ← BitmapPMM (глобальный) +│ ├── paging.md ← PageTable, 4-level walk +│ ├── buddy.md ← BuddyAllocator (intrusive list) +│ ├── pm-actor.md ← PMActor + MPSC очередь +│ ├── pm-router.md ← PMRouter (65536 каналов) +│ ├── allocator.md ← Slab-аллокатор (куча) +│ └── vmm.md ← AddressSpace, VMA, COW, TLB +│ +├── cpu/ +│ ├── introduction.md ← CPU подсистема, 2 этапа обработки +│ ├── idt.md ← IDT entry, InterruptDescriptorTable +│ ├── interrupts.md ← Обработчики, stubs, early exceptions +│ └── lapic.md ← Local APIC, IPI, EOI +│ +├── capability/ +│ ├── introduction.md ← Capability model, mint, revoke +│ ├── descriptors.md ← Capability, CapObject, CapRights +│ ├── cnode.md ← CNode, lock ranking +│ └── objects.md ← KernelObject (ref-counted) +│ +├── debug/ +│ ├── introduction.md ← Отладка: экран + serial +│ └── serial.md ← COM1 UART 16550 драйвер +│ +├── boot/ +│ ├── introduction.md ← Процесс загрузки, Limine +│ └── linker.md ← Линкер-скрипты +│ +└── events/ + └── introduction.md ← RevocationQueue (lock-free) +``` + +## Быстрый старт по файлам + +| Файл ядра | Что делает | Документация | +|-----------|-----------|--------------| +| `src/main.rs` | Точка входа, инициализация, тесты | `overview/architecture.md` (раздел порядка инициализации) | +| `src/mem/pmm.rs` | Глобальный PMM (Bitmap + L1) | `memory/physical-memory.md` | +| `src/mem/buddy.rs` | Buddy-аллокатор | `memory/buddy.md` | +| `src/mem/paging.rs` | 4-уровневые page tables | `memory/paging.md` | +| `src/mem/pm_manages.rs` | PMActor + очередь | `memory/pm-actor.md` | +| `src/mem/pm_router.rs` | PM Router каналы | `memory/pm-router.md` | +| `src/mem/vmm.rs` | AddressSpace, COW, TLB | `memory/vmm.md` | +| `src/mem/allocator.rs` | Slab-аллокатор | `memory/allocator.md` | +| `src/mem/address.rs` | Физические/виртуальные адреса | `memory/address-translation.md` | +| `src/cpu/idt.rs` | IDT структуры | `cpu/idt.md` | +| `src/cpu/interrupts.rs` | Обработчики исключений | `cpu/interrupts.md` | +| `src/cpu/lapic.rs` | Local APIC | `cpu/lapic.md` | +| `src/cap/mod.rs` | CNode | `capability/cnode.md` | +| `src/cap/descriptor.rs` | Capability типы | `capability/descriptors.md` | +| `src/cap/object.rs` | KernelObject | `capability/objects.md` | +| `src/debug/mod.rs` | Log macros | `debug/introduction.md` | +| `src/debug/serial.rs` | Serial driver | `debug/serial.md` | +| `src/events.rs` | Очередь отзыва | `events/introduction.md` | +| `src/tty.rs` | Framebuffer console | `tty.md` | diff --git a/kernel/docs/interrupts.md b/kernel/docs/interrupts.md deleted file mode 100644 index 78a2156..0000000 --- a/kernel/docs/interrupts.md +++ /dev/null @@ -1,142 +0,0 @@ -# Прерывания - ---- - -## IDT (Interrupt Descriptor Table) - -**Файл**: `src/cpu/idt.rs` - -### IdtEntry — `#[repr(C, packed)]` - -Структура записи IDT (x86-64): - -```rust -struct IdtEntry { - offset_low: u16, // адрес обработчика [15:0] - selector: u16, // селектор кода (0x28 — kernel CS) - ist: u8, // Interrupt Stack Table index - type_attr: u8, // тип шлюза, DPL, Present - offset_mid: u16, // адрес обработчика [31:16] - offset_high: u32, // адрес обработчика [63:32] - ignore: u32, // резерв (0) -} -``` - -- `new()` — все поля нули -- `set_handler(handler: u64, selector: u16, flags: u8)` — устанавливает адрес и флаги. Принудительно ставит Present (0x80) в flags. - -### IdtPtr — `#[repr(C, packed)]` - -```rust -struct IdtPtr { - limit: u16, // размер IDT - 1 - base: u64, // адрес IDT -} -``` - -Загружается инструкцией `lidt`. - -### InterruptDescriptorTable - -```rust -struct InterruptDescriptorTable { - entries: [IdtEntry; 256], // 256 векторов -} -``` - -- `set_handler(vector, handler: u64)` — устанавливает обработчик на вектор. Селектор: `0x28` (kernel code), flags: `0x8E` (32-bit interrupt gate, ring 0, present). -- `load()` — `unsafe fn`: вызывает `lidt` с `&self`. - ---- - -## Обработчики прерываний - -**Файл**: `src/cpu/interrupts.rs` - -### Глобальная IDT - -```rust -pub static mut IDT: InterruptDescriptorTable; -``` - -### Константы - -`TLB_SHOOTDOWN_VECTOR: u8 = 0xFD` - -### Ассемблерные заглушки (global_asm!) - -#### `page_fault_stub` - -```asm -save all registers (push rax, rcx, rdx, rbx, rbp, rsi, rdi, r8-r15) -mov rdi, [rsp + 15*8] ; error code из стека → 1-й аргумент -call rust_page_fault_handler -restore all registers -add rsp, 8 ; убрать error code -iretq -``` - -#### `tlb_shootdown_stub` - -```asm -save all registers -call rust_tlb_shootdown_handler -restore all registers -iretq -``` - -### `init_idt()` - -- Устанавливает вектор 14 → `page_fault_stub` -- Устанавливает вектор `TLB_SHOOTDOWN_VECTOR` → `tlb_shootdown_stub` -- `IDT.load()` - -### `process_deferred_mmu_events()` - -Лочит `KERNEL_SPACE`, вызывает `process_pending_revocations()` — дренаж очереди отзыва. - -### `rust_page_fault_handler(error_code: u64)` - -`#[unsafe(no_mangle)] pub extern "C" fn` - -1. Читает CR2 (адрес fault) -2. Из error_code: бит 1 = write, бит 0 = present -3. **Сначала** `process_deferred_mmu_events()` — обработка ревокаций до захвата VMM (deadlock prevention) -4. Лочит `KERNEL_SPACE`, вызывает `handle_fault(virt_addr, write)` -5. При неудаче: паника с деталями (address, read/write, present, error_code) -6. При успехе: возврат — инструкция будет перезапущена - -### `rust_tlb_shootdown_handler()` - -1. `handle_tlb_shootdown_ipi()` — локальный сброс TLB -2. `send_eoi()` — подтверждение LAPIC - ---- - -## Local APIC - -**Файл**: `src/cpu/lapic.rs` - -Драйвер Local APIC (Advanced Programmable Interrupt Controller). - -### Константы - -```rust -LAPIC_DEFAULT_BASE = 0xFEE0_0000 // стандартный MMIO адрес LAPIC -LAPIC_EOI = 0x0B0 // End-Of-Interrupt регистр -LAPIC_ICR_LOW = 0x300 // Interrupt Command Register (low) -``` - -### `LAPIC_VIRT_BASE: AtomicU64` - -Кэшированный виртуальный адрес LAPIC MMIO (HHDM + default base). - -### Функции - -| Функция | Описание | -|---|---| -| `init(hhdm_offset)` | Вычисляет и сохраняет `LAPIC_VIRT_BASE = 0xFEE0_0000 + hhdm_offset` | -| `current_core_id() -> u32` | CPUID leaf 1, EBX[31:24] — Initial APIC ID | -| `write_lapic_reg(offset, value)` | Volatile write u32 в LAPIC MMIO | -| `send_eoi()` | `write_lapic_reg(LAPIC_EOI, 0)` — подтверждение прерывания | -| `broadcast_ipi_exclude_self(vector)` | IPI всем остальным ядрам через ICR (destination shorthand = 10b = all except self) | diff --git a/kernel/docs/main.md b/kernel/docs/main.md deleted file mode 100644 index a503668..0000000 --- a/kernel/docs/main.md +++ /dev/null @@ -1,98 +0,0 @@ -# Точка входа: `src/main.rs` - -Точка входа ядра (`kmain`). Инициализирует оборудование, подсистемы памяти, запускает тесты capability и PMActor. - -## Атрибуты крейта - -```rust -#![no_std] // без стандартной библиотеки -#![no_main] // без точки входа из libc -extern crate alloc; // аллокатор кучи -``` - -## Константы - -- `KERNEL_FONT: &[u8]` — встроенный PSF2 шрифт 8×16 на 512 глифов (включён через `include_bytes!("font.psf")`) - -## Limine Boot Protocol Request - -Статические переменные в секции `.requests` (через `#[unsafe(link_section = ".requests")]`): - -| Переменная | Тип | Назначение | -|---|---|---| -| `MEMORY_MAP_REQUEST` | `MemoryMapRequest` | Карта физической памяти | -| `HHDM_REQUEST` | `HhdmRequest` | Higher Half Direct Map offset | -| `KERNEL_ADDR_REQUEST` | `ExecutableAddressRequest` | Базовые адреса ядра (physical + virtual) | -| `BASE_REVISION` | `BaseRevision` | Проверка версии Limine | -| `FRAMEBUFFER_REQUEST` | `FramebufferRequest` | Линейный фреймбуфер | -| `_START_MARKER` | `RequestsStartMarker` | Маркер начала секции requests | -| `_END_MARKER` | `RequestsEndMarker` | Маркер конца секции requests | - ---- - -## `FramebufferDisplay<'a>` - -Низкоуровневый рендерер пикселей, обёртка над `limine::framebuffer::Framebuffer`. - -**impl `DrawTarget` (embedded_graphics):** -- `Color = Rgb888`, `Error = Infallible` -- `draw_iter(pixels)` — запись пикселей в память фреймбуфера с проверкой границ - -**impl `OriginDimensions`:** -- `size()` — размеры фреймбуфера - ---- - -## `Console<'a>` (встроенный, НЕ используется) - -Обёртка над FramebufferDisplay. **В `kmain` используется `tty::Console`**, этот — мёртвый код. - -Поля: `display`, `x`, `y`, `current_color`. -Методы: `new()`, `set_color()`, `clear()`, `scroll()`, `write_str()`. - ---- - -## `kmain()` — точка входа - -`unsafe extern "C" fn() -> !` - -1. **Проверка `BASE_REVISION.is_supported()`** — убеждается, что загрузчик поддерживает нужную версию Limine -2. **Получение ответов Limine**: фреймбуфер, карта памяти, HHDM offset, адреса ядра -3. **Инициализация `tty::Console`** — создаёт консоль на первом фреймбуфере, чистит экран -4. **Инициализация Serial** — `debug::serial::init_global()` для отладки через COM1 -5. **Вывод "BOOT"** через `info!` макрос -6. **Инициализация BitmapPMM** — из карты памяти и HHDM смещения -7. **Инициализация LAPIC** — `cpu::lapic::init(hhdm_offset)` для MMIO доступа к Local APIC -8. **Создание P4 (PML4)** — выделяет фрейм, зануляет его -9. **Картирование HHDM** — все non-reserved entry памяти маппятся identity + HHDM -10. **Картирование .text секции ядра** — по виртуальному базовому адресу -11. **Активация страничных таблиц** — `p4.activate(p4_phys)` — запись CR3 -12. **Выделение кучи 8 MiB** — постранично маппится в `0xFFFF_9000_0000_0000` -13. **Инициализация SlabAllocator** — `ALLOCATOR.init()` с диапазоном кучи -14. **`init_cpu_features()`** — обнаружение INVPCID инструкции -15. **Регистрация адресного пространства ядра** — `vmm::init_kernel_space()` -16. **Инициализация IDT** — регистрация обработчиков page fault и TLB shootdown -17. **Инициализация PMRouter** — 65536 каналов маршрутизации -18. **Включение прерываний** — `sti` инструкция -19. **Тест capability системы**: создание CNode, insert/mint/revoke -20. **PMActor + Buddy тест** (строки 257-359): создание актора, аллокация, OOM, carve, free+coalescing -21. **Вывод ASCII-арт логотипа** -22. **Бесконечный HLT** — `hcf()` - ---- - -## `hcf()` - -```rust -fn hcf() -> ! { - loop { x86_64::instructions::hlt(); } -} -``` - -Бесконечный цикл HLT — снижает энергопотребление, процессор ждёт следующего прерывания. - ---- - -## `rust_panic(info: &PanicInfo) -> !` - -Обработчик паники: пишет информацию в serial, затем вызывает `hcf()`. diff --git a/kernel/docs/memory-management.md b/kernel/docs/memory-management.md deleted file mode 100644 index 7dde83c..0000000 --- a/kernel/docs/memory-management.md +++ /dev/null @@ -1,243 +0,0 @@ -# Управление физической памятью - -## Содержание - -1. [Адресация (PhysAddr/VirtAddr)](#physaddr--virtaddr) -2. [BitmapPMM — менеджер физических фреймов](#bitmappmm) -3. [BuddyAllocator — блочный аллокатор](#buddyallocator) -4. [Page Tables — страничные таблицы x86-64](#page-tables) - ---- - -## PhysAddr / VirtAddr - -**Файл**: `src/mem/address.rs` - -Типы-обёртки над `u64` с HHDM-трансляцией. - -### `PhysAddr(pub u64)` — `#[repr(transparent)]` - -| Метод | Описание | -|---|---| -| `to_virt(hhdm_offset) -> VirtAddr` | `PhysAddr + hhdm_offset` — вход в HHDM | -| `is_aligned() -> bool` | Проверка 4 KiB выравнивания | -| `align_down() -> Self` | Выравнивание вниз до границы страницы | -| `align_up() -> Self` | Выравнивание вверх до границы страницы | - -### `VirtAddr(pub u64)` — `#[repr(transparent)]` - -| Метод | Описание | -|---|---| -| `to_phys(hhdm_offset) -> Option` | `VirtAddr - hhdm_offset`, возвращает None если адрес ниже HHDM | -| `as_ptr::() -> *const T` | Приведение к константному указателю | -| `as_mut_ptr::() -> *mut T` | Приведение к mutable указателю | - ---- - -## BitmapPMM - -**Файл**: `src/mem/pmm.rs` - -Первичный менеджер физической памяти через битовую карту + счётчики ссылок. - -### Структура - -```rust -pub struct BitmapPMM { - bitmap: &'static mut [u8], // один бит на страницу (1 = занята) - ref_counts: &'static mut [u16], // счётчики ссылок для COW/shared - total_pages: usize, // всего физических страниц в системе - used_pages: usize, // количество выделенных страниц - last_byte: usize, // подсказка для следующего сканирования -} -``` - -Глобальная: `pub static PMM: Locked> = Locked::new(None)`. - -### `init(mmap, hhdm_offset)` - -Полный поток инициализации: - -1. Вычисляет макс. физический адрес из карты памяти Limine -2. Рассчитывает размер bitmap и ref_counts -3. Находит USABLE регион для хранения метаданных -4. Зануляет bitmap, все биты = 1 (все заняты), ref_counts = 1 -5. Проходит по карте памяти: все USABLE → `free_frame()` -6. **Лочит** страницы метаданных (чтобы не выдать повторно) -7. **Лочит** страницу 0 (зарезервирована) -8. Сохраняет в глобальную `PMM` - -### `free_frame(addr)` - -- Декрементит `ref_count` -- Если `ref_count == 0`: очищает бит, декрементит `used_pages`, обновляет `last_byte` - -### `lock_frame(addr)` - -- Если страница свободна: бит = 1, `ref_count = 1`, `used_pages++` -- Если страница занята и `ref_count` был 0: `ref_count = 1` (восстановление) - -### `inc_ref_frame(addr)` - -- Инкрементит `ref_count` (с насыщением) — для COW - -### `alloc_frame() -> Option` - -**Двухпроходный сканер:** - -1. Проход 1: от `last_byte` до конца bitmap -2. Проход 2: от 0 до `last_byte` - -Сканирует побайтово в поисках байта с нулевым битом (`!0xFF`), внутри байта через `trailing_ones`. Устанавливает бит, `ref_count = 1`, `used_pages++`, обновляет `last_byte`. - -### `alloc_contiguous(count) -> Option` - -Линейный поиск `count` последовательных свободных страниц. - -### Функции-обёртки - -- `alloc_frame()`, `alloc_contiguous(count)`, `free_frame(addr)`, `inc_ref_frame(addr)` — захватывают `PMM.lock()` -- `get_stats() -> (usize, usize)` — возвращает `(used, total)` - ---- - -## BuddyAllocator - -**Файл**: `src/mem/buddy.rs` - -Power-of-two блочный аллокатор для per-actor управления физической памятью. - -### Константы - -- `MAX_ORDER = 11` — макс. порядок (2^11 = 2048 страниц = 8 MiB) - -### `order_for(page_count) -> usize` - -Вычисляет минимальное k такое, что `2^k >= page_count`. - -```rust -order_for(1) = 0 // 2^0 = 1 -order_for(3) = 2 // 2^2 = 4 >= 3 -order_for(2048) = 11 // 2^11 = 2048 -``` - -### Структура - -```rust -pub struct BuddyAllocator { - free_lists: [Vec; MAX_ORDER + 1], // списки блоков по порядкам - total_pages: usize, // всего страниц - free_pages: usize, // свободно страниц -} -``` - -Инвариант: `free_pages == Σ 2^k * len(free_lists[k])`. - -### `new(total_pages) -> Self` - -**Жадное разложение** диапазона на блоки максимального размера: - -- Итерация по диапазону -- На каждой позиции вычисляет макс. порядок (ограничен alignment через `trailing_zeros` и остатком) -- Добавляет блок в соответствующий `free_list` - -Пример: 6 страниц → блок 4 (order 2) + блок 2 (order 1). - -### `alloc(order) -> Option` - -1. Находит наименьший `k >= order` с непустым `free_list[k]` -2. Pop с конца (O(1)) -3. **Разбивает** до нужного порядка: при каждом разбиении buddy кладётся в список порядком ниже -4. Возвращает индекс - -### `alloc_pages(page_count) -> Option<(usize, usize)>` - -Округляет page_count до степени двойки, вызывает `alloc()`. Возвращает `(index, order)`. - -### `free(block_idx, order)` - -1. Добавляет блок в `free_list[order]` -2. **Coalescing** на каждом уровне: - - `buddy_idx = block_idx ^ (1 << order)` (XOR) - - Ищет buddy в `free_list[order]` (линейно, `swap_remove`) - - Если найден: удаляет его, объединяет, переходит на уровень выше - - Если не найден или вне диапазона: стоп - -### Интроспекция - -- `free_pages()`, `total_pages()`, `is_exhausted()`, `stats()` (per-order counts) - ---- - -## Page Tables - -**Файл**: `src/mem/paging.rs` - -4-уровневая страничная таблица x86-64: P4 → P3 → P2 → P1 (512 entry × 8 байт = 4 KiB каждая). - -### `PageTableFlags` (bitflags) - -| Флаг | Бит | Описание | -|---|---|---| -| `PRESENT` | 0 | Страница в памяти | -| `WRITABLE` | 1 | Разрешена запись | -| `USER` | 2 | Доступ из ring 3 | -| `WRITE_THROUGH` | 3 | Write-through кэширование | -| `NO_CACHE` | 4 | Отключение кэширования | -| `ACCESSED` | 5 | Было обращение | -| `DIRTY` | 6 | Была запись | -| `HUGE_PAGE` | 7 | 2 MiB (P2) или 1 GiB (P3) | -| `GLOBAL` | 8 | Не сбрасывается при перезагрузке CR3 | -| `COW` | 9 | **Кастомный** — Copy-on-Write | -| `NO_EXECUTE` | 63 | Запрет исполнения (NX bit) | - -### `PageTable` — `#[repr(C, align(4096))]` - -Поле: `entries: [u64; 512]` - -### Методы - -#### `map_page(virt, phys, flags, hhdm)` - -1. Walk/create P4 → P3 → P2 → P1 (создаёт отсутствующие) -2. Записывает PTE = `phys | flags` -3. `invlpg` - -#### `get_or_create_next_table(index, hhdm, create) -> Option<&mut PageTable>` - -- Читает PTE, проверяет PRESENT -- Если есть: `&mut *(hhdm + phys_addr)` (HHDM dereference) -- Если нет и `create == true`: `pmm_alloc()`, зануляет, ставит entry с `PRESENT|WRITABLE|USER`, возвращает -- Если нет и `create == false`: None - -#### `walk_to_p1_mut(virt, hhdm, create) -> Option<&mut PageTable>` - -Из virtual address вычисляет индексы P4[47:39], P3[38:30], P2[29:21], P1[20:12], проходит цепочку. - -#### `unmap_page(virt, hhdm)` - -Walk (без create), зануляет PTE. `invlpg`. Идемпотентен. - -#### `translate(virt, hhdm) -> Option` - -Walk 4 уровней. Поддерживает huge pages (1 GiB на P3, 2 MiB на P2). Возвращает PhysAddr + page offset. - -#### `map_region(virt, phys, size, flags, hhdm)` - -Цикл по 4 KiB страницам, каждая → `map_page()`. - -#### `get_flags(virt, hhdm) -> Option` - -Walk, возвращает флаги PTE (с поддержкой huge pages). - -#### `update_flags(virt, flags, hhdm) -> Result<(), ()>` - -Walk, обновляет флаги, сохраняя физический адрес. `invlpg`. - -#### `activate(phys_addr)` - -`unsafe`: запись CR3 = phys_addr → полный сброс TLB. - -### `pmm_alloc() -> Option` - -Обёртка над `PMM.lock().unwrap().alloc_frame()` — разрывает циклическую зависимость между paging и pmm. diff --git a/kernel/docs/memory/address-translation.md b/kernel/docs/memory/address-translation.md new file mode 100644 index 0000000..8d2b35a --- /dev/null +++ b/kernel/docs/memory/address-translation.md @@ -0,0 +1,103 @@ +# Адресация и трансляция: `address.rs` + +## Концептуальная модель + +В x86-64 MMU транслирует виртуальные адреса (48 бит, канонические) +в физические (до 52 бит) через 4-уровневую иерархию page tables. + +Ядро использует **Higher-Half Direct Map (HHDM)** — область виртуальной +памяти, где вся физическая память отображена 1:1 с фиксированным смещением. + +``` +Физический адрес 0x0 + │ + │ + HHDM_OFFSET (например, 0xFFFF8000_0000_0000) + ▼ +Виртуальный адрес 0xFFFF8000_0000_0000 +``` + +## Физический адрес: `PhysAddr(pub u64)` + +Представляет собой физический адрес. Это **newtype** над `u64` — строгая +типизация предотвращает случайное смешивание физических и виртуальных адресов. + +**Методы:** +- `to_virt()` — преобразует в виртуальный адрес через HHDM: + `VirtAddr(self.0 + HHDM_OFFSET)` +- `is_aligned()` — проверка выравнивания на 4KiB +- `align_down()` / `align_up()` — выравнивание вниз/вверх до границы 4KiB + +## Виртуальный адрес: `VirtAddr(pub u64)` + +**Методы:** +- `to_phys()` — обратное преобразование: вычитает HHDM, возвращает `None` + если адрес ниже HHDM (не канонический для физической памяти) +- `as_ptr()` — получить `*const T` (для чтения) +- `as_mut_ptr()` — получить `*mut T` (для записи) + +## HHDM Offset: глобальное состояние + +```rust +static HHDM_OFFSET: AtomicU64 = AtomicU64::new(0); +``` + +- `init_hhdm(offset: u64)` — вызывается в `kmain()` сразу после получения + ответа от bootloader'а. Сохраняет offset через `Ordering::Release`. +- `get_hhdm() -> u64` — читает offset через `Ordering::Relaxed` (высокая + производительность, на этой архитектуре атомарность гарантирована). + +### Почему Atomic? + +HHDM инициализируется однократно до включения прерываний и создания +дополнительных потоков. Atomic гарантирует, что даже если в будущем +инструкции будут переупорядочены, значение будет видно всем ядрам. + +## Код и абстракция + +```rust +// address.rs (полный код) +static HHDM_OFFSET: AtomicU64 = AtomicU64::new(0); + +pub fn init_hhdm(offset: u64) { + HHDM_OFFSET.store(offset, Ordering::Release); + // ^ Release: все предыдущие записи становятся видимы для + // всех acquire-операций на HHDM_OFFSET +} + +pub fn get_hhdm() -> u64 { + HHDM_OFFSET.load(Ordering::Relaxed) + // ^ Relaxed: в однопоточном контексте и при инициализации + // до включения прерываний это безопасно и максимально быстро +} + +#[repr(transparent)] +pub struct PhysAddr(pub u64); +// ^ repr(transparent): гарантирует, что PhysAddr имеет то же +// представление в памяти, что и u64 — важно для FFI и передачи +// в ассемблерные инструкции (mov cr3, rax) + +impl PhysAddr { + pub fn to_virt(self) -> VirtAddr { + VirtAddr(self.0 + get_hhdm()) + // ^ Концептуально: физический адрес + HHDM смещение даёт + // виртуальный адрес, по которому этот физический фрейм + // отображён в higher half + } +} + +impl VirtAddr { + pub fn as_mut_ptr(self) -> *mut T { + self.0 as *mut T + // ^ Прямая интерпретация числа как указателя. + // Безопасность на стороне вызывающего кода. + } +} +``` + +## Где используется + +- `PhysAddr::to_virt()` — в `paging.rs` для доступа к Page Table entries, + в `pmm.rs` для обнуления фреймов, в `buddy.rs` для intrusive list. +- `VirtAddr::as_mut_ptr()` — в `paging.rs` для создания ссылок на PTE. +- `init_hhdm()` — в `kmain()` при старте. +- `get_hhdm()` — в `lapic.rs` для вычисления LAPIC_base. diff --git a/kernel/docs/memory/allocator.md b/kernel/docs/memory/allocator.md new file mode 100644 index 0000000..509c383 --- /dev/null +++ b/kernel/docs/memory/allocator.md @@ -0,0 +1,211 @@ +# Slab-аллокатор: куча ядра + +## Концептуальная модель + +**SlabAllocator** — это глобальный аллокатор кучи для ядра. +Rust-программы используют `alloc::vec::Vec`, `alloc::boxed::Box` и т.д. — +все они в конечном счёте вызывают `GlobalAlloc::alloc()`. + +### Стратегия + +Для маленьких блоков (≤ 2048 байт) — **slab lists**: +предварительно нарезанные блоки фиксированного размера. + +Для больших блоков (> 2048 байт) — **freelist больших блоков**: +освобождённые блоки переиспользуются. + +Если ни там, ни там нет — **bump allocation**: +последовательная раздача из заранее выделенного региона. + +``` +Запрос alloc(32 байта): + 1. list_index(32) = 2 (BLOCK_SIZES[2] = 32) + 2. list_heads[2] есть свободный блок? + - Да: отдаём его, заменяем голову списка + - Нет: bump-аллокация блока размером 32 (fallback_alloc) + +Запрос alloc(4096 байт): + 1. list_index(4096) = None (максимум 2048) + 2. large_block_free есть блок ≥ 4096? + - Да: отдаём + - Нет: bump-аллокация +``` + +## Структуры данных + +### Slab списки — `list_heads` + +```rust +const BLOCK_SIZES: &[usize] = &[8, 16, 32, 64, 128, 256, 512, 1024, 2048]; + +struct ListNode { + next: Option<&'static mut ListNode>, +} + +pub struct SlabAllocator { + list_heads: [Option<&'static mut ListNode>; BLOCK_SIZES.len()], // 9 списков + large_block_free: Option<&'static mut LargeBlockNode>, + heap_start: usize, + heap_end: usize, + next_bump: usize, +} +``` + +### LargeBlockNode — для блоков > 2048 байт + +```rust +struct LargeBlockNode { + size: usize, + next: Option<&'static mut LargeBlockNode>, +} +``` + +### Инициализация: `init(start, size)` + +```rust +pub fn init(&mut self, start: usize, size: usize) { + self.heap_start = start; + self.next_bump = start; + self.heap_end = start + size; +} +``` + +Вызывается в kmain() после настройки page table для области `0xFFFF_9000_0000_0000`. + +## GlobalAlloc — реализация + +### alloc(layout) + +```rust +unsafe fn alloc(&self, layout: Layout) -> *mut u8 { + let mut allocator = self.lock(); + match SlabAllocator::list_index(&layout) { + Some(index) => { + // 1. Пробуем slab list + match allocator.list_heads[index].take() { + Some(node) => { + allocator.list_heads[index] = node.next.take(); + node as *mut ListNode as *mut u8 + } + None => { + // 2. Нет в slab — bump alloc целого блока + let block_size = BLOCK_SIZES[index]; + allocator.fallback_alloc( + Layout::from_size_align(block_size, block_size).unwrap() + ) + } + } + } + None => allocator.fallback_alloc(layout) // > 2048 + } +} +``` + +### fallback_alloc(layout) + +```rust +fn fallback_alloc(&mut self, layout: Layout) -> *mut u8 { + let size = layout.size().max(layout.align()); + + // 1. Пробуем large block free list (для > 2048) + if size > 2048 { + // поиск по large_block_free + while let Some(ref mut node) = *field { + if node.size >= size { + // отдаём, удаляем из списка + return node as *mut u8; + } + field = &mut node.next; + } + } + + // 2. Bump alloc + let alloc_start = (self.next_bump + layout.align() - 1) & !(layout.align() - 1); + let alloc_end = alloc_start.checked_add(layout.size())?; + if alloc_end > self.heap_end { + null_mut() // OOM + } else { + self.next_bump = alloc_end; + alloc_start as *mut u8 + } +} +``` + +### dealloc(ptr, layout) + +```rust +unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) { + let mut allocator = self.lock(); + match SlabAllocator::list_index(&layout) { + Some(index) => { + // Добавляем в slab list (переиспользование) + let new_node = ListNode { next: allocator.list_heads[index].take() }; + let new_node_ptr = ptr as *mut ListNode; + unsafe { new_node_ptr.write(new_node); } + allocator.list_heads[index] = Some(&mut *new_node_ptr); + } + None => { + // Добавляем в large block free list + let new_node = LargeBlockNode { + size: layout.size().max(layout.align()), + next: allocator.large_block_free.take(), + }; + let new_node_ptr = ptr as *mut LargeBlockNode; + unsafe { new_node_ptr.write(new_node); } + allocator.large_block_free = Some(&mut *new_node_ptr); + } + } +} +``` + +**Важно:** `layout.size()` должен быть ≥ `size_of::()`, чтобы +освобождённый блок мог хранить указатели списка. Гарантируется, потому +что наименьший BLOCK_SIZE (8) ≥ `size_of::>` +(8 байт на 64-bit). + +## Locked — примитивная spinlock-обёртка + +```rust +pub struct Locked { + inner: UnsafeCell, + lock: AtomicBool, +} +``` + +- `lock()` — spin-wait с CAS + `hint::spin_loop()`. +- `LockedGuard` — RAII guard, при Drop отпускает блокировку. +- `unsafe impl Sync` — потому что `lock()` гарантирует взаимное исключение. + +Используется не только для аллокатора, но и для PMM, serial port, +VMM KERNEL_SPACE и других глобальных структур. + +## Глобальный аллокатор + +```rust +#[global_allocator] +pub static ALLOCATOR: Locked = Locked::new(SlabAllocator::new()); +``` + +Этот `static` перехватывает все вызовы `alloc::alloc::alloc()`, +делая возможным использование `Vec`, `Box`, `String` и т.д. в ядре. + +## Heap init в kmain() + +```rust +let heap_start = 0xFFFF_9000_0000_0000; +let heap_size = 8 * 1024 * 1024; // 8 MiB +// Предварительно map'им все страницы кучи +for i in (0..heap_size).step_by(4096) { + let frame = mem::pmm::alloc_frame().expect("OOM"); + p4.map_page(VirtAddr(heap_start + i), frame, flags); +} +// Инициализируем аллокатор +allocator::ALLOCATOR.lock().init(heap_start as usize, heap_size); +``` + +## Почему slab? + +1. **Скорость**: alloc/dealloc — O(1) для малых блоков. +2. **Нет фрагментации**: блоки фиксированного размера. +3. **Локальность**: блоки одного размера рядом в памяти. +4. **Простота**: ~180 строк кода. diff --git a/kernel/docs/memory/buddy.md b/kernel/docs/memory/buddy.md new file mode 100644 index 0000000..86bdba9 --- /dev/null +++ b/kernel/docs/memory/buddy.md @@ -0,0 +1,267 @@ +# Buddy Allocator: `buddy.rs` + +## Концептуальная модель + +Buddy-аллокатор — это алгоритм управления памятью, который: +- Делит память на блоки размером `2^order` страниц. +- Каждый блок может быть либо свободен, либо занят. +- При освобождении блок объединяется (coalesce) с соседом (buddy), + если тот тоже свободен, образуя блок вдвое большего размера. + +``` +Пример: порядок 0 (1 страница), порядок 1 (2 страницы), порядок 2 (4 страницы) + +Order 2: [ 0-3 ] [ 4-7 ] [ 8-11 ] +Order 1: [ 0-1 ][ 2-3 ] [ 4-5 ][ 6-7 ] [ 8-9 ][ 10-11 ] +Order 0: [0][1][2][3] [4][5][6][7] [8][9][10][11] [12][13][14][15] + ^ + buddy-пара: (0,1), (2,3), (4,5)... + buddy(i) = i XOR (1 << order) +``` + +## Intrusive List (список в самих страницах) + +Вместо отдельной структуры данных для списков свободных блоков, +Elyz использует **intrusive linked list** — указатели хранятся прямо +внутри свободных физических страниц: + +``` +Страница (4 KiB): +┌──────────────┐ +│ next: usize │ ← указатель на следующую свободную страницу +├──────────────┤ +│ prev: usize │ ← указатель на предыдущую свободную страницу +├──────────────┤ +│ │ +│ (не занято) │ +│ │ +└──────────────┘ +``` + +Для доступа к странице по индексу используется HHDM: +```rust +fn page_virt(&self, idx: usize) -> VirtAddr { + PhysAddr(self.base_phys + idx as u64 * 4096).to_virt() +} +``` + +## Структура BuddyAllocator + +```rust +pub struct BuddyAllocator { + free_heads: [usize; MAX_ORDER + 1], // головы списков для каждого порядка + total_pages: usize, // всего страниц в управлении + free_pages: usize, // свободно страниц + base_phys: u64, // физический адрес начала +} +``` + +- `MAX_ORDER = 11` — максимальный порядок (2^11 = 2048 страниц = 8 MiB). +- `NEXT_SENTINEL = usize::MAX` — маркер конца списка. +- `free_heads[order]` — индекс первой свободной страницы порядка `order`. + +## Операции со списком + +### flist_push(order, idx) — добавить в голову + +```rust +fn flist_push(&mut self, order: usize, idx: usize) { + let head = self.free_heads[order]; + // Устанавливаем: node.next = head, node.prev = SENTINEL + self.write_node(idx, head, NEXT_SENTINEL); + if head != NEXT_SENTINEL { + // head.prev = idx + self.write_node(head, self.read_next(head), idx); + } + self.free_heads[order] = idx; +} +``` + +### flist_remove(order, idx) — удалить из списка + +```rust +fn flist_remove(&mut self, order: usize, idx: usize) { + let next = self.read_next(idx); + let prev = self.read_prev(idx); + // Очищаем указатели удаляемого узла + self.write_node(idx, NEXT_SENTINEL, NEXT_SENTINEL); + if prev != NEXT_SENTINEL { + self.write_node(prev, next, self.read_prev(prev)); + } else { + self.free_heads[order] = next; // удалили голову + } + if next != NEXT_SENTINEL { + self.write_node(next, self.read_next(next), prev); + } +} +``` + +### flist_pop(order) — извлечь из головы + +```rust +fn flist_pop(&mut self, order: usize) -> Option { + let head = self.free_heads[order]; + if head == NEXT_SENTINEL { return None; } + self.flist_remove(order, head); + Some(head) +} +``` + +## Инициализация: `new(total_pages, base_phys)` + +Стратегия: разбить весь диапазон на максимально возможные блоки. + +```rust +pub fn new(total_pages: usize, base_phys: u64) -> Self { + let mut this = Self { + free_heads: [NEXT_SENTINEL; MAX_ORDER + 1], + total_pages, free_pages: 0, base_phys, + }; + if total_pages == 0 { return this; } + + let mut idx = 0; + while idx < total_pages { + let remaining = total_pages - idx; + // Максимальный порядок с учётом выравнивания и остатка + let align_order = /* макс порядок по выравниванию idx */; + let size_order = /* макс порядок по remaining */; + let order = MAX_ORDER.min(align_order).min(size_order); + let block_size = 1usize << order; + this.flist_push(order, idx); + this.free_pages += block_size; + idx += block_size; + } + this +} +``` + +### Как определяются align_order и size_order + +- `align_order`: если `idx == 0` — `MAX_ORDER`; иначе `(idx.trailing_zeros()).min(MAX_ORDER)`. + Чем больше нулевых битов в idx, тем выше порядок выравнивания. +- `size_order`: `(64 - 1) - remaining.leading_zeros()` — максимальная степень двойки + ≤ remaining. + +## Аллокация: `alloc(order)` + +```rust +pub fn alloc(&mut self, order: usize) -> Option { + // 1. Ищем первый непустой список начиная с order + let found_order = (order..=MAX_ORDER) + .find(|&o| self.free_heads[o] != NEXT_SENTINEL)?; + + // 2. Извлекаем блок из found_order + let block_idx = self.flist_pop(found_order); + self.free_pages -= 1 << found_order; + + // 3. Разбиваем до нужного порядка (split) + let mut cur_order = found_order; + while cur_order > order { + cur_order -= 1; + let buddy_idx = block_idx + (1 << cur_order); + self.flist_push(cur_order, buddy_idx); + self.free_pages += 1 << cur_order; + } + + Some(block_idx) + // Возвращается индекс первого блока +} +``` + +### Пример split + +``` +Запрос: order 1 (2 страницы) +Найден: order 3 (8 страниц, блок [0-7]) + +Шаг 1: cur_order = 3 → 2 + buddy = 0 + 4 = 4 + push(order=2, idx=4) — блок [4-7] в order 2 +Шаг 2: cur_order = 2 → 1 + buddy = 0 + 2 = 2 + push(order=1, idx=2) — блок [2-3] в order 1 +Результат: order=1, idx=0 — блок [0-1] +``` + +## Освобождение: `free(block_idx, order)` + coalesce + +```rust +pub fn free(&mut self, mut block_idx: usize, mut order: usize) { + // Пытаемся объединить с buddy + while order < MAX_ORDER { + let buddy_idx = block_idx ^ (1 << order); + + // Проверка: buddy в пределах памяти? + let buddy_end = buddy_idx.checked_add(1 << order)?; + if buddy_end > self.total_pages { break; } + + // Buddy свободен? + if self.flist_contains(order, buddy_idx) { + // Удаляем buddy из его списка + self.flist_remove(order, buddy_idx); + self.free_pages -= 1 << order; + // Объединяем: block_idx = min(block_idx, buddy_idx) + block_idx = block_idx.min(buddy_idx); + order += 1; + } else { + break; // buddy занят, не можем объединить + } + } + + // Добавляем объединённый блок в список + self.free_pages += 1 << order; + self.flist_push(order, block_idx); +} +``` + +### Пример coalesce + +``` +Освобождаем: order 0, idx = 0 +Блок [0] возвращается в список order 0. + +Проверка: buddy(0, order 0) = 0 XOR 1 = 1 +Если idx=1 тоже свободен в order 0: + - Удаляем idx=1 из order 0 + - block_idx = min(0, 1) = 0 + - order = 1 + - Проверка: buddy(0, order 1) = 0 XOR 2 = 2 + Если idx=2 свободен в order 1: + - Удаляем idx=2 из order 1 + - order = 2 + - ... +``` + +## alloc_pages(page_count) — универсальная аллокация + +```rust +pub fn alloc_pages(&mut self, page_count: usize) -> Option<(usize, usize)> { + let order = order_for(page_count); + self.alloc(order).map(|idx| (idx, order)) +} +``` + +`order_for()` вычисляет `ceil(log2(page_count))`. + +## Вспомогательные функции + +- `free_pages()` / `total_pages()` — геттеры. +- `is_exhausted()` — `free_pages == 0`. +- `stats()` — возвращает `[(order, count)]` для каждого порядка (количество + свободных блоков). Используется для отладки. +- `flist_contains(order, idx)` — проверка, есть ли блок в списке + (проверяет `prev != NEXT_SENTINEL` или `head == idx`). + +## Почему Intrusive List? + +1. **Zero overhead**: не нужно отдельной аллокации для списков. +2. **O(1)**: push/pop/remove — все O(1) операции. +3. **Константная память**: аллокатор не потребляет дополнительной памяти + сверх управляемых страниц. +4. **Кэш-дружественность**: доступ к next/prev обращается к той же странице, + которая будет выделена. + +## Где используется + +- `PMActor` содержит `BuddyAllocator` для управления своим диапазоном. +- Прямые тесты в `kmain()` демонстрируют all/alloc/free/coalesce/exhaustion. diff --git a/kernel/docs/memory/introduction.md b/kernel/docs/memory/introduction.md new file mode 100644 index 0000000..906dc66 --- /dev/null +++ b/kernel/docs/memory/introduction.md @@ -0,0 +1,82 @@ +# Управление памятью: концептуальная модель + +## Архитектурная философия + +Управление памятью в Elyz построено как **трёхуровневая иерархия**, +где каждый уровень решает свою задачу и взаимодействует с соседними +через строго определённые интерфейсы. + +``` +Уровень 1: BitmapPMM (глобальный, физический) + │ + │ предоставляет сырые фреймы + ▼ +Уровень 2: PMActor + BuddyAllocator (распределённый, физический) + │ + │ управляет диапазонами, выдаёт под-диапазоны + ▼ +Уровень 3: AddressSpace (VMM) (виртуальный) + │ + │ отображает физические фреймы в виртуальные адреса + ▼ + CPU (MMU, page tables) +``` + +### Зачем три уровня? + +- **BitmapPMM** — глобальный аллокатор физических фреймов. Простой, + надёжный, но неэффективный для частых alloc/free маленьких блоков. + Используется для начальной загрузки и для Page Table страниц. + +- **BuddyAllocator + PMActor** — распределённая модель. Каждый актор + управляет своим диапазоном физической памяти через buddy-алгоритм. + Это даёт: (1) изоляцию — актор A не может истощить память актора B; + (2) масштабирование — акторы могут работать параллельно; + (3) предсказуемость — каждый актор знает свой лимит. + +- **AddressSpace (VMM)** — виртуальные адресные пространства. PML4, + VMA-деревья, copy-on-write, lazy mapping. Использует PMM для + аллокации Page Table страниц (через `pmm_alloc` в `paging.rs`). + +### Разделение ответственности по файлам + +| Файл | Компонент | Роль в абстракции | +|------|-----------|-------------------| +| `address.rs` | PhysAddr / VirtAddr / HHDM | Базовые типы для адресов | +| `pmm.rs` | BitmapPMM | Глобальный менеджер физических фреймов | +| `buddy.rs` | BuddyAllocator | O(1) buddy allocator (intrusive list) | +| `pm_manages.rs` | PMActor + PMActorQueue | Актёр физической памяти + MPSC очередь | +| `pm_router.rs` | PMRouter | Lock-free маршрутизация запросов/ответов | +| `paging.rs` | PageTable | Аппаратные 4-уровневые page tables | +| `vmm.rs` | AddressSpace + VMA | Виртуальные адресные пространства | +| `allocator.rs` | SlabAllocator | Кучевой аллокатор (global_allocator) | +| `mod.rs` | Экспорт | Фасад подсистемы | + +### Поток данных: типичная аллокация + +``` +Процесс A хочет 16 страниц: + 1. Код пользователя отправляет PMRequest::Allocate в PMActor через + PMRouter::alloc_channel() + actor.submit_request() + 2. Когда актор получает CPU, process_messages() вызывает + buddy.alloc_pages(16) + 3. BuddyAllocator находит блок порядка 4 (2^4 = 16) или больше, + разбивает его при необходимости + 4. PMActor создаёт Capability с CapObject::Memory { phys, size_pages } + 5. Ответ (PMResponse::Allocated) отправляется через PMRouter + 6. Получатель может отобразить фреймы в своё AddressSpace через + map_region() или map_shared() +``` + +### Поток данных: page fault + +``` +CPU ловит #PF (page fault): + 1. interrupt.rs: rust_page_fault_handler() читает CR2 + 2. Получает блокировку KERNEL_SPACE + 3. process_pending_revocations() — обрабатывает накопленные отзывы + 4. handle_fault() проверяет: это COW? это lazy region? + 5. Если COW — копируем страницу (copy-on-write) + 6. Если lazy — alloc_frame() из PMM + map_page() + 7. Если нераспознанный fault — KERNEL PANIC +``` diff --git a/kernel/docs/memory/paging.md b/kernel/docs/memory/paging.md new file mode 100644 index 0000000..aacca6e --- /dev/null +++ b/kernel/docs/memory/paging.md @@ -0,0 +1,196 @@ +# 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 — представление таблицы + +```rust +#[repr(C, align(4096))] +pub struct PageTable { + entries: [u64; 512], +} +``` + +- `512` entries × 8 байт = 4096 байт (ровно одна страница). +- `align(4096)` — гарантирует, что структура размещается на физической + странице, как требует аппаратура. + +## PageTableFlags — битовые флаги + +```rust +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) — отображение одной страницы + +```rust +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) — удаление отображения + +```rust +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 + +```rust +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 — сердце навигации + +```rust +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) — следующий уровень + +```rust +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::(), 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::() }) +} +``` + +### pte_addr_mask — маска адреса + +```rust +const PTE_ADDR_MASK: u64 = 0x000F_FFFF_FFFF_F000; +``` + +Биты 51:12 — физический адрес с выравниванием 4KiB. +(0x000F_FFFF_FFFF_F000 = 52 бита адреса, очищенные нижние 12 бит). + +## pmm_alloc — шлюз к PMM + +```rust +pub fn pmm_alloc() -> Option { + 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. diff --git a/kernel/docs/memory/physical-memory.md b/kernel/docs/memory/physical-memory.md new file mode 100644 index 0000000..0120eb5 --- /dev/null +++ b/kernel/docs/memory/physical-memory.md @@ -0,0 +1,132 @@ +# Глобальный менеджер физической памяти: `pmm.rs` + +## Концептуальная модель + +`BitmapPMM` — это **глобальный, единственный в системе**, распределитель +физических фреймов (4 KiB). Он отвечает на вопрос: + +> Какой физический фрейм сейчас свободен? + +### Структуры данных + +PMM использует **трёхуровневую битовую карту**: + +``` +Уровень 1 (L1 bitmap): u64 слова + бит в L1 = 1 если соответствующее слово L0 не полностью занято + +Уровень 0 (L0 bitmap): байты + каждый байт = 8 фреймов, каждый бит = 1 фрейм + бит = 1 → фрейм занят + бит = 0 → фрейм свободен + +Ref-counts (счётчики ссылок): u16 + для каждого фрейма — сколько раз он был захвачен +``` + +### Зачем L1 (ускорение)? + +Наивный линейный поиск по битовой карте размером в мегабайты — O(N). +L1-битмап позволяет пропускать целые группы по 64 слова (4096 фреймов), +где нет свободных страниц. + +**Поиск:** +1. Ищем `l1_bitmap[l1_idx]` с ненулевым значением (есть свободные). +2. Вычисляем `word_idx` по позиции бита внутри L1-слова. +3. Читаем `bitmap[word_idx]` как u64. +4. Если слово == `!0` (все заняты) — очищаем бит в L1, продолжаем. +5. Иначе — `(!word).trailing_zeros()` даёт номер свободного бита. +6. Вычисляем `page_idx` и возвращаем `PhysAddr`. + +### Инициализация + +```rust +pub unsafe fn init(mmap: &limine::response::MemoryMapResponse) { +``` + +1. Находим максимальный физический адрес из всех entry карты памяти. +2. Вычисляем `total_pages = max_addr / PAGE_SIZE`. +3. Вычисляем размер метаданных: + - `bitmap_size = total_pages / 8` (1 бит на фрейм) + - `ref_counts_size = total_pages * 2` (u16 на фрейм) + - `l1_size = (total_pages / 64 / 64) * 8` +4. Ищем **первый USABLE регион** размером >= метаданные. +5. Размещаем метаданные в этом регионе: сначала bitmap, потом ref_counts, + потом L1. +6. **Заполняем bitmap = 0xFF** (все занято). +7. **Проходим по карте памяти**: для каждого USABLE entry очищаем биты + (free_frame). +8. **Блокируем фреймы метаданных** (они теперь заняты PMM). +9. **Блокируем фрейм 0** (традиционно reserved). +10. Сохраняем `PMM.lock() = Some(pmm)`. + +### alloc_frame() — пошагово + +```rust +pub fn alloc_frame(&mut self) -> Option { +``` + +1. Двухпроходный поиск: сначала от `last_word` до конца, потом от начала. + (`last_word` — хинт для локализации, даёт амортизированное O(1)). +2. Для каждого L1-слова: если != 0 — есть свободные. +3. `trailing_zeros()` даёт первое слово с хотя бы одним свободным битом. +4. Читаем слово как u64. +5. `(!word).trailing_zeros()` даёт свободный бит. +6. Вычисляем `page_idx`. +7. Устанавливаем бит = 1, ref_count = 1, used_pages += 1. +8. Если слово стало полностью занятым — очищаем бит в L1. +9. Обновляем `last_word`. + +### free_frame() — пошагово + +```rust +pub fn free_frame(&mut self, phys_addr: PhysAddr) { +``` + +1. Если бит уже 1 (фрейм занят): + - Уменьшаем ref_count (saturating_sub). + - Если ref_count == 0: очищаем бит, used_pages -= 1, обновляем L1. +2. Если бит == 0 (уже свободен) — ничего не делаем (идемпотентность). + +### lock_frame() — захват без освобождения + +Используется для резервирования фреймов, которые не должны быть +освобождены (нулевой фрейм, фреймы метаданных PMM). + +### inc_ref_frame() — увеличение счётчика + +Используется COW (Copy-on-Write) при fork для учёта разделяемых страниц. + +## Thread Safety + +```rust +pub static PMM: Locked> = Locked::new(None); +``` + +Locked — примитивная spinlock-обёртка (см. `allocator.rs`). +Все операции PMM требуют захвата блокировки. + +## Глобальные функции + +В `pmm.rs` определены функции-обёртки для удобства: + +| Функция | Вызывает | +|---------|----------| +| `alloc_frame()` | `PMM.lock().as_mut()?.alloc_frame()` | +| `alloc_contiguous(n)` | `PMM.lock().as_mut()?.alloc_contiguous(n)` | +| `free_frame(addr)` | `PMM.lock().as_mut()?.free_frame(addr)` | +| `inc_ref_frame(addr)` | `PMM.lock().as_mut()?.inc_ref_frame(addr)` | +| `get_stats()` | `PMM.lock().as_ref()?.used_pages/total_pages` | + +## alloc_contiguous() — выделение непрерывной области + +Линейный проход по всем страницам, поиск `count` последовательных +свободных фреймов. O(total_pages) — используется редко, только для +устройств без IOMMU. + +## Ключевая инварианта + +`BitmapPMM` не знает о buddy-аллокаторах и PMActors. Он просто +отвечает на запросы «дай фрейм» и «забери фрейм». PMActors получают +свои диапазоны из PMM на этапе инициализации и больше к нему не +обращаются. diff --git a/kernel/docs/memory/pm-actor.md b/kernel/docs/memory/pm-actor.md new file mode 100644 index 0000000..388f429 --- /dev/null +++ b/kernel/docs/memory/pm-actor.md @@ -0,0 +1,258 @@ +# 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 глубок. diff --git a/kernel/docs/memory/pm-router.md b/kernel/docs/memory/pm-router.md new file mode 100644 index 0000000..58e094e --- /dev/null +++ b/kernel/docs/memory/pm-router.md @@ -0,0 +1,189 @@ +# PM Router: Lock-Free маршрутизация ответов + +## Концептуальная модель + +**PM Router** — это центральный коммутатор, соединяющий исполнителей +запросов (PMActors) с потребителями результатов (процессы/потоки). + +- Он предоставляет **65536 каналов** для асинхронной коммуникации. +- Каналы **lock-free**: alloc/release канала — CAS на атомарных счётчиках. +- Исключает блокировки между продюсером и консьюмером. + +``` +Процесс A PMActor #1 + │ │ + │ alloc_channel() │ + │ ─────────► router ──────► │ + │ │ + │ submit_request(Alloc) │ + │ ────────────────────────► │ + │ │ process_messages() + │ │ ──────► buddy.alloc() + │ │ + │ ◄─────────────── │ + │ route_responses([Resp]) │ + │ router │ + │ │ + │ wait_for_response(ch) │ + │ ◄═══ RESULT ════ │ + │ │ + │ free_channel (неявно) │ +``` + +## Структура канала + +```rust +#[repr(align(64))] +pub struct Channel { + state: AtomicU8, // FREE=0, PENDING=1, READY=2 + next_free: AtomicU16, // указатель в стеке свободных каналов + result: UnsafeCell>, // ячейка результата +} +``` + +- `align(64)` — каждая кэш-линия содержит ровно один канал. +- `state` — конечный автомат: FREE → PENDING → READY → FREE. +- `UnsafeCell` — потому что write происходит из route_responses, + read — из wait_for_response. Синхронизация через state. + +## Структура PMRouter + +```rust +pub struct PMRouter { + channels: Box<[Channel]>, // 65536 каналов, Box<[T]> в куче + free_head: AtomicU16, // стек свободных каналов +} +``` + +### Стек свободных каналов + +Изначально все каналы свободны, `free_head = 1` (канал 0 зарезервирован +как «discard» — ответы на канал 0 игнорируются). + +``` +free_head ──► channel[1].next_free = 2 + channel[2].next_free = 3 + channel[3].next_free = 4 + ... + channel[65535].next_free = 0 (NULL) +``` + +## Операции + +### alloc_channel() — выделить канал + +```rust +pub fn alloc_channel(&self) -> Option { + let mut head = self.free_head.load(Acquire); + loop { + if head == 0 { return None; } // нет свободных + let next = self.channels[head].next_free.load(Relaxed); + // CAS: free_head = head → next + match self.free_head.compare_exchange_weak(head, next, AcqRel, Acquire) { + Ok(_) => { + self.channels[head].state.store(STATE_PENDING, Release); + return Some(head); + } + Err(new) => head = new, + } + } +} +``` + +Lock-free: CAS на `free_head` позволяет нескольким продюсерам +конкурировать без блокировок. + +### route_responses(responses) — запись результатов + +```rust +pub fn route_responses(&self, responses: Vec) { + for resp in responses { + if resp.channel_id == 0 { continue; } + let channel = &self.channels[resp.channel_id as usize]; + unsafe { *channel.result.get() = Some(resp.result); } + channel.state.store(STATE_READY, Release); + // TODO: Focus Mode — пробуждение ожидающего потока + } +} +``` + +### wait_for_response(id) — ожидание результата + +```rust +pub fn wait_for_response(&self, id: u16) -> PMResult { + let channel = &self.channels[id as usize]; + // busy-wait с HLT + while channel.state.load(Acquire) != STATE_READY { + unsafe { asm!("hlt") }; // CPU остановка до прерывания + } + let result = unsafe { (*channel.result.get()).take().unwrap() }; + channel.state.store(STATE_FREE, Release); + + // Возвращаем канал в стек свободных + let mut head = self.free_head.load(Relaxed); + loop { + channel.next_free.store(head, Relaxed); + match self.free_head.compare_exchange_weak(head, id, Release, Relaxed) { + Ok(_) => break, + Err(new) => head = new, + } + } + result +} +``` + +### request_and_wait — синхронная обёртка + +```rust +pub fn request_and_wait(actor: &PMActor, req_builder: F) -> PMResult +where + F: FnOnce(u16) -> PMRequest +{ + let router = get_router(); + let channel_id = router.alloc_channel().expect("OOM"); + let req = req_builder(channel_id); + actor.submit_request(req).expect("Inbox full"); + router.wait_for_response(channel_id) +} +``` + +## Глобальный экземпляр + +```rust +static ROUTER: GlobalRouter = GlobalRouter { + is_ready: AtomicBool::new(false), + inner: UnsafeCell::new(None), +}; +``` + +Double-checked initialization: +1. `init()` — сетап всех каналов, `is_ready = true`. +2. `get_router()` — проверяет `is_ready`, затем `unwrap_unchecked()`. + +**Безопасность**: `is_ready` устанавливается один раз и никогда не +очищается, поэтому TOCTOU между проверкой и unwrap — безопасен. + +## dispatch() — утилита + +```rust +pub fn dispatch(responses: Vec) { + get_router().route_responses(responses); +} +``` + +## Ключевые свойства + +| Свойство | Значение | +|----------|----------| +| Количество каналов | 65536 | +| Размер канала | 64 байта (1 cache line) | +| alloc_channel | Lock-free, O(1) | +| route_responses | O(N), N = количество ответов | +| wait_for_response | Lock-free + HLT | +| Состояния | FREE → PENDING → READY → FREE | + +## Где используется + +- В `kmain()`: тестирование PMActor через Router. +- Будущие IPC и процесс-менеджер будут использовать Router для + асинхронного взаимодействия с PMActors. diff --git a/kernel/docs/memory/vmm.md b/kernel/docs/memory/vmm.md new file mode 100644 index 0000000..55d32fb --- /dev/null +++ b/kernel/docs/memory/vmm.md @@ -0,0 +1,267 @@ +# 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 (сортирован по адресу)│ +└─────────────────────────────────────────────┘ + │ + ▼ + ┌───────────────┐ + │ PML4 (P4) │ ◄── CR3 + ├───────────────┤ + │ PDE (P3) │ + ├───────────────┤ + │ PTE (P2) │ + ├───────────────┤ + │ PTE (P1) │ ◄── 4 KiB страницы + └───────────────┘ +``` + +## VmaRegion — регион виртуальной памяти + +```rust +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 — источник физических страниц + +```rust +pub enum VmaBacking { + Anonymous(Vec>), + // ^ 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 + +```rust +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). + +```rust +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()` — создание пустого пространства + +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)` — отображение + +```rust +pub fn map_region(&mut self, virt: VirtAddr, phys: Option, + size: u64, flags: VmaFlags, cap_token: u64) -> Result +``` + +- `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 + +```rust +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 + +```rust +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) + +```rust +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() — контекстный переключатель + +```rust +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 + +```rust +pub static KERNEL_SPACE: Locked> = 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 — типы ошибок + +```rust +pub enum VmError { + OutOfMemory, // Нет фреймов в PMM + RegionOverlap, // VMA пересекается с существующим + RegionNotFound, // Нет VMA по адресу + InvalidAlignment, // Адрес/размер не выровнены на 4k + InvalidRange, // Нулевой размер или переполнение + PermissionDenied, // Нет прав на запись + UnexpectedFault, // Fault в не-lazy, не-COW регионе + NonCanonical, // Некорректный адрес + AsidExhausted, // Закончились ASID +} +``` diff --git a/kernel/docs/overview/architecture.md b/kernel/docs/overview/architecture.md new file mode 100644 index 0000000..0f0d899 --- /dev/null +++ b/kernel/docs/overview/architecture.md @@ -0,0 +1,105 @@ +# Архитектура ядра Elyz + +## Высокоуровневая схема + +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ kmain() — точка входа │ +│ (инициализация: Limine, PMM, LAPIC, IDT, VMM, PM Router, Caps) │ +└──────────────┬──────────────────────────────────────┬───────────────┘ + │ │ + ┌──────────▼──────────┐ ┌─────────▼──────────────┐ + │ memory subsystem │ │ capability system │ + │ │ │ │ + │ ┌───────────────┐ │ │ ┌──────────────────┐ │ + │ │ BitmapPMM │ │ │ │ CapDescriptor │ │ + │ │ (глобальный) │ │ │ │ (права + объект) │ │ + │ └───────┬───────┘ │ │ └────────┬─────────┘ │ + │ │ │ │ │ │ + │ ┌───────▼───────┐ │ │ ┌────────▼─────────┐ │ + │ │ PM Actor + │ │ │ │ CNode │ │ + │ │ BuddyAlloc │ │ │ │ (таблица caps) │ │ + │ └───────┬───────┘ │ │ └──────────────────┘ │ + │ │ │ │ │ + │ ┌───────▼───────┐ │ └────────────────────────┘ + │ │ PM Router │ │ + │ │ (каналы) │ │ + │ └───────┬───────┘ │ + │ │ │ + │ ┌───────▼───────┐ │ + │ │ AddressSpace │ │ + │ │ (VMM, PML4) │ │ + │ └───────────────┘ │ + └─────────────────────┘ + +┌──────────────────────┐ ┌──────────────────────┐ +│ cpu subsystem │ │ debug subsystem │ +│ ┌────────────────┐ │ │ ┌─────────────────┐ │ +│ │ IDT + Interrupt│ │ │ │ Serial port │ │ +│ │ Handlers │ │ │ │ (COM1) │ │ +│ └────────────────┘ │ │ └─────────────────┘ │ +│ ┌────────────────┐ │ │ ┌─────────────────┐ │ +│ │ Local APIC │ │ │ │ Log macros │ │ +│ └────────────────┘ │ │ │ (info!/warn!) │ │ +└──────────────────────┘ │ └─────────────────┘ │ + └──────────────────────┘ +``` + +## Порядок инициализации в `kmain()` + +1. **Limine requests** — статические структуры, сообщающие bootloader'у, + что ядру нужны: framebuffer, memory map, HHDM offset, адрес ядра. +2. **BASE_REVISION проверка** — убеждаемся, что Limine совместим. +3. **HHDM инициализация** — сохраняем Higher-Half Direct Map offset. +4. **Framebuffer/TTY** — создаём консоль для вывода на экран. +5. **Serial port** — инициализируем COM1 для отладки. +6. **Early IDT** — загружаем базовые обработчики исключений (0–31). +7. **BitmapPMM::init()** — сканируем карту памяти, строим битовую карту. +8. **LAPIC::init()** — настраиваем Local APIC. +9. **Создание P4** — аллоцируем фрейм для корневой таблицы страниц. +10. **Identity map** — отображаем всю физическую память в HHDM + и (для non-reserved) в идентичное отображение. +11. **Map kernel** — отображаем образ ядра. +12. **Activate P4** — загружаем новую таблицу страниц в CR3. +13. **Heap init** — резервируем 8 MiB для slab-аллокатора. +14. **CPU features** — проверяем INVPCID. +15. **VMM init** — регистрируем AddressSpace ядра. +16. **IDT final** — перезагружаем IDT с Page Fault и TLB shootdown. +17. **PM Router init** — создаём 65536 каналов. +18. **STI** — разрешаем прерывания. +19. **Capability тесты** — тестируем CNode, mint, revoke. +20. **PMActor тесты** — создаём актор, тестируем Alloc/Free/Carve/OOM. +21. **Buddy тесты** — прямое тестирование аллокатора. +22. **PMM stress test** — alloc/free 64 фреймов. +23. **HCF** — гасим CPU. + +## Модульная структура + +``` +src/ +├── main.rs — точка входа, инициализация, тесты +├── cap/ — capability система +│ ├── mod.rs — CNode (таблица дескрипторов) +│ ├── descriptor.rs — типы Capability, CapObject, CapRights, Relation +│ └── object.rs — KernelObject (ref-counted) +├── cpu/ — подсистема CPU +│ ├── mod.rs — реэкспорт модулей +│ ├── idt.rs — IDT entry/table/ptr +│ ├── interrupts.rs — обработчики исключений, TLB shootdown +│ └── lapic.rs — Local APIC драйвер +├── debug/ — подсистема отладки +│ ├── mod.rs — LogLevel, log!/info!/warn! макросы +│ └── serial.rs — COM1 serial port драйвер +├── events.rs — RevocationQueue (lock-free кольцевой буфер) +├── tty.rs — Framebuffer console (PSF2 шрифты) +├── mem/ — подсистема памяти +│ ├── mod.rs — реэкспорт модулей +│ ├── address.rs — PhysAddr, VirtAddr, HHDM offset +│ ├── allocator.rs — SlabAllocator + Locked + глобальный аллокатор +│ ├── buddy.rs — BuddyAllocator (intrusive list, O(1)) +│ ├── paging.rs — PageTable, 4-level page walk +│ ├── pmm.rs — BitmapPMM (tree bitmap с L1 ускорением) +│ ├── pm_manages.rs — PMActor + PMActorQueue (MPSC) +│ ├── pm_router.rs — PMRouter (65536 lock-free каналов) +│ └── vmm.rs — AddressSpace, VMA, COW, fork, TLB shootdown +``` diff --git a/kernel/docs/overview/introduction.md b/kernel/docs/overview/introduction.md new file mode 100644 index 0000000..74a32b5 --- /dev/null +++ b/kernel/docs/overview/introduction.md @@ -0,0 +1,72 @@ +# Введение в ядро Elyz (LISA) + +## Концептуальная модель + +Elyz (LISA) — это модульное, capability-ориентированное ядро для x86-64, +спроектированное вокруг следующих архитектурных принципов: + +### 1. Capability-ориентированная безопасность + +Вместо традиционной модели «всё или ничего» (ring 0 vs ring 3), Elyz +использует систему **capabilities** (дескрипторов прав). Каждый дескриптор +представляет собой *неподделываемый токен*, дающий доступ к конкретному +объекту ядра (фрейму памяти, CNode, PMActor) с определёнными правами. + +- Capability — это не просто число; это структура с полями `object`, `rights`, + `relation`, `token_sig`. +- Capability можно *создавать* (mint) с урезанными правами от родительского + дескриптора. +- Capability можно *отозвать* (revoke), что каскадно уничтожает всех потомков. +- Система гарантирует, что access — это всегда наличие capability. + +### 2. Многоуровневое управление памятью + +Управление физической памятью разделено на три уровня: + +| Уровень | Компонент | Ответственность | +|---------|-----------|-----------------| +| 1 (глобальный) | `BitmapPMM` | Владение всей физической памятью, аллокация/освобождение по 4KiB фреймам. Инициализируется из карты памяти bootloader'а. | +| 2 (распределённый) | `PMActor` | Актёр физической памяти — владеет *диапазоном* физической памяти, использует `BuddyAllocator` для аллокации внутри этого диапазона. Каждый PMActor — изолированный аллокатор. | +| 3 (виртуальный) | `AddressSpace` (VMM) | Управляет виртуальными адресными пространствами (PML4 + VMA-деревья). Использует PMM для аллокации PT-страниц и lazy demand-paging. | + +Эти уровни связывает **PM Router** — lock-free система каналов для асинхронной +пересылки запросов и ответов между исполнителями. + +### 3. Асинхронная модель акторов + +PMActor следует модели CSP (Communicating Sequential Processes): + +- Каждый актор имеет lock-free MPSC-очередь входящих сообщений. +- Потоки/процессы отправляют запросы (Alloc/Free/Carve) в очередь актора. +- Когда актор получает CPU time, он вызывает `process_messages()` и + отправляет ответы через PM Router. +- PM Router использует 65536 предварительно выделенных каналов для + lock-free маршрутизации результатов. + +### 4. Виртуальная память с Copy-on-Write + +`AddressSpace` поддерживает: +- **Lazy demand-paging**: физическая страница выделяется только при + page fault'е. +- **Copy-on-Write (COW)**: при fork'е адресного пространства страницы + разделяются между родителем и потомком; копирование происходит + при первой записи. +- **Zero-copy shared mappings**: физические страницы, принадлежащие + capability, могут быть отображены в другое адресное пространство + без копирования. Владелец страниц — capability, а не VMA. + +### 5. Отзыв (Revocation) через lock-free очередь + +Система отзыва дескрипторов использует глобальную lock-free кольцевую +очередь `MMU_REVOCATION_QUEUE`. При отзыве capability: +1. Токен capability помещается в очередь. +2. При следующем page fault'е VMM обрабатывает накопленные отзывы. +3. Все VMA, связанные с отозванным токеном, аннулируются. + +### 6. Минималистичный дизайн + +- `#![no_std]` — никакой стандартной библиотеки. +- `#![no_main]` — точка входа `kmain`. +- Все структуры данных, аллокаторы и драйверы написаны с нуля. +- Единственные внешние зависимости: `limine` (bootloader протокол), + `embedded-graphics` (шрифты), `bitflags`. diff --git a/kernel/docs/pmactor.md b/kernel/docs/pmactor.md deleted file mode 100644 index 4f756f0..0000000 --- a/kernel/docs/pmactor.md +++ /dev/null @@ -1,173 +0,0 @@ -# Акторы физической памяти (PMActor) и маршрутизатор (PM Router) - ---- - -## Формат сообщений - -Вся коммуникация использует упаковку в `u64`: - -``` -[8b opcode | 16b channel_id | 20b arg1 | 20b arg2] -``` - -Константы: `OPCODE_SHIFT = 56`, `CHAN_SHIFT = 40`, `ARG1_SHIFT = 20` -Маски: `CHAN_MASK = 0x0000_FFFF`, `ARG_MASK = 0x000F_FFFF` - ---- - -## PMActor - -**Файл**: `src/mem/pm_manages.rs` - -Распределённый актор физической памяти. Каждый актор владеет фиксрованным диапазоном и управляет им через BuddyAllocator. - -### PMRequest - -```rust -pub enum PMRequest { - Allocate { size_pages: usize, token_sig: u32, channel_id: u16 }, - Free { local_frame_idx: usize, order: usize }, - Carve { offset_pages: usize, size_pages: usize, channel_id: u16 }, - None, // sentinel (не может быть отправлен) -} -``` - -Методы: `pack() -> u64`, `unpack(val: u64) -> Self`. - -### PMResult - -```rust -pub enum PMResult { - Allocated { cap: Capability, order: usize }, - OutOfMemory { size_pages: usize }, - Carved { cap: Capability }, - Freed { pages_returned: usize }, -} -``` - -### PMResponse - -```rust -pub struct PMResponse { - pub channel_id: u16, - pub result: PMResult, -} -``` - -### PMActorQueue — Lock-free MPSC - -Кольцевой буфер на 1024 слота (`AtomicU64`) с cache-line padding. - -- `send(req)` — CAS loop на `tail` (multi-producer). Отклоняет None. -- `pop()` — single-consumer. `Acquire`/`Release` ordering. - -### PMActor - -```rust -pub struct PMActor { - actor_id: u64, - root_untyped: Capability, - managed_range: (PhysAddr, PhysAddr), // [start, end) - queue: PMActorQueue, - buddy: BuddyAllocator, -} -``` - -#### `new(actor_id, root_cap, start, size_bytes) -> Self` - -Конструктор. - -#### `submit_request(req) -> Result<(), &str>` - -Thread-safe: пушит в `queue`. - -#### `process_messages(&mut self) -> Vec` - -Дренирует очередь, обрабатывает каждый запрос через приватные методы. - -**Только из контекста актора** (`&mut self` гарантирует single consumer). - -#### Приватные обработчики - -**`handle_allocate(size_pages, token_sig, channel_id) -> Option`** - -1. `buddy.alloc_pages(size_pages)` → `(local_idx, order)` -2. Строит сильную дочернюю Capability с уникальным `token_sig` -3. Возвращает `Allocated { cap, order }` -4. При неудаче: `OutOfMemory` (планируется ballooning) - -**`handle_free(local_frame_idx, order) -> Option`** - -1. Валидация границ -2. `buddy.free(local_frame_idx, order)` -3. Возвращает None (маршрутизация не нужна) - -**`handle_carve(offset_pages, size_pages, channel_id) -> Option`** - -1. Валидация диапазона -2. Возвращает `Carved { cap }` -3. Не взаимодействует с buddy - ---- - -## PMRouter - -**Файл**: `src/mem/pm_router.rs` - -Глобальная ткань маршрутизации: 65536 lock-free каналов. - -### Константы - -- `CHANNEL_COUNT = 65536` -- Состояния: `STATE_FREE = 0`, `STATE_PENDING = 1`, `STATE_READY = 2` - -### Channel — `#[repr(align(64))]` - -```rust -struct Channel { - state: AtomicU8, - next_free: AtomicU16, - result: UnsafeCell>, -} -``` - -### PMRouter - -```rust -pub struct PMRouter { - channels: Box<[Channel]>, // 65536 каналов - free_head: AtomicU16, // голова free list (0 = sentinel) -} -``` - -### GlobalRouter - -```rust -static ROUTER: GlobalRouter; // синглтон -``` - -### `init()` - -1. Создаёт Vec из 65536 каналов, соединённых в linked list (каждый `next_free` указывает на следующий) -2. Преобразует в `Box<[Channel]>` -3. Сохраняет в `ROUTER.inner`, устанавливает `is_ready` - -### `alloc_channel() -> Option` - -CAS-цикл на `free_head`: извлекает голову списка, устанавливает `state = PENDING`. - -### `route_responses(responses: Vec)` - -Для каждого PMResponse с ненулевым `channel_id`: сохраняет `result` в канале, устанавливает `state = READY`. - -### `wait_for_response(id: u16) -> PMResult` - -Spin-wait до `state == READY`. Забирает результат, пушит канал обратно в freelist, возвращает результат. - -### `request_and_wait(actor, req_builder) -> PMResult` - -Convenience: аллоцирует канал, строит запрос с channel_id, отправляет актору, ждёт ответ. (process_messages должен быть вызван внешне.) - -### `dispatch(responses: Vec)` - -Точка входа для маршрутизации ответов через `PMActor::route_responses()` (вызов на глобальном роутере). diff --git a/kernel/docs/tty.md b/kernel/docs/tty.md new file mode 100644 index 0000000..a2ddc51 --- /dev/null +++ b/kernel/docs/tty.md @@ -0,0 +1,150 @@ +# Framebuffer Console: `tty.rs` + +## Назначение + +Драйвер графического вывода текста на framebuffer. Использует +PSF2 (PC Screen Font 2) шрифты для отрисовки символов. + +Является основным визуальным каналом вывода ядра (через info! макрос). + +## Структура Console + +```rust +pub struct Console<'a> { + framebuffer: &'a Framebuffer<'a>, // ссылка на Limine framebuffer + font: &'static [u8], // PSF2 шрифт + pub x: usize, // текущая позиция X (колонка) + pub y: usize, // текущая позиция Y (строка) + pub fg_color: u32, // цвет текста (RGB) + pub bg_color: u32, // цвет фона (RGB) +} +``` + +## PSF2 Header + +```rust +#[repr(C, packed)] +struct Psf2Header { + magic: u32, // 0x864AB572 (PSF2 magic) + version: u32, + header_size: u32, // размер заголовка + flags: u32, + num_glyphs: u32, // количество глифов + bytes_per_glyph: u32, // байт на глиф + height: u32, // высота глифа в пикселях + width: u32, // ширина глифа в пикселях +} +``` + +Шрифт — `font.psf` (8×16, встроен через `include_bytes!`). + +## Операции + +### clear() + +Заполняет весь framebuffer нулями. Сбрасывает позицию курсора в (0, 0). + +```rust +pub fn clear(&mut self) { + unsafe { + core::ptr::write_bytes(fb.addr(), 0, (fb.pitch() * fb.height()) as usize); + } + self.x = 0; + self.y = 0; +} +``` + +### scroll() + +Сдвигает содержимое framebuffer вверх на высоту шрифта. +Новые строки заполняются нулями. + +```rust +fn scroll(&mut self) { + let shift = font_height * pitch; // смещение на 1 строку + let size = pitch * (height - font_height); // сколько копировать + unsafe { + ptr::copy(addr.add(shift), addr, size); + ptr::write_bytes(addr.add(size), 0, shift); + } + self.y -= font_height; +} +``` + +### draw_glyph(glyph_index, x, y) + +Рисует глиф (символ) в заданной позиции. + +```rust +fn draw_glyph(&mut self, glyph_index: u32, x: usize, y: usize) { + let bytes_per_line = (width + 7) / 8; // пиксели → байты + let glyph_offset = header_size + (glyph_index * bytes_per_glyph); + + for cy in 0..height { + let glyph_row = self.font[glyph_offset + cy * bytes_per_line]; + for cx in 0..width { + if (glyph_row & (0x80 >> cx)) != 0 { + // Пиксель включён — рисуем fg_color + let offset = ((y + cy) * fb_pitch) + ((x + cx) * 4); + unsafe { + fb_addr.add(offset).cast::().write_volatile(self.fg_color); + } + } + // Если пиксель выключен — фон остаётся (нет очистки) + } + } +} +``` + +- `width=8`, `bytes_per_line=1`: каждый ряд глифа = 1 байт. +- `(0x80 >> cx)`: проверка бита слева направо (MSB first). +- `write_volatile`: запрещает компилятору оптимизировать запись в MMIO. + +### write_char(c) + +Печатает символ: +- `\n` → переход на новую строку. +- Другие символы → draw_glyph. Если символ > num_glyphs → глиф 0. +- Перенос строки по ширине экрана. +- Скроллинг при достижении низа. + +## fmt::Write реализация + +```rust +impl fmt::Write for Console<'_> { + fn write_str(&mut self, s: &str) -> fmt::Result { + for c in s.chars() { + self.write_char(c); + } + Ok(()) + } +} +``` + +## Использование в kmain() + +```rust +let fb = fb_res.framebuffers().next().expect("No framebuffer"); +let mut console = tty::Console::new(&fb, KERNEL_FONT); +console.clear(); + +info!(console, "BOOT", "LIS4 Kernel Starting..."); +// ... остальные логи + +let _ = writeln!(console, "{}", logo); // ASCII art при загрузке +``` + +## Детали реализации + +- **Цвета**: write_volatile для 32-битного пикселя. Формат — RGBA? + (зависит от режима framebuffer). +- **Фон**: не перерисовывается. Только пиксели текста. + Для полной очистки — clear(). +- **PSF2 magic**: `0x864AB572` — проверяется в заголовке. +- **Производительность**: draw_glyph простая, без буферизации. + Для скроллинга — memcpy всего framebuffer. + +## Зависимости + +- `KERNEL_FONT: &[u8]` — встроен в main.rs через `include_bytes!("font.psf")`. +- `limine::framebuffer::Framebuffer` — через HHDM отображённый framebuffer. diff --git a/kernel/docs/vmm.md b/kernel/docs/vmm.md deleted file mode 100644 index b73200e..0000000 --- a/kernel/docs/vmm.md +++ /dev/null @@ -1,212 +0,0 @@ -# Виртуальная память (VMM) - -**Файл**: `src/mem/vmm.rs` - -Управление адресными пространствами: PML4 + VMA списки, lazy demand paging, copy-on-write, shared mappings, ASID/PCID, TLB shootdown IPI. - ---- - -## Глобальные статики - -| Переменная | Тип | Назначение | -|---|---|---| -| `ACTIVE_CPUS_MASK` | `AtomicU64` | Битовая маска активных CPU (core 0 по умолчанию) | -| `SHOOTDOWN_LOCK` | `Locked<()>` | Блокировка TLB shootdown IPI | -| `SHOOTDOWN_ASID` | `AtomicU16` | ASID, подлежащий сбросу | -| `SHOOTDOWN_ACK` | `AtomicU64` | Битовая маска подтверждений удалённых ядер | -| `INVPCID_SUPPORTED` | `AtomicBool` | Поддержка инструкции INVPCID | -| `ASID_ALLOC` | `Locked` | Аллокатор ASID/PCID | -| `KERNEL_SPACE` | `Locked>` | Адресное пространство ядра | - ---- - -## Типы ошибок: `VmError` - -- `OutOfMemory` — нет свободной физической памяти -- `RegionOverlap` — VMA перекрывается с существующей -- `RegionNotFound` — VMA не найдена -- `InvalidAlignment` — неверное выравнивание -- `InvalidRange` — некорректный диапазон -- `PermissionDenied` — нет прав доступа -- `UnexpectedFault` — page fault в не-lazy, не-COW регионе -- `NonCanonical` — неканонический адрес -- `AsidExhausted` — кончились ASID - ---- - -## `VmaBacking` — типы подложки - -```rust -pub enum VmaBacking { - Anonymous(Vec>), // per-page tracking: None = ещё не выделена - Physical(PhysAddr), // фиксированный физический диапазон (MMIO, DMA) - Shared { owner_cap: u64, phys_base: PhysAddr }, // zero-copy borrow -} -``` - ---- - -## `VmaFlags` (bitflags) - -| Флаг | Бит | Описание | -|---|---|---| -| `READ` | 0 | Чтение | -| `WRITE` | 1 | Запись | -| `EXEC` | 2 | Исполнение | -| `USER` | 3 | Доступ из userspace | -| `LAZY` | 4 | Demand paging (фрейм при page fault) | -| `SHARED` | 5 | Разделяемая память | -| `PINNED` | 6 | Защита от revoke | -| `NOCACHE` | 7 | Отключение кэширования | -| `MMIO` | 8 | Memory-mapped I/O | -| `COW` | 9 | Copy-on-Write | - -**`to_page_flags() -> PageTableFlags`**: преобразует VmaFlags в аппаратные флаги. PRESENT всегда включён, NO_EXECUTE если не EXEC. - ---- - -## `AsidAllocator` - -Битовая карта на 4096 ASID/PCID (128 × u32). - -- ASID 0 (ядро) и ASID 4095 (зарезервирован) — заняты постоянно -- `alloc() -> Option` — двухпроходный с пропуском целых слов (амортизированное O(1)) -- `free(asid)` — сброс бита - ---- - -## `VmaRegion` - -```rust -pub struct VmaRegion { - virt_start: VirtAddr, - virt_end: VirtAddr, - flags: VmaFlags, - cap_token: u64, - backing: VmaBacking, -} -``` - -Методы: `contains(addr)`, `size()`, `pages()`. - ---- - -## `AddressSpace` - -```rust -pub struct AddressSpace { - asid: u16, - pml4_phys: PhysAddr, - regions: Vec, // sorted by virt_start, non-overlapping - hhdm: u64, -} -``` - -### Создание - -| Метод | Описание | -|---|---| -| `new(hhdm)` | Выделяет PML4 (занулённый), аллоцирует ASID | -| `from_active(pml4_phys, asid, hhdm)` | Обёртка для существующей PML4 (ядро, ASID=0) | - -### `map_region(virt, phys, size, flags, cap_token) -> Result` - -1. Валидация выравнивания (4 KiB) и проверка перекрытий (`check_overlap`) -2. Создание `VmaBacking`: - - **LAZY** → `Anonymous(vec![None; page_count])` - - **Physical** (если `phys` не None и нет LAZY) → `Physical(phys)` - - **Eager anonymous**: цикл по страницам, `pmm_alloc() + zero + map` -3. Вставка в `regions` с сохранением сортировки (`insert_sorted`) - -### `map_shared(virt, phys_base, page_count, flags, owner_cap)` - -Eager маппинг физического диапазона без владения фреймами (не освобождаются при unmap). - -### `handle_fault(fault_addr, write) -> Result<(), VmError>` - -Обработка page fault: - -1. `find_idx(fault_addr)` — бинарный поиск региона -2. Проверка прав: write fault в WRITE-регионе -3. **COW**: если флаг COW и пишем: - - Выделяет новый фрейм - - Копирует содержимое старого - - Обновляет PTE: убирает COW, добавляет WRITABLE -4. **LAZY**: если флаг LAZY: - - Выделяет фрейм, зануляет, маппит - - Обновляет `Vec>` -5. Иначе: `Err(UnexpectedFault)` - -### `unmap_region(virt) -> Result<(), VmError>` - -1. `find_idx(virt)` → регион -2. `do_unmap(region)`: анмаппит PTE, освобождает фреймы если Anonymous -3. Удаляет регион из списка -4. `tlb_flush_asid(self.asid)` - -### `revoke_by_token(cap_token)` - -Анмаппит все VMA с совпадающим `cap_token` (кроме PINNED). Сливает TLB. - -### `process_pending_revocations(&mut self)` - -1. Дренирует `MMU_REVOCATION_QUEUE` (pop в цикле) -2. Для каждого токена → `revoke_by_token(token)` -3. TLB flush если была хотя бы одна ревокация - -### `clone_for_fork(child_cap_token) -> Result` - -Создание дочернего адресного пространства (fork): - -- **Physical/Shared**: маппинг напрямую -- **Anonymous**: **COW setup**: - - Убирает WRITABLE из PTE родителя - - Устанавливает COW флаг в обоих PTEs - - `inc_ref_frame()` для всех фреймов - -### `translate(virt) -> Option` - -Аппаратный walk страничной таблицы (через `PageTable::translate`). - -### `activate()` - -`unsafe`: запись CR3 = `pml4_phys | asid | (1<<63)`. Бит 63 = NOFLUSH — сохраняет PCID-тегированные записи TLB. - -### `Drop` - -1. Анмаппит все регионы -2. Освобождает PML4 фрейм через `free_frame` -3. Возвращает ASID в пул - ---- - -## TLB Management - -### `local_tlb_flush_asid(asid)` - -- INVPCID type 1 если поддерживается (single-context flush) -- Иначе `tlb_flush_all()` - -### `tlb_flush_asid(asid)` - -1. `local_tlb_flush_asid(asid)` — локальный сброс -2. Устанавливает глобальные статики: `SHOOTDOWN_ASID = asid`, `SHOOTDOWN_ACK = 0` -3. `broadcast_ipi_exclude_self(TLB_SHOOTDOWN_VECTOR)` — IPI всем остальным ядрам -4. Spin-loop: `while SHOOTDOWN_ACK != ACTIVE_CPUS_MASK { hlt(); }` - -### `tlb_flush_all()` - -Читает CR3 → перезаписывает без бита 63 → полный сброс всех non-global entries. - -### `handle_tlb_shootdown_ipi()` - -1. `local_tlb_flush_asid(SHOOTDOWN_ASID.load())` -2. Устанавливает свой бит в `SHOOTDOWN_ACK` (через `fetch_or`) - -### `init_cpu_features()` - -CPUID leaf 7, проверка EBX[10] (INVPCID). Сохраняет в `INVPCID_SUPPORTED`. - -### `init_kernel_space(pml4_phys, hhdm)` - -Регистрирует PML4 ядра как `KERNEL_SPACE` с ASID 0. diff --git a/kernel/src/cpu/lapic.rs b/kernel/src/cpu/lapic.rs index 0c0a084..2278699 100644 --- a/kernel/src/cpu/lapic.rs +++ b/kernel/src/cpu/lapic.rs @@ -1,6 +1,7 @@ // src/cpu/lapic.rs use core::sync::atomic::{AtomicU64, Ordering}; +use crate::mem::address::get_hhdm; pub const LAPIC_DEFAULT_BASE: u64 = 0xFEE00_000; const LAPIC_EOI: u64 = 0x0B0; @@ -8,8 +9,8 @@ const LAPIC_ICR_LOW: u64 = 0x300; static LAPIC_VIRT_BASE: AtomicU64 = AtomicU64::new(0); -pub fn init(hhdm_offset: u64) { - LAPIC_VIRT_BASE.store(LAPIC_DEFAULT_BASE + hhdm_offset, Ordering::SeqCst); +pub fn init() { + LAPIC_VIRT_BASE.store(LAPIC_DEFAULT_BASE + get_hhdm(), Ordering::SeqCst); } #[inline(always)] diff --git a/kernel/src/main.rs b/kernel/src/main.rs index 2e9fcce..07651fa 100644 --- a/kernel/src/main.rs +++ b/kernel/src/main.rs @@ -14,6 +14,7 @@ use crate::mem::paging::{PageTable, PageTableFlags}; use crate::mem::address::{PhysAddr, VirtAddr}; use crate::cap::{Relation, CapRights, Capability, CapObject}; use crate::mem::pm_manages::{PMActor, PMRequest, PMResult}; +use crate::mem::buddy::BuddyAllocator; pub mod cap; pub mod cpu; @@ -175,6 +176,7 @@ unsafe extern "C" fn kmain() -> ! { let hhdm_res = HHDM_REQUEST.get_response().expect("Limine: No HHDM"); let kaddr_res = KERNEL_ADDR_REQUEST.get_response().expect("Limine: No Kernel Address"); let hhdm_offset = hhdm_res.offset(); + crate::mem::address::init_hhdm(hhdm_offset); let fb = fb_res.framebuffers().next().expect("Limine: No active framebuffer found"); let mut console = tty::Console::new(&fb, KERNEL_FONT); @@ -185,17 +187,17 @@ unsafe extern "C" fn kmain() -> ! { cpu::interrupts::init_early_exceptions(); info!(console, "BOOT", "LIS4 Kernel Starting..."); - unsafe { mem::pmm::BitmapPMM::init(&mmap_res, hhdm_offset); } + unsafe { mem::pmm::BitmapPMM::init(&mmap_res); } info!(console, "MEM", "Primary Physical Memory Manager (BitmapPMM) initialized."); - cpu::lapic::init(hhdm_offset); + cpu::lapic::init(); info!(console, "LAPIC", "Local APIC initialized."); // debug: locate the free page info!(console, "BOOT", "alloc_frame..."); let p4_phys = mem::pmm::alloc_frame().expect("OOM: Failed to allocate P4 table"); info!(console, "BOOT", "alloc_frame ok: phys=0x{:x}", p4_phys.0); - let virt = p4_phys.to_virt(hhdm_offset); + let virt = p4_phys.to_virt(); info!(console, "BOOT", "virt=0x{:x}", virt.0); let p4 = unsafe { &mut *virt.as_mut_ptr::() }; info!(console, "BOOT", "zeroing page..."); @@ -206,10 +208,10 @@ unsafe extern "C" fn kmain() -> ! { for (i, entry) in mmap_res.entries().iter().enumerate() { let phys = PhysAddr(entry.base); - let virt_hhdm = phys.to_virt(hhdm_offset); - p4.map_region(virt_hhdm, phys, entry.length, flags, hhdm_offset); + let virt_hhdm = phys.to_virt(); + p4.map_region(virt_hhdm, phys, entry.length, flags); if entry.entry_type != limine::memory_map::EntryType::RESERVED { - p4.map_region(VirtAddr(entry.base), phys, entry.length, flags, hhdm_offset); + p4.map_region(VirtAddr(entry.base), phys, entry.length, flags); } } @@ -218,7 +220,6 @@ unsafe extern "C" fn kmain() -> ! { PhysAddr(kaddr_res.physical_base()), 0x1000 * 1024, flags, - hhdm_offset ); info!(console, "MMU", "Activating Kernel Page Tables..."); @@ -228,7 +229,7 @@ unsafe extern "C" fn kmain() -> ! { let heap_size = 8 * 1024 * 1024; for i in (0..heap_size).step_by(4096) { let frame = mem::pmm::alloc_frame().expect("OOM: Heap allocation failed"); - p4.map_page(VirtAddr(heap_start + i as u64), frame, flags, hhdm_offset); + p4.map_page(VirtAddr(heap_start + i as u64), frame, flags); } { @@ -238,7 +239,7 @@ unsafe extern "C" fn kmain() -> ! { info!(console, "HEAP", "Kernel Slab Allocator is online."); mem::init_cpu_features(); - mem::vmm::init_kernel_space(p4_phys, hhdm_offset); + mem::vmm::init_kernel_space(p4_phys); info!(console, "VMM", "Kernel Address Space registered successfully."); cpu::interrupts::init_idt(); @@ -367,6 +368,74 @@ unsafe extern "C" fn kmain() -> ! { panic!("CRITICAL STATE LOSS: Memory leak detected inside PMActor context!"); } + // === ARCH 2.2: Buddy Allocator Intrusive List — Direct Test === + info!(console, "PM", "=-= Buddy Allocator Direct Test (Intrusive List) =-="); + + let buddy_test_pages = 128; + let buddy_test_base = PhysAddr(0x5000_0000); + let mut buddy = BuddyAllocator::new(buddy_test_pages, buddy_test_base.0); + + let b1 = buddy.alloc(0).expect("buddy: alloc order 0"); + let b2 = buddy.alloc(0).expect("buddy: alloc order 0 #2"); + buddy.free(b1, 0); + buddy.free(b2, 0); + assert_eq!(buddy.free_pages(), buddy_test_pages, "buddy: pages not fully recovered after simple alloc/free"); + info!(console, "PM", " [OK] Simple alloc/free"); + + let b3 = buddy.alloc(1).expect("buddy: alloc order 1"); + assert_eq!(b3, 0, "buddy: first order 1 block at 0"); + let b4 = buddy.alloc(1).expect("buddy: alloc order 1 #2"); + assert_eq!(b4, 2, "buddy: second order 1 block at 2"); + buddy.free(b3, 1); + buddy.free(b4, 1); + let b5 = buddy.alloc(2).expect("buddy: order 2 after coalescing"); + assert_eq!(b5, 0, "buddy: coalesced block at 0"); + buddy.free(b5, 2); + info!(console, "PM", " [OK] Coalescing across orders (O(1) intrusive list)"); + + let mut allocs = Vec::new(); + loop { + match buddy.alloc(0) { + Some(idx) => allocs.push(idx), + None => break, + } + } + assert!(buddy.is_exhausted(), "buddy: should be exhausted"); + info!(console, "PM", " [OK] Exhaustion after {} allocs", allocs.len()); + + for (i, idx) in allocs.iter().enumerate() { + buddy.free(*idx, 0); + } + assert_eq!(buddy.free_pages(), buddy_test_pages, "buddy: not fully recovered"); + info!(console, "PM", " [OK] Full recovery after freeing {} pages", allocs.len()); + + info!(console, "PM", "~) BUDDY DIRECT TEST PASSED (~"); + + // === ARCH 2.3: PMM Tree Bitmap — alloc/free stress test === + info!(console, "PM", "=-= PMM Tree Bitmap Alloc/Free Stress Test =-="); + + const PMM_TEST_COUNT: usize = 64; + let mut frames = [PhysAddr(0); PMM_TEST_COUNT]; + for i in 0..PMM_TEST_COUNT { + frames[i] = mem::pmm::alloc_frame().expect("PMM tree: OOM during alloc test"); + } + info!(console, "PM", " [OK] Allocated {} frames", PMM_TEST_COUNT); + + for f in &frames { + mem::pmm::free_frame(*f); + } + info!(console, "PM", " [OK] Freed {} frames back", PMM_TEST_COUNT); + + for i in 0..PMM_TEST_COUNT { + frames[i] = mem::pmm::alloc_frame().expect("PMM tree: OOM after free cycle"); + } + info!(console, "PM", " [OK] Re-allocated {} frames after free cycle", PMM_TEST_COUNT); + + for f in &frames { + mem::pmm::free_frame(*f); + } + info!(console, "PM", "~) PMM TREE BITMAP TEST PASSED (~"); + let logo = r#" ########### ################## diff --git a/kernel/src/mem/address.rs b/kernel/src/mem/address.rs index 8a7675f..190088f 100644 --- a/kernel/src/mem/address.rs +++ b/kernel/src/mem/address.rs @@ -1,3 +1,19 @@ +use core::sync::atomic::{AtomicU64, Ordering}; + +static HHDM_OFFSET: AtomicU64 = AtomicU64::new(0); + +/// Store the Higher-Half Direct Map offset obtained from the bootloader. +/// Must be called once during early boot, before any address translation. +pub fn init_hhdm(offset: u64) { + HHDM_OFFSET.store(offset, Ordering::Release); +} + +/// Return the HHDM offset (kernel virtual base for physical memory). +#[inline] +pub fn get_hhdm() -> u64 { + HHDM_OFFSET.load(Ordering::Relaxed) +} + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] #[repr(transparent)] pub struct PhysAddr(pub u64); @@ -8,8 +24,8 @@ pub struct VirtAddr(pub u64); impl PhysAddr { /// Convert physical address to virtual via HHDM offset - pub fn to_virt(self, hhdm_offset: u64) -> VirtAddr { - VirtAddr(self.0 + hhdm_offset) + pub fn to_virt(self) -> VirtAddr { + VirtAddr(self.0 + get_hhdm()) } #[allow(dead_code)] @@ -24,9 +40,10 @@ impl PhysAddr { impl VirtAddr { #[allow(dead_code)] - pub fn to_phys(self, hhdm_offset: u64) -> Option { - if self.0 < hhdm_offset { return None; } - Some(PhysAddr(self.0 - hhdm_offset)) + pub fn to_phys(self) -> Option { + let hhdm = get_hhdm(); + if self.0 < hhdm { return None; } + Some(PhysAddr(self.0 - hhdm)) } #[allow(dead_code)] diff --git a/kernel/src/mem/buddy.rs b/kernel/src/mem/buddy.rs index 88fc44a..d4a40f7 100644 --- a/kernel/src/mem/buddy.rs +++ b/kernel/src/mem/buddy.rs @@ -1,105 +1,99 @@ -//! # Per-Actor Buddy Allocator (`src/mem/buddy.rs`) -//! -//! A power-of-two page-block allocator for a fixed, pre-committed physical -//! region. Designed for **single-consumer** use inside a [`crate::mem::pm_manages::PMActor`]: -//! the owning actor is the only entity that ever mutates its buddy allocator, so -//! no locking is needed — the entire subsystem is inherently lock-free from the -//! consumer's perspective. -//! -//! ## Block Layout -//! -//! The region is subdivided into blocks of `2^order` pages (order 0 … MAX_ORDER). -//! Every block at order *k* is aligned to `2^k` pages within the region. -//! -//! ```text -//! order 0 → 1 page = 4 KiB -//! order 1 → 2 pages = 8 KiB -//! … -//! order 11 → 2 048 pages = 8 MiB (MAX_ORDER) -//! ``` -//! -//! ## Complexity -//! -//! | Operation | Amortised | Worst-case | -//! |-----------|-----------|------------| -//! | `alloc` | O(log N) | O(MAX_ORDER · Lₖ) | -//! | `free` | O(log N) | O(MAX_ORDER · Lₖ) | -//! -//! where Lₖ = `free_lists[k].len()` ≤ `total_pages / 2^k`. -//! -//! ## Production note -//! -//! For managed ranges > 1 GiB (> 256 K pages), replace the per-order `Vec` -//! free lists with a radix tree or interval tree to bound Lₖ. For current -//! PMActor capital sizes (typically ≤ 128 MiB = 32 K pages at order 0), the -//! `Vec`-based implementation is fully adequate. +use crate::mem::address::{PhysAddr, VirtAddr}; -extern crate alloc; -use alloc::vec::Vec; - -// Constants & helpers -/// Maximum allocation order. -/// `2^11 × 4 096 bytes = 8 MiB` per single allocation. pub const MAX_ORDER: usize = 11; -/// ⌈log2(n)⌉ — the minimum order whose block size covers `page_count` pages. -/// -/// ```text -/// order_for(1) = 0 (2^0 = 1) -/// order_for(2) = 1 (2^1 = 2) -/// order_for(3) = 2 (2^2 = 4 ≥ 3) -/// order_for(2048) = 11 -/// ``` +const NEXT_SENTINEL: usize = usize::MAX; + #[inline] pub fn order_for(page_count: usize) -> usize { if page_count <= 1 { 0 } else { - // Number of bits needed to represent (page_count - 1). usize::BITS as usize - (page_count - 1).leading_zeros() as usize } } -// BuddyAllocator -/// Buddy allocator over a contiguous, pre-committed range of physical pages. -/// -/// All page indices stored in free lists are **relative to the start of the -/// managed range**. The owner (`PMActor`) translates to absolute `PhysAddr` -/// by adding `managed_range.0`. -/// -/// # Invariants -/// - `free_pages ≤ total_pages` at all times. -/// - Every block in `free_lists[k]` is aligned to `2^k` pages (i.e. -/// `idx % (1 << k) == 0`). -/// - No block appears in more than one order's free list simultaneously. pub struct BuddyAllocator { - /// `free_lists[k]` = relative page indices of free 2^k-page blocks. - /// Ordering within each list is irrelevant; `pop()` / `swap_remove()` are used. - free_lists: [Vec; MAX_ORDER + 1], - /// Total pages in the managed range (need not be a power of two). + free_heads: [usize; MAX_ORDER + 1], total_pages: usize, - /// Running count of free pages. Always equals `Σ (2^k × free_lists[k].len())`. free_pages: usize, + base_phys: u64, } impl BuddyAllocator { - //Construction - /// Create a new allocator over `total_pages` pages, **all initially free**. - /// - /// Uses a greedy largest-first decomposition to build the initial free lists - /// in O(total_pages / 2^MAX_ORDER) iterations — essentially O(1) for - /// power-of-two sizes. - /// - /// Example: 7 pages → blocks [4, 2, 1] → free_lists[2]=[0], [1]=[4], [0]=[6]. - pub fn new(total_pages: usize) -> Self { - // core::array::from_fn is the idiomatic way to init a non-Copy array. - let free_lists: [Vec; MAX_ORDER + 1] = - core::array::from_fn(|_| Vec::new()); + #[inline] + fn page_virt(&self, idx: usize) -> VirtAddr { + PhysAddr(self.base_phys + idx as u64 * 4096).to_virt() + } + unsafe fn write_node(&self, idx: usize, next: usize, prev: usize) { + unsafe { + let ptr = self.page_virt(idx).as_mut_ptr::(); + ptr.write(next); + ptr.add(1).write(prev); + } + } + + unsafe fn read_next(&self, idx: usize) -> usize { + unsafe { self.page_virt(idx).as_ptr::().read() } + } + + unsafe fn read_prev(&self, idx: usize) -> usize { + unsafe { self.page_virt(idx).as_ptr::().add(1).read() } + } + + fn flist_push(&mut self, order: usize, idx: usize) { + let head = self.free_heads[order]; + unsafe { + self.write_node(idx, head, NEXT_SENTINEL); + if head != NEXT_SENTINEL { + self.write_node(head, self.read_next(head), idx); + } + } + self.free_heads[order] = idx; + } + + fn flist_remove(&mut self, order: usize, idx: usize) { + unsafe { + let next = self.read_next(idx); + let prev = self.read_prev(idx); + + self.write_node(idx, NEXT_SENTINEL, NEXT_SENTINEL); + + if prev != NEXT_SENTINEL { + self.write_node(prev, next, self.read_prev(prev)); + } else { + self.free_heads[order] = next; + } + + if next != NEXT_SENTINEL { + self.write_node(next, self.read_next(next), prev); + } + } + } + + fn flist_pop(&mut self, order: usize) -> Option { + let head = self.free_heads[order]; + if head == NEXT_SENTINEL { + return None; + } + self.flist_remove(order, head); + Some(head) + } + + fn flist_contains(&self, order: usize, idx: usize) -> bool { + if self.free_heads[order] == idx { + return true; + } + unsafe { self.read_prev(idx) != NEXT_SENTINEL } + } + + pub fn new(total_pages: usize, base_phys: u64) -> Self { let mut this = Self { - free_lists, + free_heads: [NEXT_SENTINEL; MAX_ORDER + 1], total_pages, free_pages: 0, + base_phys, }; if total_pages == 0 { @@ -110,22 +104,19 @@ impl BuddyAllocator { while idx < total_pages { let remaining = total_pages - idx; - // Alignment constraint: block at `idx` must be aligned to 2^order. - // trailing_zeros(0) is u32::MAX, so we clamp to MAX_ORDER. let align_order = if idx == 0 { MAX_ORDER } else { (idx.trailing_zeros() as usize).min(MAX_ORDER) }; - // Size constraint: 2^order ≤ remaining → order ≤ ⌊log₂(remaining)⌋. let size_order = (usize::BITS as usize - 1) - - remaining.leading_zeros() as usize; // ⌊log2(remaining)⌋ + - remaining.leading_zeros() as usize; - let order = MAX_ORDER.min(align_order).min(size_order); + let order = MAX_ORDER.min(align_order).min(size_order); let block_size = 1usize << order; - this.free_lists[order].push(idx); + this.flist_push(order, idx); this.free_pages += block_size; idx += block_size; } @@ -133,60 +124,30 @@ impl BuddyAllocator { this } - //Allocation - /// Allocate a 2^`order`-page block. - /// - /// Returns the **relative** page index of the block's first page, or `None` - /// if insufficient contiguous memory remains. - /// - /// The returned index can be converted to a physical address: - /// ```text - /// phys = actor.managed_range.0.0 + (idx as u64) * PAGE_SIZE - /// ``` pub fn alloc(&mut self, order: usize) -> Option { if order > MAX_ORDER { return None; } - // Find the smallest available order ≥ requested. let found_order = (order..=MAX_ORDER) - .find(|&o| !self.free_lists[o].is_empty())?; + .find(|&o| self.free_heads[o] != NEXT_SENTINEL)?; - // Consume one block from the found order. - let block_idx = self.free_lists[found_order] - .pop() - .expect("buddy: free_list non-empty but pop() returned None"); + let block_idx = self.flist_pop(found_order) + .expect("buddy: free_list non-empty but flist_pop returned None"); - // Debit: we removed a 2^found_order block from free. self.free_pages -= 1 << found_order; - // Split down to the requested order, putting buddies back into free lists. - // - // Invariant at each iteration: - // `block_idx` is the lower half of a 2^cur_order block. - // The upper half (= `block_idx + 2^(cur_order-1)`) is returned to the - // free list as an order-(cur_order-1) block. let mut cur_order = found_order; while cur_order > order { cur_order -= 1; let buddy_idx = block_idx + (1 << cur_order); - self.free_lists[cur_order].push(buddy_idx); + self.flist_push(cur_order, buddy_idx); self.free_pages += 1 << cur_order; } - // Net accounting: removed 2^found_order, added (2^found_order − 2^order). - // Result: free_pages decreased by exactly 2^order. ✓ Some(block_idx) } - /// Allocate `page_count` pages, rounding up to the nearest power of two. - /// - /// Returns `(relative_page_idx, actual_order)`. The caller **must** pass the - /// same `order` to [`BuddyAllocator::free`] — mismatched orders corrupt the - /// allocator. - /// - /// The wasted "rounding up" pages remain unusable until the block is freed. - /// For tightly-packed allocations, callers should use `alloc(order)` directly. pub fn alloc_pages(&mut self, page_count: usize) -> Option<(usize, usize)> { if page_count == 0 { return None; @@ -198,17 +159,6 @@ impl BuddyAllocator { self.alloc(order).map(|idx| (idx, order)) } - //Deallocation - - /// Return a 2^`order`-page block at **relative** index `block_idx` to the - /// free pool, coalescing with free buddies up the order chain. - /// - /// # Panics (debug builds only) - /// - `order > MAX_ORDER` - /// - `block_idx + 2^order > total_pages` - /// - /// In release builds the checks are elided for performance; passing incorrect - /// arguments causes undefined bookkeeping, not UB (no unsafe indexing). pub fn free(&mut self, mut block_idx: usize, mut order: usize) { debug_assert!( order <= MAX_ORDER, @@ -224,67 +174,60 @@ impl BuddyAllocator { self.total_pages ); - // Try to coalesce with our buddy at each order. - // - // The buddy of a block at relative index `i` of order `k` is at: - // buddy_idx = i XOR 2^k - // This works because aligned buddy pairs always differ in exactly bit k. while order < MAX_ORDER { let buddy_idx = block_idx ^ (1 << order); - // Buddy must lie entirely within the managed range. let buddy_end = match buddy_idx.checked_add(1 << order) { Some(e) => e, - None => break, + None => break, }; if buddy_end > self.total_pages { break; } - // Search for the buddy in the free list for this order. - // `swap_remove` is O(1) and order-preserving is not required. - if let Some(pos) = self.free_lists[order].iter().position(|&b| b == buddy_idx) { - self.free_lists[order].swap_remove(pos); - self.free_pages -= 1 << order; // buddy was in free count; remove it - - // Merged block starts at the lower address of the two. + if self.flist_contains(order, buddy_idx) { + self.flist_remove(order, buddy_idx); + self.free_pages -= 1 << order; block_idx = block_idx.min(buddy_idx); order += 1; - // Continue trying to merge at the next level. } else { - break; // buddy is allocated or out-of-range; stop coalescing + break; } } - // Push the (possibly merged) block onto the appropriate free list. self.free_pages += 1 << order; - self.free_lists[order].push(block_idx); + self.flist_push(order, block_idx); } - //Introspection - - /// Number of pages currently available for allocation. #[inline] pub fn free_pages(&self) -> usize { self.free_pages } - /// Total pages in the managed range. #[inline] pub fn total_pages(&self) -> usize { self.total_pages } - /// `true` if the allocator has no free pages. #[inline] pub fn is_exhausted(&self) -> bool { self.free_pages == 0 } - /// Dump free-list statistics for each order (useful in panic handlers). - /// - /// Returns an array `[(order, free_block_count); MAX_ORDER + 1]`. pub fn stats(&self) -> [(usize, usize); MAX_ORDER + 1] { - core::array::from_fn(|o| (o, self.free_lists[o].len())) + core::array::from_fn(|o| { + let count = if self.free_heads[o] == NEXT_SENTINEL { + 0 + } else { + let mut cnt = 0; + let mut cur = self.free_heads[o]; + while cur != NEXT_SENTINEL { + cnt += 1; + cur = unsafe { self.read_next(cur) }; + } + cnt + }; + (o, count) + }) } } diff --git a/kernel/src/mem/mod.rs b/kernel/src/mem/mod.rs index 116ffcd..6f5eb8d 100644 --- a/kernel/src/mem/mod.rs +++ b/kernel/src/mem/mod.rs @@ -12,8 +12,8 @@ pub mod pm_router; /// Initialise the physical memory manager. /// /// Must be called before any heap allocation or VMM operation. -pub fn init_pmm(memmap: &limine::response::MemoryMapResponse, hhdm_offset: u64) { - unsafe { pmm::BitmapPMM::init(memmap, hhdm_offset); } +pub fn init_pmm(memmap: &limine::response::MemoryMapResponse) { + unsafe { pmm::BitmapPMM::init(memmap); } } /// Initialise CPU features required by the VMM (INVPCID detection). @@ -25,8 +25,8 @@ pub fn init_cpu_features() { /// Initialise the kernel address space record (after PMM, heap, and the /// initial P4 page table have been set up in `kmain`). -pub fn init_vmm(pml4_phys: address::PhysAddr, hhdm_offset: u64) { - vmm::init_kernel_space(pml4_phys, hhdm_offset); +pub fn init_vmm(pml4_phys: address::PhysAddr) { + vmm::init_kernel_space(pml4_phys); } /// Physical memory statistics: `(used_pages, total_pages)`. diff --git a/kernel/src/mem/paging.rs b/kernel/src/mem/paging.rs index 0e4e189..1bb1a8e 100644 --- a/kernel/src/mem/paging.rs +++ b/kernel/src/mem/paging.rs @@ -40,16 +40,15 @@ impl PageTable { phys: PhysAddr, size: u64, flags: PageTableFlags, - hhdm: u64, ) { let pages = size.div_ceil(4096); for i in 0..pages { let offset = i * 4096; - self.map_page(VirtAddr(virt.0 + offset), PhysAddr(phys.0 + offset), flags, hhdm); + self.map_page(VirtAddr(virt.0 + offset), PhysAddr(phys.0 + offset), flags); } } - pub fn get_flags(&self, virt: VirtAddr, hhdm: u64) -> Option { + pub fn get_flags(&self, virt: VirtAddr) -> Option { 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; @@ -59,7 +58,7 @@ impl PageTable { ($entry:expr) => {{ let e = $entry; if e & PageTableFlags::PRESENT.bits() == 0 { return None; } - unsafe { &*PhysAddr(e & PTE_ADDR_MASK).to_virt(hhdm).as_mut_ptr::() } + unsafe { &*PhysAddr(e & PTE_ADDR_MASK).to_virt().as_mut_ptr::() } }}; } @@ -82,8 +81,8 @@ impl PageTable { Some(PageTableFlags::from_bits_truncate(p1e)) } - pub fn update_flags(&mut self, virt: VirtAddr, flags: PageTableFlags, hhdm: u64) -> Result<(), ()> { - let Some(p1) = self.walk_to_p1_mut(virt, hhdm, false) else { return Err(()); }; + pub fn update_flags(&mut self, virt: VirtAddr, flags: PageTableFlags) -> Result<(), ()> { + let Some(p1) = self.walk_to_p1_mut(virt, false) else { return Err(()); }; let p1_idx = ((virt.0 >> 12) & 0x1FF) as usize; let entry = p1.entries[p1_idx]; @@ -107,10 +106,9 @@ impl PageTable { virt: VirtAddr, phys: PhysAddr, flags: PageTableFlags, - hhdm: u64, ) { let p1 = self - .walk_to_p1_mut(virt, hhdm, true /* create */) + .walk_to_p1_mut(virt, true /* create */) .expect("map_page: OOM allocating intermediate page-table pages"); let p1_idx = ((virt.0 >> 12) & 0x1FF) as usize; @@ -126,8 +124,8 @@ impl PageTable { /// Does **not** free the underlying physical frame — that is the caller's /// responsibility (e.g. `VmaBacking::do_unmap`). /// Silently returns if any level is not present (idempotent). - pub fn unmap_page(&mut self, virt: VirtAddr, hhdm: u64) { - let Some(p1) = self.walk_to_p1_mut(virt, hhdm, false /* no create */) else { + pub fn unmap_page(&mut self, virt: VirtAddr) { + let Some(p1) = self.walk_to_p1_mut(virt, false /* no create */) else { return; // already absent — nothing to do }; let p1_idx = ((virt.0 >> 12) & 0x1FF) as usize; @@ -144,7 +142,7 @@ impl PageTable { /// /// Handles 1 GiB and 2 MiB huge pages transparently. /// Returns `None` if any level is absent or the page is not present. - pub fn translate(&self, virt: VirtAddr, hhdm: u64) -> Option { + pub fn translate(&self, virt: VirtAddr) -> Option { 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; @@ -155,7 +153,7 @@ impl PageTable { ($entry:expr) => {{ let e = $entry; if e & PageTableFlags::PRESENT.bits() == 0 { return None; } - unsafe { &*PhysAddr(e & PTE_ADDR_MASK).to_virt(hhdm).as_mut_ptr::() } + unsafe { &*PhysAddr(e & PTE_ADDR_MASK).to_virt().as_mut_ptr::() } }}; } @@ -209,16 +207,16 @@ impl PageTable { /// /// If `create` is `false` and any intermediate entry is absent, returns `None`. /// If `create` is `true`, allocates missing intermediate pages from the PMM. - fn walk_to_p1_mut(&mut self, virt: VirtAddr, hhdm: u64, create: bool) + 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, hhdm, create)?; - let p2 = p3.get_or_create_next_table(p3_idx, hhdm, create)?; - p2.get_or_create_next_table(p2_idx, hhdm, create) + 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) } /// Return a mutable reference to the next-level table at `index`. @@ -229,7 +227,6 @@ impl PageTable { fn get_or_create_next_table( &mut self, index: usize, - hhdm: u64, create: bool, ) -> Option<&mut Self> { let entry = self.entries[index]; @@ -237,7 +234,7 @@ impl PageTable { if !create { return None; } let pt_phys = pmm_alloc().expect("VMM: OOM allocating page-table page"); - let pt_virt = pt_phys.to_virt(hhdm); + let pt_virt = pt_phys.to_virt(); unsafe { core::ptr::write_bytes(pt_virt.as_mut_ptr::(), 0, 4096); } // Install with USER so both kernel and user pages can live under it; @@ -247,7 +244,7 @@ impl PageTable { } let next_phys = PhysAddr(self.entries[index] & PTE_ADDR_MASK); - Some(unsafe { &mut *next_phys.to_virt(hhdm).as_mut_ptr::() }) + Some(unsafe { &mut *next_phys.to_virt().as_mut_ptr::() }) } } diff --git a/kernel/src/mem/pm_manages.rs b/kernel/src/mem/pm_manages.rs index 90b93a0..2ab65e2 100644 --- a/kernel/src/mem/pm_manages.rs +++ b/kernel/src/mem/pm_manages.rs @@ -279,7 +279,7 @@ impl PMActor { root_untyped: root_cap, managed_range: (start, PhysAddr(start.0 + total_pages as u64 * PAGE_SIZE)), queue: PMActorQueue::new(), - buddy: BuddyAllocator::new(total_pages), + buddy: BuddyAllocator::new(total_pages, start.0), } } diff --git a/kernel/src/mem/pm_router.rs b/kernel/src/mem/pm_router.rs index babf350..a8dad0a 100644 --- a/kernel/src/mem/pm_router.rs +++ b/kernel/src/mem/pm_router.rs @@ -140,9 +140,10 @@ impl PMRouter { let channel = &self.channels[id as usize]; while channel.state.load(Ordering::Acquire) != STATE_READY { - core::hint::spin_loop(); - // TODO: Для "Focus Mode" и полноценного планировщика: - // scheduler::yield_to_actor(); + // HLT-ожидание вместо busy-wait: CPU останавливается до ближайшего + // прерывания (timer tick, IPI от route_responses и т.д.). + // Когда появится планировщик — заменить на yield_to_actor(). + unsafe { core::arch::asm!("hlt", options(nomem, nostack, preserves_flags)); } } let result = unsafe { diff --git a/kernel/src/mem/pmm.rs b/kernel/src/mem/pmm.rs index 0047712..d5c9b3b 100644 --- a/kernel/src/mem/pmm.rs +++ b/kernel/src/mem/pmm.rs @@ -1,19 +1,36 @@ -use crate::mem::address::PhysAddr; +use crate::mem::address::{PhysAddr, get_hhdm}; use crate::mem::allocator::Locked; pub const PAGE_SIZE: u64 = 4096; pub struct BitmapPMM { bitmap: &'static mut [u8], + l1_bitmap: &'static mut [u64], ref_counts: &'static mut [u16], total_pages: usize, used_pages: usize, - last_byte: usize, + last_word: usize, } pub static PMM: Locked> = Locked::new(None); impl BitmapPMM { + unsafe fn read_word(&self, word_idx: usize) -> u64 { + let byte_off = word_idx * 8; + unsafe { (self.bitmap.as_ptr().add(byte_off) as *const u64).read() } + } + + fn l1_update_word(&mut self, word_idx: usize) { + let word = unsafe { self.read_word(word_idx) }; + let l1_idx = word_idx / 64; + let l1_bit = word_idx % 64; + if word != !0u64 { + self.l1_bitmap[l1_idx] |= 1 << l1_bit; + } else { + self.l1_bitmap[l1_idx] &= !(1 << l1_bit); + } + } + #[allow(dead_code)] pub fn used_pages(&self) -> usize { self.used_pages } #[allow(dead_code)] @@ -21,7 +38,7 @@ impl BitmapPMM { #[allow(dead_code)] pub fn free_pages(&self) -> usize { self.total_pages.saturating_sub(self.used_pages) } - pub unsafe fn init(mmap: &limine::response::MemoryMapResponse, hhdm_offset: u64) { + pub unsafe fn init(mmap: &limine::response::MemoryMapResponse) { let max_addr = mmap.entries().iter() .map(|e| e.base + e.length) .max() @@ -31,7 +48,10 @@ impl BitmapPMM { let bitmap_size = total_pages.div_ceil(8); let ref_counts_size = total_pages * core::mem::size_of::(); - let total_meta_size = bitmap_size + ref_counts_size; + let num_words = total_pages.div_ceil(64); + let l1_u64_count = num_words.div_ceil(64); + let l1_byte_size = l1_u64_count * 8; + let total_meta_size = bitmap_size + ref_counts_size + l1_byte_size; let meta_phys = mmap.entries().iter() .find(|e| { @@ -41,8 +61,10 @@ impl BitmapPMM { .map(|e| e.base) .expect("PMM: no usable region large enough for metadata"); - let bitmap_ptr = (meta_phys + hhdm_offset) as *mut u8; - let ref_counts_ptr = (meta_phys + hhdm_offset + bitmap_size as u64) as *mut u16; + let hhdm = get_hhdm(); + let bitmap_ptr = (meta_phys + hhdm) as *mut u8; + let ref_counts_ptr = (meta_phys + hhdm + bitmap_size as u64) as *mut u16; + let l1_ptr = (meta_phys + hhdm + bitmap_size as u64 + ref_counts_size as u64) as *mut u64; let bitmap = unsafe { core::slice::from_raw_parts_mut(bitmap_ptr, bitmap_size) }; bitmap.fill(0xFF); @@ -50,15 +72,18 @@ impl BitmapPMM { let ref_counts = unsafe { core::slice::from_raw_parts_mut(ref_counts_ptr, total_pages) }; ref_counts.fill(1); + let l1_bitmap = unsafe { core::slice::from_raw_parts_mut(l1_ptr, l1_u64_count) }; + l1_bitmap.fill(0); + let mut pmm = Self { bitmap, + l1_bitmap, ref_counts, total_pages, used_pages: total_pages, - last_byte: 0, + last_word: 0, }; - // Free all USABLE pages (gap pages not covered by any entry stay locked). for entry in mmap.entries() { if entry.entry_type == limine::memory_map::EntryType::USABLE { for addr in (entry.base..entry.base + entry.length).step_by(PAGE_SIZE as usize) { @@ -67,7 +92,6 @@ impl BitmapPMM { } } - // Metadata pages sit inside a USABLE region — lock them back. let meta_end = (meta_phys + total_meta_size as u64 + PAGE_SIZE - 1) & !(PAGE_SIZE - 1); for addr in (meta_phys..meta_end).step_by(PAGE_SIZE as usize) { pmm.lock_frame(PhysAddr(addr)); @@ -78,8 +102,6 @@ impl BitmapPMM { *PMM.lock() = Some(pmm); } - //Core operations - pub fn free_frame(&mut self, phys_addr: PhysAddr) { let idx = (phys_addr.0 / PAGE_SIZE) as usize; if idx >= self.total_pages { return; } @@ -92,8 +114,11 @@ impl BitmapPMM { if self.ref_counts[idx] == 0 { self.bitmap[byte] &= !(1 << bit); - self.used_pages -= 1; - if byte < self.last_byte { self.last_byte = byte; } + self.used_pages -= 1; + + let word_idx = idx / 64; + self.l1_update_word(word_idx); + if word_idx < self.last_word { self.last_word = word_idx; } } } } @@ -108,10 +133,13 @@ impl BitmapPMM { if self.bitmap[byte] & (1 << bit) == 0 { self.bitmap[byte] |= 1 << bit; self.ref_counts[idx] = 1; - self.used_pages += 1; + self.used_pages += 1; } else if self.ref_counts[idx] == 0 { self.ref_counts[idx] = 1; } + + let word_idx = idx / 64; + self.l1_update_word(word_idx); } pub fn inc_ref_frame(&mut self, phys_addr: PhysAddr) { @@ -127,31 +155,45 @@ impl BitmapPMM { } pub fn alloc_frame(&mut self) -> Option { - let len = self.bitmap.len(); + let l1_len = self.l1_bitmap.len(); for pass in 0..2usize { let (from, to) = if pass == 0 { - (self.last_byte, len) + (self.last_word / 64, l1_len) } else { - (0, self.last_byte) + (0, self.last_word / 64) }; - for byte_idx in from..to { - if self.bitmap[byte_idx] == 0xFF { continue; } + for l1_idx in from..to { + let l1_word = self.l1_bitmap[l1_idx]; + if l1_word == 0 { continue; } - for bit in 0..8u8 { - if self.bitmap[byte_idx] & (1 << bit) == 0 { - let page_idx = byte_idx * 8 + bit as usize; - if page_idx >= self.total_pages { return None; } + let word_offset = l1_word.trailing_zeros() as usize; + let word_idx = l1_idx * 64 + word_offset; - self.bitmap[byte_idx] |= 1 << bit; - self.ref_counts[page_idx] = 1; - self.used_pages += 1; - self.last_byte = byte_idx; - - return Some(PhysAddr(page_idx as u64 * PAGE_SIZE)); - } + let word = unsafe { self.read_word(word_idx) }; + if word == !0u64 { + self.l1_bitmap[l1_idx] &= !(1 << word_offset); + continue; } + + let free_bit = (!word).trailing_zeros() as usize; + let page_idx = word_idx * 64 + free_bit; + if page_idx >= self.total_pages { continue; } + + let byte = page_idx / 8; + let bit = page_idx % 8; + + self.bitmap[byte] |= 1 << bit; + self.ref_counts[page_idx] = 1; + self.used_pages += 1; + self.last_word = word_idx; + + if unsafe { self.read_word(word_idx) } == !0u64 { + self.l1_bitmap[l1_idx] &= !(1 << word_offset); + } + + return Some(PhysAddr(page_idx as u64 * PAGE_SIZE)); } } @@ -176,9 +218,11 @@ impl BitmapPMM { for i in run_start..run_start + count { self.bitmap[i / 8] |= 1 << (i % 8); self.ref_counts[i] = 1; + let word_idx = i / 64; + self.l1_update_word(word_idx); } self.used_pages += count; - self.last_byte = run_start / 8; + self.last_word = run_start / 64; return Some(PhysAddr(run_start as u64 * PAGE_SIZE)); } } else { @@ -190,8 +234,6 @@ impl BitmapPMM { } } -//Module-level convenience functions - pub fn alloc_frame() -> Option { PMM.lock().as_mut()?.alloc_frame() } @@ -219,7 +261,3 @@ pub fn get_stats() -> (usize, usize) { (0, 0) } } - - - - diff --git a/kernel/src/mem/vmm.rs b/kernel/src/mem/vmm.rs index 4dbcb67..8c4ceb0 100644 --- a/kernel/src/mem/vmm.rs +++ b/kernel/src/mem/vmm.rs @@ -306,14 +306,12 @@ pub struct AddressSpace { pub pml4_phys: PhysAddr, /// Sorted (by virt_start), non-overlapping VMA list. regions: Vec, - /// HHDM offset for dereferencing page-table pages. - hhdm: u64, } impl AddressSpace { #[inline] unsafe fn pml4_raw(&self) -> *mut PageTable { - self.pml4_phys.to_virt(self.hhdm).as_mut_ptr::() + self.pml4_phys.to_virt().as_mut_ptr::() } fn check_overlap(&self, start: VirtAddr, end: VirtAddr) -> Result<(), VmError> { @@ -326,10 +324,9 @@ impl AddressSpace { } pub fn clone_for_fork(&mut self, child_cap_token: u64) -> Result { - let mut child = AddressSpace::new(self.hhdm)?; + let mut child = AddressSpace::new()?; let child_pml4 = unsafe { &mut *child.pml4_raw() }; let parent_pml4 = unsafe { &mut *self.pml4_raw() }; - let hhdm = self.hhdm; for region in &mut self.regions { let mut child_region = VmaRegion { @@ -350,10 +347,10 @@ impl AddressSpace { match &mut region.backing { VmaBacking::Physical(base) => { - child_pml4.map_region(region.virt_start, *base, region.size(), region.flags.to_page_flags(), hhdm); + child_pml4.map_region(region.virt_start, *base, region.size(), region.flags.to_page_flags()); } VmaBacking::Shared { phys_base, .. } => { - child_pml4.map_region(region.virt_start, *phys_base, region.size(), region.flags.to_page_flags(), hhdm); + child_pml4.map_region(region.virt_start, *phys_base, region.size(), region.flags.to_page_flags()); } VmaBacking::Anonymous(frames) => { let cow_needed = region.flags.contains(VmaFlags::WRITE); @@ -377,10 +374,10 @@ impl AddressSpace { page_flags.remove(PageTableFlags::WRITABLE); page_flags.insert(PageTableFlags::COW); - let _ = parent_pml4.update_flags(virt, page_flags, hhdm); + let _ = parent_pml4.update_flags(virt, page_flags); } - child_pml4.map_page(virt, *frame, page_flags, hhdm); + child_pml4.map_page(virt, *frame, page_flags); child_frames[i] = Some(*frame); } } @@ -408,12 +405,12 @@ impl AddressSpace { } /// Unmap pages + free frames (if owned) for one region. - fn do_unmap(pml4: *mut PageTable, region: &VmaRegion, hhdm: u64) { + fn do_unmap(pml4: *mut PageTable, region: &VmaRegion) { let pml4 = unsafe { &mut *pml4 }; let own = region.backing.owns_frames(); for i in 0..region.pages() { let virt = VirtAddr(region.virt_start.0 + i as u64 * 4096); - pml4.unmap_page(virt, hhdm); + pml4.unmap_page(virt); if own { if let Some(frame) = region.backing.phys_for_page(i) { pmm::free_frame(frame); @@ -423,7 +420,6 @@ impl AddressSpace { } fn do_revoke_by_token(&mut self, cap_token: u64) { - let hhdm = self.hhdm; let pml4 = unsafe { self.pml4_raw() }; let indices: Vec = self.regions @@ -435,7 +431,7 @@ impl AddressSpace { for idx in indices.into_iter().rev() { let region = self.regions.remove(idx); - Self::do_unmap(pml4, ®ion, hhdm); + Self::do_unmap(pml4, ®ion); } } @@ -456,23 +452,22 @@ impl AddressSpace { impl AddressSpace { /// Allocate a fresh, empty address space with a zeroed PML4. - pub fn new(hhdm: u64) -> Result { + pub fn new() -> Result { let pml4_phys = pmm::alloc_frame().ok_or(VmError::OutOfMemory)?; unsafe { - core::ptr::write_bytes(pml4_phys.to_virt(hhdm).as_mut_ptr::(), 0, 4096); + core::ptr::write_bytes(pml4_phys.to_virt().as_mut_ptr::(), 0, 4096); } Ok(Self { asid: alloc_asid()?, pml4_phys, regions: Vec::new(), - hhdm, }) } /// Wrap an already-active PML4 (bootloader-provided kernel table). /// ASID 0 = kernel "no PCID tagging"; it is never returned to the pool. - pub fn from_active(pml4_phys: PhysAddr, asid: u16, hhdm: u64) -> Self { - Self { asid, pml4_phys, regions: Vec::new(), hhdm } + pub fn from_active(pml4_phys: PhysAddr, asid: u16) -> Self { + Self { asid, pml4_phys, regions: Vec::new() } } pub fn map_region( @@ -494,11 +489,10 @@ impl AddressSpace { let pml4 = unsafe { &mut *self.pml4_raw() }; let page_flags = flags.to_page_flags(); let page_count = (size / 4096) as usize; - let hhdm = self.hhdm; let backing = match phys { Some(base) => { - pml4.map_region(virt, base, size, page_flags, hhdm); + pml4.map_region(virt, base, size, page_flags); VmaBacking::Physical(base) } None if flags.contains(VmaFlags::LAZY) => { @@ -511,13 +505,12 @@ impl AddressSpace { for i in 0..page_count { let frame = pmm::alloc_frame().ok_or(VmError::OutOfMemory)?; unsafe { - core::ptr::write_bytes(frame.to_virt(hhdm).as_mut_ptr::(), 0, 4096); + core::ptr::write_bytes(frame.to_virt().as_mut_ptr::(), 0, 4096); } pml4.map_page( VirtAddr(virt.0 + i as u64 * 4096), frame, page_flags, - hhdm, ); frames.push(Some(frame)); } @@ -557,7 +550,6 @@ impl AddressSpace { let pml4 = unsafe { &mut *self.pml4_raw() }; let page_flags = flags.to_page_flags(); - let hhdm = self.hhdm; // Eagerly map all pages — the physical addresses are already known. for i in 0..page_count { @@ -565,7 +557,6 @@ impl AddressSpace { VirtAddr(virt.0 + i as u64 * 4096), PhysAddr(phys_base.0 + i as u64 * 4096), page_flags, - hhdm, ); } @@ -585,7 +576,6 @@ impl AddressSpace { /// Returns `Ok(())` if the fault was a valid lazy demand-page (caller should /// `iretq` to retry). Returns `Err` for illegal accesses. pub fn handle_fault(&mut self, fault_addr: VirtAddr, write: bool) -> Result<(), VmError> { - let hhdm = self.hhdm; let idx = self.find_idx(fault_addr).ok_or(VmError::RegionNotFound)?; let (virt_start, region_flags) = { @@ -601,7 +591,7 @@ impl AddressSpace { let page_virt = VirtAddr(virt_start.0 + page_idx as u64 * 4096); let pml4 = unsafe { &mut *self.pml4_raw() }; - let current_pte_flags = pml4.get_flags(page_virt, hhdm); + let current_pte_flags = pml4.get_flags(page_virt); let is_cow = current_pte_flags.map_or(false, |f| f.contains(PageTableFlags::COW)); if write && is_cow { @@ -617,8 +607,8 @@ impl AddressSpace { unsafe { core::ptr::copy_nonoverlapping( - old_frame.to_virt(hhdm).as_ptr::(), - new_frame.to_virt(hhdm).as_mut_ptr::(), + old_frame.to_virt().as_ptr::(), + new_frame.to_virt().as_mut_ptr::(), 4096, ); } @@ -629,7 +619,7 @@ impl AddressSpace { target_flags.remove(PageTableFlags::COW); target_flags.insert(PageTableFlags::WRITABLE); - pml4.map_page(page_virt, new_frame, target_flags, hhdm); + pml4.map_page(page_virt, new_frame, target_flags); pmm::free_frame(old_frame); @@ -651,7 +641,7 @@ impl AddressSpace { let frame = pmm::alloc_frame().ok_or(VmError::OutOfMemory)?; unsafe { - core::ptr::write_bytes(frame.to_virt(hhdm).as_mut_ptr::(), 0, 4096); + core::ptr::write_bytes(frame.to_virt().as_mut_ptr::(), 0, 4096); } frames[page_idx] = Some(frame); @@ -660,7 +650,7 @@ impl AddressSpace { target_flags.remove(PageTableFlags::WRITABLE); } - pml4.map_page(page_virt, frame, target_flags, hhdm); + pml4.map_page(page_virt, frame, target_flags); Ok(()) } @@ -669,7 +659,7 @@ impl AddressSpace { let idx = self.find_idx(virt).ok_or(VmError::RegionNotFound)?; let region = self.regions.remove(idx); let pml4 = unsafe { self.pml4_raw() }; - Self::do_unmap(pml4, ®ion, self.hhdm); + Self::do_unmap(pml4, ®ion); tlb_flush_asid(self.asid); Ok(()) } @@ -679,7 +669,6 @@ impl AddressSpace { /// Hardware access is terminated before this function returns. /// Shared-backed VMAs are unmapped from the PT without freeing frames. pub fn revoke_by_token(&mut self, cap_token: u64) { - let hhdm = self.hhdm; let pml4 = unsafe { self.pml4_raw() }; let indices: Vec = self.regions @@ -693,7 +682,7 @@ impl AddressSpace { for idx in indices.into_iter().rev() { let region = self.regions.remove(idx); - Self::do_unmap(pml4, ®ion, hhdm); + Self::do_unmap(pml4, ®ion); } tlb_flush_asid(self.asid); @@ -703,7 +692,7 @@ impl AddressSpace { /// Walk the live page table to translate `virt` → physical address. pub fn translate(&self, virt: VirtAddr) -> Option { - unsafe { (*self.pml4_raw()).translate(virt, self.hhdm) } + unsafe { (*self.pml4_raw()).translate(virt) } } //Activation @@ -737,10 +726,9 @@ impl AddressSpace { impl Drop for AddressSpace { fn drop(&mut self) { - let hhdm = self.hhdm; let pml4 = unsafe { self.pml4_raw() }; while let Some(region) = self.regions.pop() { - Self::do_unmap(pml4, ®ion, hhdm); + Self::do_unmap(pml4, ®ion); } pmm::free_frame(self.pml4_phys); // Return the ASID to the pool so it can be reused by future processes. @@ -823,6 +811,6 @@ pub static KERNEL_SPACE: Locked> = Locked::new(None); /// /// ASID 0 = PCID 0 = kernel (no per-process PCID tagging). /// Must be called after `BitmapPMM::init` and `init_cpu_features`. -pub fn init_kernel_space(pml4_phys: PhysAddr, hhdm: u64) { - *KERNEL_SPACE.lock() = Some(AddressSpace::from_active(pml4_phys, 0, hhdm)); +pub fn init_kernel_space(pml4_phys: PhysAddr) { + *KERNEL_SPACE.lock() = Some(AddressSpace::from_active(pml4_phys, 0)); }