Compare commits

...

2 Commits

Author SHA1 Message Date
Faynot
1bfde637d0 feat: docs & ARCH 2.2, 2.3, 2.4 2026-07-07 16:40:41 +03:00
Faynot
c092a81331 fix: bugs 2026-07-06 00:25:49 +03:00
42 changed files with 6065 additions and 396 deletions

626
PLAN.md Normal file
View File

@@ -0,0 +1,626 @@
# Elyz (LISA) Microkernel — Полный план оптимизации и полировки
> Анализ кода по состоянию на июнь 2026.
> Составлен на основе исходников: `kernel/src/`, linker-скрипты, Cargo.toml, Makefile.
---
## Содержание
1. [Условные обозначения](#условные-обозначения)
2. [Критические баги (не работает)](#ф-1-критические-баги-фаза-1)
3. [Средние баги (UB / утечки)](#ф-1-критические-баги-фаза-1)
4. [Проблемы архитектуры](#ф-2-архитектурный-рефакторинг-фаза-2)
5. [Code Quality & Naming](#ф-3-code-quality--naming-фаза-3)
6. [Производительность](#ф-4-производительность-фаза-4)
7. [Инфраструктура](#ф-5-инфраструктура-фаза-5)
8. [Полный пошаговый план (сортированный)](#полный-пошаговый-план-сортированный-по-приоритету)
9. [Roadmap](#roadmap)
---
## Условные обозначения
| Метка | Значение |
|-------|----------|
| 🔴 CRIT | Системный баг — крах, silent corruption, UB |
| 🟡 HIGH | Утечка памяти, логическая ошибка |
| 🟢 MED | Неоптимальность, технический долг |
| 🔵 LOW | Косметика, нейминг, кодстайл |
---
## Ф 1: Критические баги (Фаза 1 — CRIT 1.11.6 ✅ исправлены)
### 🔴 CRIT 1.6 — `tlb_flush_asid` не работает с >64 ядрами ✅ исправлено
**Файл**: `kernel/src/mem/vmm.rs:794-802`
**Проблема**: `1u64 << current_core` при `current_core >= 64` — UB (panic в debug, wrap в release).
**Решение**: Добавлена проверка `current_core < 64` перед shift. Для core >= 64 self-exclusion пропускается (core не входит в нижние 64 бита маски). В `handle_tlb_shootdown_ipi` аналогичная проверка предотвращает запись в маску по некорректному смещению.
```rust
let target_mask = if current_core < 64 {
active_cpus & !(1u64 << current_core)
} else {
active_cpus
};
```
**TODO**: При реализации SMP с >64 ядер — перейти на `AtomicU128` или динамический список APIC ID.
---
## Ф 1.5: Средние баги (Фаза 1.5 — MED 1.71.10 ✅ исправлены)
### 🟡 MED 1.7 — `unwrap_unchecked()` на Option без full fence ✅ исправлено
**Файл**: `kernel/src/mem/pm_router.rs:70-82`
**Проблема**: TOCTOU race между check `is_ready` и `unwrap_unchecked()`. Хотя `is_ready` никогда не сбрасывается, code smell.
**Решение**: Добавлен комментарий-инвариант, объясняющий почему TOCTOU безопасен (is_ready set-once, never cleared). Panic path вынесен в `#[cold]` функцию:
```rust
#[cold]
fn not_initialized() -> ! {
panic!("FATAL: PMRouter is accessed before initialization!")
}
```
---
### 🟡 MED 1.8 — `process_messages` не имеет квоты ✅ исправлено
**Файл**: `kernel/src/mem/pm_manages.rs:328-354`
**Проблема**: Дренирует всю очередь за один вызов. Если 10 000 сообщений, заблокирует ядро на неопределённое время.
**Решение**: Добавлен лимит `MAX_MESSAGES_PER_CALL = 64`. После обработки 64 сообщений вызов возвращает управление, позволяя другим компонентам ядра получить процессорное время.
---
### 🟡 MED 1.9 — Bitmap PMM инициализирует `ref_counts` в 1, потом маркирует used-фреймы ✅ исправлено
**Файл**: `kernel/src/mem/pmm.rs:49-76`
**Проблема**: `fill(0xFF)` + `fill(1)` для всех страниц, затем итерация free по USABLE. Лишняя работа.
**Решение**: Инвертирована логика: `fill(0)` для bitmap и ref_counts, затем итерация по **не-USABLE** записям для mark_used. Убрана лишняя запись в резервированные страницы.
---
### 🟡 MED 1.10 — Page fault handler берёт `KERNEL_SPACE.lock()` каждый раз ✅ исправлено
**Файл**: `kernel/src/cpu/interrupts.rs:109-148`
**Проблема**: Двойной захват `KERNEL_SPACE.lock()` — в `process_deferred_mmu_events()` и в `rust_page_fault_handler()`. На SMP каждый PF будет сериализован.
**Решение**: `process_deferred_mmu_events()` встроен в `rust_page_fault_handler` — блокировка захватывается один раз и удерживается для обеих операций. Добавлен TODO о переходе на `RwLock` для SMP.
---
## Ф 2: Архитектурный рефакторинг (Фаза 2)
### 🏗️ ARCH 2.1 — HHDM offset передаётся параметром в каждую функцию
**Проблема**: `hhdm: u64` тащится через все вызовы:
```rust
p4.map_region(virt, phys, size, flags, hhdm);
p4.map_page(virt, frame, flags, hhdm);
```
Загрязняет сигнатуры и не нужно (HHDM offset — константа после boot).
**Решение**: Глобальный `static HHDM_OFFSET: AtomicU64`:
```rust
pub fn init(offset: u64) { HHDM_OFFSET.store(offset, Ordering::Release); }
pub fn get() -> u64 { HHDM_OFFSET.load(Ordering::Relaxed) }
```
Убрать `hhdm` из параметров всех функций paging/vmm.
---
### 🏗️ ARCH 2.2 — Buddy allocator: O(n) поиск при free()
**Файл**: `kernel/src/mem/buddy.rs:246`
```rust
if let Some(pos) = self.free_lists[order].iter().position(|&b| b == buddy_idx) {
```
Линейный поиск по `Vec<usize>` — O(количество блоков этого порядка). При 10 000 блоков порядка 0, free() занимает тысячи итераций.
**Решение**: Перейти на **intrusive linked list** через сами страницы: первые 8 байт неиспользуемой страницы хранят `next` указатель. Даёт O(1) удаление.
Или хеш-таблицу: `HashMap<usize, ()>` (buddy_idx → exists).
---
### 🏗️ ARCH 2.3 — PMM Bitmap: O(n) allocation scan
**Файл**: `kernel/src/mem/pmm.rs:128-158`
Побайтовый (побитовый) scan всего bitmap в поиске нулевого бита. На системе с 64 GB RAM → 64M бит → до 8M итераций на аллокацию.
**Решение**: Использовать **tree bitmap** (buddy-style bitmap). Хранить дополнительный уровень с `u64` словами, где каждый бит = `all_ones(word)`. Позволяет за O(1) найти свободный блок через `ctz` / `clz`.
---
### 🏗️ ARCH 2.4 — Channel `wait_for_response` — busy-wait
**Файл**: `kernel/src/mem/pm_router.rs:130-137`
```rust
while channel.state.load(Ordering::Acquire) != STATE_READY {
core::hint::spin_loop();
}
```
100% CPU consumption в ожидании. Для реального асинхронного рантайма нужен yield-to-scheduler или waker.
**Решение**: Добавить `Waker` registration: поток регистрирует свой `Waker` в канале, а `route_responses` вызывает `wake()` при записи результата. Пока планировщика нет — хотя бы HLT-based ожидание.
---
### 🏗️ ARCH 2.5 — Capability revocation: O(n) рекурсия
**Файл**: `kernel/src/cap/mod.rs:97-137`
`revoke_internal()` сканирует **весь CNode** на каждом уровне рекурсии. С 256+ слотами и глубиной 10 → 2560 итераций.
**Решение**: Держать per-slot список children:
```rust
struct CNodeSlot {
cap: Capability,
parent: Option<usize>,
children: Vec<usize>, // явный список наследников
}
```
Отзыв — проход по дереву без сканирования всей таблицы.
---
### 🏗️ ARCH 2.6 — Нет SMP startup кода
`ACTIVE_CPUS_MASK = 1` (только BSP). TLB shootdown кидает IPI в пустоту. Нет:
- SIPI trampoline page
- Startup sequence для AP
- Per-CPU data areas
- Stack allocation для AP
**Решение**: Реализовать SIPI-последовательность:
1. Выделить страницу по адресу <1MB (real mode trampoline)
2. Записать startup код (выход из real mode → long mode → setup stack → jump to rust)
3. Отправить SIPI с вектором на эту страницу
4. AP просыпается, инициализирует LAPIC, регистрируется в `ACTIVE_CPUS_MASK`
---
### 🏗️ ARCH 2.7 — Вся память в HHDM identity-map при старте
**Файл**: `kernel/src/main.rs:194-201`
```rust
for entry in mmap_res.entries() {
p4.map_region(virt_hhdm, phys, entry.length, flags, hhdm_offset);
if entry.entry_type != RESERVED {
p4.map_region(VirtAddr(entry.base), phys, entry.length, flags, hhdm_offset);
}
}
```
Все *не-reserved* регионы identity-map'ятся по физическому адресу (second mapping). На системе с 64GB RAM и 4KB страницами = 16M calls к `map_page`. Каждый call:
- Аллоцирует до 3 страниц таблиц (P3, P2, P1)
- INVLPG на каждую 4KB страницу
- Время: ~часы при эмуляции, минуты при KVM
**Решение**: Использовать **2MB huge pages** (`PageTableFlags::HUGE_PAGE`) для identity map. 64GB → 32K entries вместо 16M. INVLPG заменяется на CR3 reload.
---
## Ф 3: Code Quality & Naming (Фаза 3)
### 🧹 CQ 3.1 — Dead code: FramebufferDisplay + Console (main.rs)
**Файл**: `kernel/src/main.rs:42-144`
Две структуры:
- `FramebufferDisplay` (реализует `DrawTarget` для `embedded-graphics`)
- `Console` (обёртка над `FramebufferDisplay` + `embedded-graphics`)
Никто не используется. Настоящий консольный вывод — `tty::Console`. При этом `embedded-graphics` висит в Cargo.toml как зависимость.
**Решение**: Удалить об struct + реализацию, убрать `embedded-graphics` из зависимостей.
---
### 🧹 CQ 3.2 — Unused imports в main.rs
**Файл**: `kernel/src/main.rs:6-16`
```rust
use core::fmt::{self, Write};
use alloc::vec::Vec;
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};
```
Часть из них используется, часть — нет (например `fmt::self`, `PageTable`). После удаления dead code (3.1) ещё часть станет не нужна.
---
### 🧹 CQ 3.3 — `#[allow(dead_code)]` на весь mod vmm
**Файл**: `kernel/src/mem/vmm.rs:27`
```rust
#![allow(dead_code)]
```
Отключает warning для ВСЕГО модуля. 800+ строк кода с отключённой проверкой мёртвого кода. Означает что значительная часть VMM API не используется сейчас — надо либо использовать, либо убрать, либо маркировать `#[allow(dead_code)]` на конкретные единицы.
---
### 🧹 CQ 3.4 — Имя проекта: LISA / Elyz / kernel
| Где | Что написано |
|-----|-------------|
| `Cargo.toml` | `name = "LISA"` |
| `README.md` | "Elyz" |
| `limine.conf` | `Elyz (LISA KERNEL)` |
| `GNUmakefile` | `elyz-x86_64.iso` |
| `main.rs` | `LIS4` (LISA 4?) |
Три разных публичных имени.
---
### 🧹 CQ 3.5 — Странное имя файла: `pm_manages.rs`
Грамматически некорректно (глагол в имени файла). Основная структура — `PMActor`, файл должен называться `pm_actor.rs`.
---
### 🧹 CQ 3.6 — Мёртвые linker-скрипты
- `linker-aarch64.ld`
- `linker-riscv64.ld`
- `linker-loongarch64.ld`
Не используются (build.rs хардкодит `linker-x86_64.ld`). Ни один из этих таргетов не подключен в `rust-toolchain.toml`. Dead weight.
---
### 🧹 CQ 3.7 — Locked SpinLock без backoff
**Файл**: `kernel/src/mem/allocator.rs:27`
```rust
while self.lock.compare_exchange_weak(false, true, Ordering::Acquire, Ordering::Relaxed).is_err() {
core::hint::spin_loop();
}
```
`spin_loop()` на x86 — это `REP NOP` (PAUSE). Это правильно, но при длительном ожидании нужно экспоненциально увеличивать задержку.
---
### 🧹 CQ 3.8 — `log!` максрос не lock-free
При вызове `info!(...)` внутри макроса:
1. Вызывается `SerialPort::init()` (см. CRIT 1.3)
2. Пишет на экран через framebuffer
3. Пишет в serial
Если в PF handler вызвать `info!`, и PF произошёл из-за framebuffer DMA или serial IRQ — будет reentrancy. Надо маркировать как `#[inline(never)]` или добавить reentrancy guard.
---
## Ф 4: Производительность (Фаза 4)
### ⚡ PERF 4.1 — Bitmap PMM: word-level skip
**Файл**: `kernel/src/mem/pmm.rs:139`
```rust
if self.bitmap[byte_idx] == 0xFF { continue; }
```
Проверка на уровне байта — можно на уровне `u64` слова: `if word == u64::MAX { skip 8 bytes }`. В 8x меньше итераций.
---
### ⚡ PERF 4.2 — Buddy free: O(n) → O(1)
**Файл**: `kernel/src/mem/buddy.rs:246`
См. ARCH 2.2. Замена `Vec` на intrusive list — ключевой перф-фикс.
---
### ⚡ PERF 4.4 — INVLPG в bulk_map: batch flush
**Файл**: `kernel/src/mem/paging.rs:119-121`
```rust
unsafe {
asm!("invlpg [{}]", in(reg) virt.0, ...);
}
```
`map_region` вызывает `map_page` в цикле — каждая итерация делает INVLPG. 16M страниц = 16M INVLPG. CPU заливает TLBs и потом их же сбрасывает.
**Решение**: Убрать INVLPG из `map_page` (оставить вызывающему), сделать отдельную функцию `flush_range(virt, pages)` или вызов `invpcid` type-2 (flush-by-range).
**Но осторожно**: INVLPG нужен при модификации активной таблицы. Если identity-map ещё не загружена — можно без INVLPG.
---
### ⚡ PERF 4.5 — TLB shootdown: busy-wait → IPI + HLT
**Файл**: `kernel/src/mem/vmm.rs:809-811`
```rust
while SHOOTDOWN_ACK.load(Ordering::Acquire) & target_mask != target_mask {
spin_loop();
}
```
Пока нет AP — код мёртвый. Когда AP появятся, надо чтобы AP висели в `HLT` и просыпались от IPI с TLB_SHOOTDOWN_VECTOR.
---
### ⚡ PERF 4.6 — Page fault handler: lock-free VMA tree
**Файл**: `kernel/src/cpu/interrupts.rs:110`
`KERNEL_SPACE.lock()` — каждый PF сериализуется. На одноядерной системе — OK, на SMP — bottleneck.
**Решение**: `RWLock` на VMA region или lock-free RB-tree для VMA поиска.
---
## Ф 5: Инфраструктура (Фаза 5)
### 🛠️ INFRA 5.1 — SMP AP Startup Code
Отсутствует. Нужен:
- Startup trampoline в real mode (<1MB)
- Переход в protected → long mode
- Настройка GDT/IDT для AP
- Per-CPU stack
- Регистрация в `ACTIVE_CPUS_MASK`
---
### 🛠️ INFRA 5.2 — Мультиархитектурность
Есть linker-скрипты под aarch64, riscv64, loongarch64, но:
- `build.rs` хардкодит `linker-x86_64.ld`
- ASM в interrupts.rs только x86
- LAPIC — x86-specific
- Port IO — x86-specific
- IDT — x86-specific
Для портирования нужна архитектурная абстракция — traits:
```rust
trait Arch {
type PteFlags;
fn init_interrupts();
fn send_ipi(core_id: u32, vector: u8);
fn read_cycle_counter() -> u64;
}
```
---
### 🛠️ INFRA 5.3 — Unit-тесты для Buddy Allocator
Buddy allocator — идеальный кандидат для `#[cfg(test)]`:
- Проверка инвариантов после alloc/free sequence
- Fuzzing случайных порядков
- Проверка coalescing (граничные случаи)
- Проверка alignment
---
### 🛠️ INFRA 5.4 — Утилита для бинарного патчинга
Сейчас результирующий ELF надо руками objcopy'ить. Можно добавить Cargo alias или build.rs скрипт для автоматизации.
---
### 🛠️ INFRA 5.5 — GDB/QEMU debugging support
Добавить `.gdbinit` / `gdb.py` с:
- `target remote localhost:1234`
- Разгрузка символов из ELF
- Макросы для просмотра page table, capability, VMA regions
---
## Ф 6: Безопасность (сквозная)
### 🛡️ SEC 6.1 — Capability minting не проверяет `Relation` parent
**Файл**: `kernel/src/cap/mod.rs:64-77`
```rust
if !src_slot.cap.rights.contains(CapRights::GRANT) {
return Err("Insufficient rights to mint (Missing GRANT flag)");
}
```
Проверяет только GRANT флаг. Но не проверяет `relation`: если source — `Borrow`, можно ли mint? В текущей реализации — можно. Должен быть check:
```rust
if src_slot.cap.relation == Relation::Borrow {
return Err("Cannot mint from borrowed capability");
}
```
---
### 🛡️ SEC 6.2 — CapGuardToken не реализован
В дескрипторе есть упоминание `guards: Vec<CapGuardToken>`, `Borrow` relation и guard-токены, логика самих guard'ов не реализована. Если есть borrow, mint не должен позволять дальнейшее распространение — но это не проверяется.
---
### 🛡️ SEC 6.3 — Нет проверки прав на операции с PMM через capabilities
`BitmapPMM` полностью глобальный: любой код может вызвать `alloc_frame()`, `free_frame()` без проверки capability. Капы пока хранят токены и права, но PMM их не проверяет.
---
---
## Полный пошаговый план (сортированный по приоритету)
### Этап 1: Исправить критические баги (сейчас — не работает)
```
[X] 1. ✅ HIGH — Исправить target_mask для >64 core
Добавлена проверка current_core < 64 перед shift.
Estimate: ~10 мин
[ ] 2. 🔥 HIGH — Исправить ref_counts инициализацию
Вместо fill(1) + free, сделать fill(0) + mark_used.
Estimate: ~15 мин
```
### Этап 2: Архитектурный рефакторинг
```
[ ] 3. 🏗️ ARCH — Глобальный HHDM_OFFSET
AtomicU64 static, убрать hhdm из сигнатур.
Estimate: ~1 ч
[ ] 4. 🏗️ ARCH — Intrusive free-list в buddy allocator
O(1) free вместо O(n).
Estimate: ~2 ч
[ ] 5. 🏗️ ARCH — Tree bitmap для PMM
word-level scan с ctz.
Estimate: ~1.5 ч
[ ] 6. 🏗️ ARCH — HLT-based wait в router channel
Пока нет планировщика — HLT вместо spin.
Estimate: ~30 мин
[ ] 7. 🏗️ ARCH — Capability revoke без O(n) scan
Children list в слоте.
Estimate: ~1 ч
[ ] 8. 🏗️ ARCH — Identity-map через 2MB huge pages
map_region с HUGE_PAGE флагом для identity.
Estimate: ~1 ч
```
### Этап 3: Code Quality
```
[ ] 9. 🧹 CQ — Удалить dead code (FramebufferDisplay, Console, embedded-graphics)
+ неиспользуемые импорты.
Estimate: ~20 мин
[ ] 10. 🧹 CQ — Убрать #![allow(dead_code)] из vmm.rs
Маркировать конкретные единицы.
Estimate: ~15 мин
[ ] 11. 🧹 CQ — Удалить мёртвые linker-скрипты
aarch64/riscv64/loongarch64.ld.
Estimate: ~5 мин
[ ] 12. 🧹 CQ — Переименовать pm_manages.rs → pm_actor.rs
+ все референсы.
Estimate: ~10 мин
[ ] 13. 🧹 CQ — Вынести panic.rs из main.rs
Отдельный файл для panic_handler.
Estimate: ~10 мин
[ ] 14. 🧹 CQ — Синхронизировать имена: LISA → Elyz
Cargo.toml, main.rs ("LIS4"), README.
Estimate: ~15 мин
[ ] 15. 🧹 CQ — Locked spinlock с exponential backoff
PAUSE + цикл задержки.
Estimate: ~15 мин
```
### Этап 4: Производительность
```
[ ] 16. ⚡ PERF — Bitmap scan: word-level (u64) вместо byte-level
+ ctzl для поиска бита.
Estimate: ~30 мин
[ ] 17. ⚡ PERF — INVLPG-free bulk map
Убрать INVLPG из map_page, вызов flush_range после.
Estimate: ~30 мин
[ ] 18. ⚡ PERF — process_messages quota
Max N сообщений за вызов.
Estimate: ~15 мин
```
### Этап 5: Инфраструктура
```
[ ] 19. 🛠️ INFRA — SMP AP startup code
SIPI → trampoline → long mode → Rust.
Estimate: ~1 неделя
[ ] 20. 🛠️ INFRA — Unit tests для buddy allocator
#[cfg(test)] модуль.
Estimate: ~1 ч
[ ] 21. 🛠️ INFRA — GDB script + QEMU -s
.gdbinit для отладки.
Estimate: ~30 мин
[ ] 22. 🛠️ INFRA — Arch trait для x86_64
Подготовка к портированию.
Estimate: ~3-4 ч
```
### Этап 6: Безопасность
```
[ ] 23. 🛡️ SEC — Проверка Relation при mint
Запретить mint из Borrow.
Estimate: ~10 мин
[ ] 24. 🛡️ SEC — CapGuard реализация
Привязка guard-токенов к source slot при mint.
Estimate: ~45 мин
```
---
## Roadmap (визуально)
```
Неделя 1 Неделя 2 Неделя 3
┌─────────────────────────┐ ┌─────────────────────────┐ ┌─────────────────────────┐
│ ✅ CRIT 16 готово │▸▸ │ ARCH 3 HHDM global │▸▸ │ PERF 16 word-level PMM │
│ ✅ MED 1.71.10 │ │ ARCH 4 intrusive list │ │ PERF 17 INVLPG batch │
│ HIGH 2 ref_counts │ │ ARCH 5 tree bitmap │ │ INFRA 19 SMP startup │
│ │ │ CQ 9 clean dead code │ │ INFRA 20 unit tests │
│ │ │ CQ 11-15 rename/refact│ │ SEC 23-24 caps │
└─────────────────────────┘ └─────────────────────────┘ └─────────────────────────┘
↓ ↓ ↓
Kernel bootable и Код читаемый, Производительность:
логирует без крашей архитектура чище SMP + оптимизации
```
---
*Документ создан 29 июня 2026 на основе анализа исходного кода проекта Elyz.*

View File

@@ -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)
```

151
kernel/docs/boot/linker.md Normal file
View File

@@ -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.

View File

@@ -0,0 +1,146 @@
# CNode: таблица capability: `mod.rs`
## Концептуальная модель
**CNode** (Capability Node) — это массив слотов, каждый из которых
может хранить один capability. Это аналог файловой таблицы в Unix,
но для capabilities.
```
CNode {
slots: Vec<Locked<CNodeSlot>>
}
CNodeSlot {
cap: Capability,
parent_idx: Option<usize>, // индекс родительского слота
}
```
## Инициализация
```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<Capability> {
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<Locked<CNodeSlot>>**: каждый слот — отдельная spinlock-ячейка.
Это позволяет параллельно читать разные слоты.
2. **`pub slots`**: прямой доступ к слоту возможен (для тестов).
3. **panic на overflow**: если очередь отзыва переполнена — паника.
Это критический сбой подсистемы ресурсов.

View File

@@ -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`).

View File

@@ -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);
```

View File

@@ -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 | — |

115
kernel/docs/cpu/idt.md Normal file
View File

@@ -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::<Self>() - 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 (один стек для
всех обработчиков).

View File

@@ -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.

View File

@@ -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 │
```

115
kernel/docs/cpu/lapic.md Normal file
View File

@@ -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 доставляет
прерывание. Запись блокирующая (ждёт готовности шины).

View File

@@ -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 ] <module> | <message>
```
**Формат в serial:**
```
GREEN[ LOG] RESET <module> | <message>
```
## Цветовое кодирование
| Уровень | Экран (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) │
└──────────┘ └──────────┘ └──────────┘
```

110
kernel/docs/debug/serial.md Normal file
View File

@@ -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<Option<SerialPort>> = 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
}
```

View File

@@ -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<u64> {
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 дренирует очередь).

File diff suppressed because it is too large Load Diff

70
kernel/docs/index.md Normal file
View File

@@ -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` |

View File

@@ -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<T>()` — получить `*const T` (для чтения)
- `as_mut_ptr<T>()` — получить `*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<T>(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.

View File

@@ -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::<ListNode>()`, чтобы
освобождённый блок мог хранить указатели списка. Гарантируется, потому
что наименьший BLOCK_SIZE (8) ≥ `size_of::<Option<&'static mut ListNode>>`
(8 байт на 64-bit).
## Locked<T> — примитивная spinlock-обёртка
```rust
pub struct Locked<A> {
inner: UnsafeCell<A>,
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<SlabAllocator> = 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 строк кода.

267
kernel/docs/memory/buddy.md Normal file
View File

@@ -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<usize> {
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<usize> {
// 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.

View File

@@ -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
```

View File

@@ -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::<u8>(), 0, 4096);
// Устанавливаем entry: PRESENT | WRITABLE | USER
self.entries[index] = pt_phys.0 | (PRESENT | WRITABLE | USER);
}
// Преобразуем физический адрес entry в ссылку на PageTable
let next_phys = PhysAddr(self.entries[index] & PTE_ADDR_MASK);
Some(unsafe { &mut *next_phys.to_virt().as_mut_ptr::<Self>() })
}
```
### pte_addr_mask — маска адреса
```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<PhysAddr> {
PMM.lock().as_mut()?.alloc_frame()
}
```
Тонкая обёртка, чтобы избежать циклической зависимости между paging и pmm.
## Ключевые инварианты
1. Каждая PageTable занимает ровно одну физическую страницу (4 KiB).
2. Физический адрес таблицы всегда 4KiB-выровнен.
3. Все intermediate таблицы создаются с флагом USER (чтобы и ядро,
и пользователь могли их использовать). Разделение на user/kernel
контролируется на уровне P1 entry.
4. `INVLPG` вызывается после каждого изменения PTE для консистентности TLB.

View File

@@ -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<PhysAddr> {
```
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<Option<BitmapPMM>> = Locked::new(None);
```
Locked<T> — примитивная 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 на этапе инициализации и больше к нему не
обращаются.

View File

@@ -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<PMResponse> │
│ ▼ │
│ Через 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<PMRequest> {
let head = self.head.load(Ordering::Relaxed);
let tail = self.tail.load(Ordering::Acquire);
if head == tail { return None; }
let packed = self.buffer[head & QUEUE_MASK].load(Ordering::Acquire);
self.head.store(head + 1, Ordering::Release);
Some(PMRequest::unpack(packed))
}
```
- Только актор вызывает pop — никакой конкуренции.
- Acquire на tail: видим последнюю запись продюсера.
## Message Packing
Каждое сообщение упаковывается в одно `u64`:
```
Bit 63:56 OPCODE (8 bits) 1=Alloc, 2=Free, 3=Carve
Bit 55:40 CHANNEL_ID (16 bits) маршрутизация ответа; 0 = discard
Bit 39:20 ARG1 (20 bits) size_pages/local_frame_idx/offset_pages
Bit 19: 0 ARG2 (20 bits) token_sig (low 20 bits) / size_pages / order
```
Максимум: 2^20 = 1 048 576 страниц = 4 GiB на одну аллокацию.
## Типы сообщений (PMRequest)
```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<PMResponse> {
match self.buddy.alloc_pages(size_pages) {
Some((rel_idx, order)) => {
let phys = PhysAddr(self.managed_range.0.0 + rel_idx * PAGE_SIZE);
let cap = Capability {
object: CapObject::Memory { phys, size_pages: 1 << order },
rights: CapRights::READ | WRITE | GRANT,
relation: Relation::Strong,
token_sig: (token_sig as u64) ^ self.actor_id ^ (rel_idx as u64),
};
Some(PMResponse { channel_id, result: Allocated { cap, order } })
}
None => {
Some(PMResponse { channel_id, result: OutOfMemory { size_pages } })
}
}
}
```
**Детали:**
- `rel_idx` — относительный индекс в buddy (от 0 до total_pages).
- Физический адрес: `managed_range.start + rel_idx * 4096`.
- `token_sig` — XOR от token_sig запроса, actor_id и rel_idx для уникальности.
- Размер в capability: `1 << order` (округляется до степени двойки).
### handle_free(local_frame_idx, order)
```rust
fn handle_free(&mut self, local_frame_idx, order) -> Option<PMResponse> {
// Валидация границ
let block_end = local_frame_idx.saturating_add(1usize << order);
if block_end > self.buddy.total_pages() {
debug_assert!(false, "Out of range");
return None;
}
self.buddy.free(local_frame_idx, order);
None // Free не требует ответа
}
```
### handle_carve(offset_pages, size_pages, channel_id)
Carve — выделение под-диапазона без участия buddy-аллокатора.
Используется для статических размещений (framebuffer alias).
```rust
fn handle_carve(&mut self, offset_pages, size_pages, channel_id) -> Option<PMResponse> {
let range_pages = self.buddy.total_pages();
// Проверка границ
if offset_pages >= range_pages || size_pages == 0 || ... {
return Some(PMResponse { channel_id, result: OutOfMemory { size_pages } });
}
let phys = PhysAddr(self.managed_range.0.0 + offset_pages * PAGE_SIZE);
let cap = Capability {
object: CapObject::Memory { phys, size_pages },
rights: CapRights::READ | WRITE | GRANT,
relation: Relation::Strong,
token_sig: self.actor_id ^ offset_pages,
};
Some(PMResponse { channel_id, result: Carved { cap } })
}
```
## Ballooning hook (TODO)
Когда `BuddyAllocator::alloc_pages` возвращает `None`, актор генерирует
`OutOfMemory`. Будущая подсистема ballooning будет перехватывать этот
ответ до того, как он дойдёт до запросившего, и договариваться о передаче
памяти от соседнего актора.
## Владение и capabilities
- PMActor держит Strong capability на весь свой диапазон.
- Все выделенные или вырезанные под-диапазоны — тоже Strong (дочерние).
- При освобождении через Free buddy-дерево объединяет блоки.
- Актёр **никогда** не обращается к глобальному BitmapPMM.
## Максимальное количество сообщений за вызов
```rust
const MAX_MESSAGES_PER_CALL: usize = 64;
```
Ограничение предотвращает голодание ядра, когда inbox глубок.

View File

@@ -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<Option<PMResult>>, // ячейка результата
}
```
- `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<u16> {
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<PMResponse>) {
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<F>(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<PMResponse>) {
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.

267
kernel/docs/memory/vmm.md Normal file
View File

@@ -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<VmaRegion> (сортирован по адресу)│
└─────────────────────────────────────────────┘
┌───────────────┐
│ 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<Option<PhysAddr>>),
// ^ Owned: каждая страница выделена из PMM под этот VMA.
// Option: None = lazy (страница ещё не выделена).
Physical(PhysAddr),
// ^ Не-owned: всё отображение — непрерывный физический диапазон.
// Frame не освобождаются при unmap.
Shared { owner_cap: u64, phys_base: PhysAddr },
// ^ Zero-copy shared mapping: чужие физические страницы.
// Frame не освобождаются при unmap.
// При revoke owner_cap — все Shared VMA с этим токеном удаляются.
}
```
### VmaFlags
```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 14094: пользовательские.
`alloc()` — Two-pass: от hint до 4094, затем от 1 до hint.
Пропускает полностью занятые 32-битные слова.
## Основные операции AddressSpace
### `new()` — создание пустого пространства
1. Аллоцирует фрейм из PMM для PML4.
2. Обнуляет его (все entry = not present).
3. Выделяет ASID из ASID_ALLOC.
4. Пустой список regions.
### `from_active(pml4_phys, asid)` — обёртка существующего PML4
Используется для kernel space (ASID 0).
### `map_region(virt, phys, size, flags, cap_token)` — отображение
```rust
pub fn map_region(&mut self, virt: VirtAddr, phys: Option<PhysAddr>,
size: u64, flags: VmaFlags, cap_token: u64) -> Result<VirtAddr, VmError>
```
- `phys = Some(base)`: Physical mapping — непрерывный диапазон.
- `phys = None + LAZY`: Demand-paging — фреймы выделяются при fault'е.
- `phys = None + !LAZY`: Eager alloc — все фреймы выделяются сейчас.
Создаёт VmaRegion, вставляет в отсортированный список.
### `map_shared(virt, phys_base, page_count, flags, owner_cap)` — zero-copy
Как map_region, но:
- Не копирует фреймы (не владеет ими).
- Всегда eagerly отображает (адреса известны).
- Записывает `cap_token = owner_cap` для revoke.
### `unmap_region(virt)` — удаление VMA
1. Находит VMA по адресу.
2. Удаляет из списка.
3. Вызывает `do_unmap(pml4, region)`:
- unmap_page для каждой страницы.
- Если backing owns_frames (Anonymous) — free_frame.
4. TLB flush.
### `handle_fault(fault_addr, write)` — Page Fault handler
```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<Option<AddressSpace>> = Locked::new(None);
```
Инициализируется в `init_kernel_space(pml4_phys)` после создания
первичных page tables.
## Drop для AddressSpace
При Drop:
1. unmap всех регионов (с освобождением owned frames).
2. free_frame(pml4_phys).
3. free_asid(asid).
## VmError — типы ошибок
```rust
pub enum VmError {
OutOfMemory, // Нет фреймов в PMM
RegionOverlap, // VMA пересекается с существующим
RegionNotFound, // Нет VMA по адресу
InvalidAlignment, // Адрес/размер не выровнены на 4k
InvalidRange, // Нулевой размер или переполнение
PermissionDenied, // Нет прав на запись
UnexpectedFault, // Fault в не-lazy, не-COW регионе
NonCanonical, // Некорректный адрес
AsidExhausted, // Закончились ASID
}
```

View File

@@ -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** — загружаем базовые обработчики исключений (031).
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<T> + глобальный аллокатор
│ ├── 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
```

View File

@@ -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`.

150
kernel/docs/tty.md Normal file
View File

@@ -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::<u32>().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.

View File

@@ -7,25 +7,29 @@ pub static mut IDT: crate::cpu::idt::InterruptDescriptorTable = crate::cpu::idt:
pub const TLB_SHOOTDOWN_VECTOR: u8 = 0xFD; pub const TLB_SHOOTDOWN_VECTOR: u8 = 0xFD;
global_asm!( macro_rules! exception_stub {
".global page_fault_stub", ($name:ident, $handler:ident) => {
"page_fault_stub:", concat!(
"push rax", ".global ", stringify!($name), "\n",
"push rcx", stringify!($name), ":\n",
"push rdx", "push rax\npush rcx\npush rdx\npush rbx\npush rbp\n",
"push rbx", "push rsi\npush rdi\npush r8\npush r9\npush r10\n",
"push rbp", "push r11\npush r12\npush r13\npush r14\npush r15\n",
"push rsi", "mov rdi, [rsp + 15*8]\n",
"push rdi", "call ", stringify!($handler), "\n",
"push r8", "pop r15\npop r14\npop r13\npop r12\npop r11\n",
"push r9", "pop r10\npop r9\npop r8\npop rdi\npop rsi\n",
"push r10", "pop rbp\npop rbx\npop rdx\npop rcx\npop rax\n",
"push r11", "add rsp, 8\n",
"push r12", "iretq\n",
"push r13", )
"push r14", };
"push r15", }
global_asm!(
exception_stub!(page_fault_stub, rust_page_fault_handler),
exception_stub!(gpf_stub, rust_gpf_handler),
exception_stub!(double_fault_stub, rust_double_fault_handler),
".global tlb_shootdown_stub", ".global tlb_shootdown_stub",
"tlb_shootdown_stub:", "tlb_shootdown_stub:",
"push rax", "push rax",
@@ -43,12 +47,7 @@ global_asm!(
"push r13", "push r13",
"push r14", "push r14",
"push r15", "push r15",
"call rust_tlb_shootdown_handler", "call rust_tlb_shootdown_handler",
"mov rdi, [rsp + 15*8]",
"call rust_page_fault_handler",
"pop r15", "pop r15",
"pop r14", "pop r14",
"pop r13", "pop r13",
@@ -64,36 +63,202 @@ global_asm!(
"pop rdx", "pop rdx",
"pop rcx", "pop rcx",
"pop rax", "pop rax",
"add rsp, 8",
"iretq" "iretq"
); );
unsafe extern "C" { unsafe extern "C" {
fn page_fault_stub(); fn page_fault_stub();
fn gpf_stub();
fn double_fault_stub();
fn tlb_shootdown_stub(); fn tlb_shootdown_stub();
} }
pub fn init_idt() { pub fn init_early_exceptions() {
unsafe { unsafe {
let idt_mut_ptr = core::ptr::addr_of_mut!(IDT); let idt = core::ptr::addr_of_mut!(IDT);
(*idt_mut_ptr).set_handler(14, page_fault_stub as u64); let early: [u64; 32] = [
early_handler_0 as u64, early_handler_1 as u64,
early_handler_2 as u64, early_handler_3 as u64,
early_handler_4 as u64, early_handler_5 as u64,
early_handler_6 as u64, early_handler_7 as u64,
early_handler_8 as u64, early_handler_9 as u64,
early_handler_10 as u64, early_handler_11 as u64,
early_handler_12 as u64, early_handler_13 as u64,
early_handler_14 as u64, early_handler_15 as u64,
early_handler_16 as u64, early_handler_17 as u64,
early_handler_18 as u64, early_handler_19 as u64,
early_handler_20 as u64, early_handler_21 as u64,
early_handler_22 as u64, early_handler_23 as u64,
early_handler_24 as u64, early_handler_25 as u64,
early_handler_26 as u64, early_handler_27 as u64,
early_handler_28 as u64, early_handler_29 as u64,
early_handler_30 as u64, early_handler_31 as u64,
];
for (v, &handler) in early.iter().enumerate() {
(*idt).set_handler(v as u8, handler);
}
// Override with proper handlers for vectors 8, 13, 14 and TLB IPI.
(*idt).set_handler(8, double_fault_stub as u64);
(*idt).set_handler(13, gpf_stub as u64);
(*idt).set_handler(14, page_fault_stub as u64);
(*idt).set_handler(TLB_SHOOTDOWN_VECTOR, tlb_shootdown_stub as u64);
(*idt_mut_ptr).set_handler(TLB_SHOOTDOWN_VECTOR, tlb_shootdown_stub as u64); let ptr: &'static crate::cpu::idt::InterruptDescriptorTable = &*core::ptr::addr_of!(IDT);
ptr.load();
let idt_static_ref: &'static crate::cpu::idt::InterruptDescriptorTable = &*core::ptr::addr_of!(IDT);
idt_static_ref.load();
} }
} }
pub fn init_idt() {
pub fn process_deferred_mmu_events() { // init_early_exceptions already loaded the IDT; this just overrides
let mut vmm_guard = KERNEL_SPACE.lock(); // vectors that the full kernel needs. Calling lidt again is harmless.
if let Some(space) = vmm_guard.as_mut() { unsafe {
space.process_pending_revocations(); let idt = core::ptr::addr_of_mut!(IDT);
(*idt).set_handler(14, page_fault_stub as u64);
(*idt).set_handler(TLB_SHOOTDOWN_VECTOR, tlb_shootdown_stub as u64);
let ptr: &'static crate::cpu::idt::InterruptDescriptorTable = &*core::ptr::addr_of!(IDT);
ptr.load();
} }
} }
#[unsafe(no_mangle)]
pub extern "C" fn rust_gpf_handler(error_code: u64) -> ! {
crate::debug::serial::write_global(format_args!(
"\n!!! GENERAL PROTECTION FAULT !!! error_code={:#x}\n\
CPU halted.\n",
error_code
));
loop {
unsafe { asm!("cli; hlt", options(nomem, nostack, preserves_flags)); }
}
}
#[unsafe(no_mangle)]
pub extern "C" fn rust_double_fault_handler(error_code: u64) -> ! {
crate::debug::serial::write_global(format_args!(
"\n!!! DOUBLE FAULT !!! error_code={:#x}\n\
CPU halted.\n",
error_code
));
loop {
unsafe { asm!("cli; hlt", options(nomem, nostack, preserves_flags)); }
}
}
global_asm!(r#"
.altmacro
.macro early_stub vec
.globl early_handler_\vec
.balign 16
early_handler_\vec:
push 0 /* dummy error code */
push \vec /* vector number */
jmp early_common
.endm
early_stub 0
early_stub 1
early_stub 2
early_stub 3
early_stub 4
early_stub 5
early_stub 6
early_stub 7
early_stub 8
early_stub 9
early_stub 10
early_stub 11
early_stub 12
early_stub 13
early_stub 14
early_stub 15
early_stub 16
early_stub 17
early_stub 18
early_stub 19
early_stub 20
early_stub 21
early_stub 22
early_stub 23
early_stub 24
early_stub 25
early_stub 26
early_stub 27
early_stub 28
early_stub 29
early_stub 30
early_stub 31
early_common:
push rax
push rcx
push rdx
push rbx
push rbp
push rsi
push rdi
push r8
push r9
push r10
push r11
push r12
push r13
push r14
push r15
mov rdi, [rsp + 15*8] /* vector number */
mov rsi, [rsp + 16*8] /* error code (or dummy 0) */
call rust_early_exception_handler
/* never returns */
"#);
unsafe extern "C" {
fn early_handler_0();
fn early_handler_1();
fn early_handler_2();
fn early_handler_3();
fn early_handler_4();
fn early_handler_5();
fn early_handler_6();
fn early_handler_7();
fn early_handler_8();
fn early_handler_9();
fn early_handler_10();
fn early_handler_11();
fn early_handler_12();
fn early_handler_13();
fn early_handler_14();
fn early_handler_15();
fn early_handler_16();
fn early_handler_17();
fn early_handler_18();
fn early_handler_19();
fn early_handler_20();
fn early_handler_21();
fn early_handler_22();
fn early_handler_23();
fn early_handler_24();
fn early_handler_25();
fn early_handler_26();
fn early_handler_27();
fn early_handler_28();
fn early_handler_29();
fn early_handler_30();
fn early_handler_31();
}
#[unsafe(no_mangle)]
pub extern "C" fn rust_early_exception_handler(vector: u64, _error_code: u64) -> ! {
crate::debug::serial::write_global(format_args!(
"\n!!! EARLY EXCEPTION !!! vector={} error_code={:#x}\n\
CPU halted.\n",
vector, _error_code
));
loop {
unsafe { asm!("cli; hlt", options(nomem, nostack, preserves_flags)); }
}
}
#[unsafe(no_mangle)] #[unsafe(no_mangle)]
pub extern "C" fn rust_page_fault_handler(error_code: u64) { pub extern "C" fn rust_page_fault_handler(error_code: u64) {
let fault_addr: u64; let fault_addr: u64;
@@ -105,13 +270,18 @@ pub extern "C" fn rust_page_fault_handler(error_code: u64) {
let present = (error_code & 0x1) != 0; let present = (error_code & 0x1) != 0;
let virt_addr = VirtAddr(fault_addr); let virt_addr = VirtAddr(fault_addr);
process_deferred_mmu_events(); // Acquire KERNEL_SPACE lock once for both deferred events and fault handling.
// TODO SMP: switch to RwLock so concurrent read-only faults are not
// serialised. `handle_fault` is read-only for COW faults but mutates
// the VMA tree on demand-paging — a lock-free VMA tree or per-region
// locks would be ideal.
let mut vmm_guard = KERNEL_SPACE.lock(); let mut vmm_guard = KERNEL_SPACE.lock();
if let Some(space) = vmm_guard.as_mut() { if let Some(space) = vmm_guard.as_mut() {
space.process_pending_revocations();
match space.handle_fault(virt_addr, write) { match space.handle_fault(virt_addr, write) {
Ok(_) => return, Ok(_) => {},
Err(e) => { Err(e) => {
panic!( panic!(
"KERNEL PANIC: Unprocessed failure of virtual memory (Page Fault)!\n\ "KERNEL PANIC: Unprocessed failure of virtual memory (Page Fault)!\n\

View File

@@ -1,6 +1,7 @@
// src/cpu/lapic.rs // src/cpu/lapic.rs
use core::sync::atomic::{AtomicU64, Ordering}; use core::sync::atomic::{AtomicU64, Ordering};
use crate::mem::address::get_hhdm;
pub const LAPIC_DEFAULT_BASE: u64 = 0xFEE00_000; pub const LAPIC_DEFAULT_BASE: u64 = 0xFEE00_000;
const LAPIC_EOI: u64 = 0x0B0; const LAPIC_EOI: u64 = 0x0B0;
@@ -8,8 +9,8 @@ const LAPIC_ICR_LOW: u64 = 0x300;
static LAPIC_VIRT_BASE: AtomicU64 = AtomicU64::new(0); static LAPIC_VIRT_BASE: AtomicU64 = AtomicU64::new(0);
pub fn init(hhdm_offset: u64) { pub fn init() {
LAPIC_VIRT_BASE.store(LAPIC_DEFAULT_BASE + hhdm_offset, Ordering::SeqCst); LAPIC_VIRT_BASE.store(LAPIC_DEFAULT_BASE + get_hhdm(), Ordering::SeqCst);
} }
#[inline(always)] #[inline(always)]

View File

@@ -36,15 +36,13 @@ macro_rules! log {
let _ = writeln!($console, $($arg)*); let _ = writeln!($console, $($arg)*);
// Serial debug output // Serial debug output
let mut sp = unsafe { $crate::debug::serial::SerialPort::init() }; $crate::debug::serial::write_global(format_args!(
let _ = writeln!( "{}[{:>5}]\x1b[0m {:<8} | {}\n",
sp,
"{}[{:>5}]\x1b[0m {:<8} | {}",
$level.serial_color_code(), $level.serial_color_code(),
"LOG", "LOG",
$module, $module,
format_args!($($arg)*) format_args!($($arg)*)
); ));
}}; }};
} }

View File

@@ -1,4 +1,6 @@
use core::arch::asm; use core::arch::asm;
use core::fmt::Write;
use crate::mem::allocator::Locked;
pub struct SerialPort(u16); pub struct SerialPort(u16);
@@ -7,16 +9,14 @@ impl SerialPort {
pub unsafe fn init() -> Self { pub unsafe fn init() -> Self {
let port = Self::COM1; let port = Self::COM1;
// В новых версиях Rust даже внутри unsafe fn
// вызовы других unsafe функций требуют явного блока
unsafe { unsafe {
outb(port + 1, 0x00); // Disable interrupts
outb(port + 3, 0x80); // Enable DLAB
outb(port + 0, 0x03); // Divisor 3 (38400 baud)
outb(port + 1, 0x00); outb(port + 1, 0x00);
outb(port + 3, 0x03); // 8 bits, no parity, 1 stop bit outb(port + 3, 0x80);
outb(port + 2, 0xC7); // Enable FIFO outb(port + 0, 0x03);
outb(port + 4, 0x0B); // IRQs enabled, RTS/DSR set outb(port + 1, 0x00);
outb(port + 3, 0x03);
outb(port + 2, 0xC7);
outb(port + 4, 0x0B);
} }
SerialPort(port) SerialPort(port)
} }
@@ -38,6 +38,20 @@ impl core::fmt::Write for SerialPort {
} }
} }
static SERIAL_PORT: Locked<Option<SerialPort>> = 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);
}
}
unsafe fn outb(port: u16, val: u8) { unsafe fn outb(port: u16, val: u8) {
unsafe { unsafe {
asm!("out dx, al", in("dx") port, in("al") val, options(nomem, nostack, preserves_flags)); asm!("out dx, al", in("dx") port, in("al") val, options(nomem, nostack, preserves_flags));

View File

@@ -14,6 +14,7 @@ use crate::mem::paging::{PageTable, PageTableFlags};
use crate::mem::address::{PhysAddr, VirtAddr}; use crate::mem::address::{PhysAddr, VirtAddr};
use crate::cap::{Relation, CapRights, Capability, CapObject}; use crate::cap::{Relation, CapRights, Capability, CapObject};
use crate::mem::pm_manages::{PMActor, PMRequest, PMResult}; use crate::mem::pm_manages::{PMActor, PMRequest, PMResult};
use crate::mem::buddy::BuddyAllocator;
pub mod cap; pub mod cap;
pub mod cpu; pub mod cpu;
@@ -175,28 +176,42 @@ unsafe extern "C" fn kmain() -> ! {
let hhdm_res = HHDM_REQUEST.get_response().expect("Limine: No HHDM"); 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 kaddr_res = KERNEL_ADDR_REQUEST.get_response().expect("Limine: No Kernel Address");
let hhdm_offset = hhdm_res.offset(); 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 fb = fb_res.framebuffers().next().expect("Limine: No active framebuffer found");
let mut console = tty::Console::new(&fb, KERNEL_FONT); let mut console = tty::Console::new(&fb, KERNEL_FONT);
console.clear(); console.clear();
debug::serial::init_global();
cpu::interrupts::init_early_exceptions();
info!(console, "BOOT", "LIS4 Kernel Starting..."); 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."); info!(console, "MEM", "Primary Physical Memory Manager (BitmapPMM) initialized.");
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"); let p4_phys = mem::pmm::alloc_frame().expect("OOM: Failed to allocate P4 table");
let p4 = unsafe { &mut *p4_phys.to_virt(hhdm_offset).as_mut_ptr::<PageTable>() }; info!(console, "BOOT", "alloc_frame ok: phys=0x{:x}", p4_phys.0);
let virt = p4_phys.to_virt();
info!(console, "BOOT", "virt=0x{:x}", virt.0);
let p4 = unsafe { &mut *virt.as_mut_ptr::<PageTable>() };
info!(console, "BOOT", "zeroing page...");
unsafe { core::ptr::write_bytes(p4 as *mut _ as *mut u8, 0, 4096); } unsafe { core::ptr::write_bytes(p4 as *mut _ as *mut u8, 0, 4096); }
info!(console, "BOOT", "zero done");
let flags = PageTableFlags::PRESENT | PageTableFlags::WRITABLE; let flags = PageTableFlags::PRESENT | PageTableFlags::WRITABLE;
for entry in mmap_res.entries() { for (i, entry) in mmap_res.entries().iter().enumerate() {
let phys = PhysAddr(entry.base); let phys = PhysAddr(entry.base);
let virt_hhdm = phys.to_virt(hhdm_offset); let virt_hhdm = phys.to_virt();
p4.map_region(virt_hhdm, phys, entry.length, flags, hhdm_offset); p4.map_region(virt_hhdm, phys, entry.length, flags);
if entry.entry_type != limine::memory_map::EntryType::RESERVED { 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);
} }
} }
@@ -205,7 +220,6 @@ unsafe extern "C" fn kmain() -> ! {
PhysAddr(kaddr_res.physical_base()), PhysAddr(kaddr_res.physical_base()),
0x1000 * 1024, 0x1000 * 1024,
flags, flags,
hhdm_offset
); );
info!(console, "MMU", "Activating Kernel Page Tables..."); info!(console, "MMU", "Activating Kernel Page Tables...");
@@ -215,7 +229,7 @@ unsafe extern "C" fn kmain() -> ! {
let heap_size = 8 * 1024 * 1024; let heap_size = 8 * 1024 * 1024;
for i in (0..heap_size).step_by(4096) { for i in (0..heap_size).step_by(4096) {
let frame = mem::pmm::alloc_frame().expect("OOM: Heap allocation failed"); 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);
} }
{ {
@@ -225,7 +239,7 @@ unsafe extern "C" fn kmain() -> ! {
info!(console, "HEAP", "Kernel Slab Allocator is online."); info!(console, "HEAP", "Kernel Slab Allocator is online.");
mem::init_cpu_features(); 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."); info!(console, "VMM", "Kernel Address Space registered successfully.");
cpu::interrupts::init_idt(); cpu::interrupts::init_idt();
@@ -354,6 +368,74 @@ unsafe extern "C" fn kmain() -> ! {
panic!("CRITICAL STATE LOSS: Memory leak detected inside PMActor context!"); 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#" let logo = r#"
########### ###########
################## ##################
@@ -379,6 +461,7 @@ unsafe extern "C" fn kmain() -> ! {
} }
fn hcf() -> ! { fn hcf() -> ! {
unsafe { asm!("cli", options(nomem, nostack, preserves_flags)); }
loop { loop {
unsafe { asm!("hlt", options(nomem, nostack, preserves_flags)); } unsafe { asm!("hlt", options(nomem, nostack, preserves_flags)); }
} }
@@ -386,7 +469,6 @@ fn hcf() -> ! {
#[panic_handler] #[panic_handler]
fn rust_panic(info: &core::panic::PanicInfo) -> ! { fn rust_panic(info: &core::panic::PanicInfo) -> ! {
let mut sp = unsafe { debug::serial::SerialPort::init() }; debug::serial::write_global(format_args!("KERNEL PANIC: {:?}\n", info));
let _ = writeln!(sp, "KERNEL PANIC: {:?}", info);
hcf(); hcf();
} }

View File

@@ -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)] #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
#[repr(transparent)] #[repr(transparent)]
pub struct PhysAddr(pub u64); pub struct PhysAddr(pub u64);
@@ -8,8 +24,8 @@ pub struct VirtAddr(pub u64);
impl PhysAddr { impl PhysAddr {
/// Convert physical address to virtual via HHDM offset /// Convert physical address to virtual via HHDM offset
pub fn to_virt(self, hhdm_offset: u64) -> VirtAddr { pub fn to_virt(self) -> VirtAddr {
VirtAddr(self.0 + hhdm_offset) VirtAddr(self.0 + get_hhdm())
} }
#[allow(dead_code)] #[allow(dead_code)]
@@ -24,9 +40,10 @@ impl PhysAddr {
impl VirtAddr { impl VirtAddr {
#[allow(dead_code)] #[allow(dead_code)]
pub fn to_phys(self, hhdm_offset: u64) -> Option<PhysAddr> { pub fn to_phys(self) -> Option<PhysAddr> {
if self.0 < hhdm_offset { return None; } let hhdm = get_hhdm();
Some(PhysAddr(self.0 - hhdm_offset)) if self.0 < hhdm { return None; }
Some(PhysAddr(self.0 - hhdm))
} }
#[allow(dead_code)] #[allow(dead_code)]

View File

@@ -55,8 +55,14 @@ struct ListNode {
const BLOCK_SIZES: &[usize] = &[8, 16, 32, 64, 128, 256, 512, 1024, 2048]; const BLOCK_SIZES: &[usize] = &[8, 16, 32, 64, 128, 256, 512, 1024, 2048];
struct LargeBlockNode {
size: usize,
next: Option<&'static mut LargeBlockNode>,
}
pub struct SlabAllocator { pub struct SlabAllocator {
list_heads: [Option<&'static mut ListNode>; BLOCK_SIZES.len()], list_heads: [Option<&'static mut ListNode>; BLOCK_SIZES.len()],
large_block_free: Option<&'static mut LargeBlockNode>,
heap_start: usize, heap_start: usize,
heap_end: usize, heap_end: usize,
next_bump: usize, next_bump: usize,
@@ -66,6 +72,7 @@ impl SlabAllocator {
pub const fn new() -> Self { pub const fn new() -> Self {
Self { Self {
list_heads: [None, None, None, None, None, None, None, None, None], list_heads: [None, None, None, None, None, None, None, None, None],
large_block_free: None,
heap_start: 0, heap_start: 0,
heap_end: 0, heap_end: 0,
next_bump: 0, next_bump: 0,
@@ -84,11 +91,28 @@ impl SlabAllocator {
} }
fn fallback_alloc(&mut self, layout: Layout) -> *mut u8 { fn fallback_alloc(&mut self, layout: Layout) -> *mut u8 {
let size = layout.size().max(layout.align());
if size > 2048 {
let mut field: *mut Option<&'static mut LargeBlockNode> = &mut self.large_block_free;
unsafe {
while let Some(ref mut node) = *field {
if node.size >= size {
let node_ptr: *mut LargeBlockNode = *node;
let next = node.next.take();
*field = next;
return node_ptr as *mut u8;
}
field = &mut node.next;
}
}
}
let alloc_start = (self.next_bump + layout.align() - 1) & !(layout.align() - 1); let alloc_start = (self.next_bump + layout.align() - 1) & !(layout.align() - 1);
let alloc_end = alloc_start.checked_add(layout.size()).unwrap_or(self.heap_end + 1); let alloc_end = alloc_start.checked_add(layout.size()).unwrap_or(self.heap_end + 1);
if alloc_end > self.heap_end { if alloc_end > self.heap_end {
null_mut() // Out of memory null_mut()
} else { } else {
self.next_bump = alloc_end; self.next_bump = alloc_end;
alloc_start as *mut u8 alloc_start as *mut u8
@@ -139,6 +163,17 @@ unsafe impl GlobalAlloc for Locked<SlabAllocator> {
} }
} }
None => { None => {
// Large block: add to free list for reuse
let size = layout.size().max(layout.align());
let new_node = LargeBlockNode {
size,
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);
}
} }
} }
} }

View File

@@ -1,105 +1,99 @@
//! # Per-Actor Buddy Allocator (`src/mem/buddy.rs`) use crate::mem::address::{PhysAddr, VirtAddr};
//!
//! 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<usize>`
//! 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.
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; pub const MAX_ORDER: usize = 11;
/// ⌈log2(n)⌉ — the minimum order whose block size covers `page_count` pages. const NEXT_SENTINEL: usize = usize::MAX;
///
/// ```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
/// ```
#[inline] #[inline]
pub fn order_for(page_count: usize) -> usize { pub fn order_for(page_count: usize) -> usize {
if page_count <= 1 { if page_count <= 1 {
0 0
} else { } else {
// Number of bits needed to represent (page_count - 1).
usize::BITS as usize - (page_count - 1).leading_zeros() as usize 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 { pub struct BuddyAllocator {
/// `free_lists[k]` = relative page indices of free 2^k-page blocks. free_heads: [usize; MAX_ORDER + 1],
/// Ordering within each list is irrelevant; `pop()` / `swap_remove()` are used.
free_lists: [Vec<usize>; MAX_ORDER + 1],
/// Total pages in the managed range (need not be a power of two).
total_pages: usize, total_pages: usize,
/// Running count of free pages. Always equals `Σ (2^k × free_lists[k].len())`.
free_pages: usize, free_pages: usize,
base_phys: u64,
} }
impl BuddyAllocator { impl BuddyAllocator {
//Construction #[inline]
/// Create a new allocator over `total_pages` pages, **all initially free**. fn page_virt(&self, idx: usize) -> VirtAddr {
/// PhysAddr(self.base_phys + idx as u64 * 4096).to_virt()
/// 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<usize>; MAX_ORDER + 1] =
core::array::from_fn(|_| Vec::new());
unsafe fn write_node(&self, idx: usize, next: usize, prev: usize) {
unsafe {
let ptr = self.page_virt(idx).as_mut_ptr::<usize>();
ptr.write(next);
ptr.add(1).write(prev);
}
}
unsafe fn read_next(&self, idx: usize) -> usize {
unsafe { self.page_virt(idx).as_ptr::<usize>().read() }
}
unsafe fn read_prev(&self, idx: usize) -> usize {
unsafe { self.page_virt(idx).as_ptr::<usize>().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<usize> {
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 { let mut this = Self {
free_lists, free_heads: [NEXT_SENTINEL; MAX_ORDER + 1],
total_pages, total_pages,
free_pages: 0, free_pages: 0,
base_phys,
}; };
if total_pages == 0 { if total_pages == 0 {
@@ -110,22 +104,19 @@ impl BuddyAllocator {
while idx < total_pages { while idx < total_pages {
let remaining = total_pages - idx; 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 { let align_order = if idx == 0 {
MAX_ORDER MAX_ORDER
} else { } else {
(idx.trailing_zeros() as usize).min(MAX_ORDER) (idx.trailing_zeros() as usize).min(MAX_ORDER)
}; };
// Size constraint: 2^order ≤ remaining → order ≤ ⌊log₂(remaining)⌋.
let size_order = (usize::BITS as usize - 1) 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; let block_size = 1usize << order;
this.free_lists[order].push(idx); this.flist_push(order, idx);
this.free_pages += block_size; this.free_pages += block_size;
idx += block_size; idx += block_size;
} }
@@ -133,60 +124,30 @@ impl BuddyAllocator {
this 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<usize> { pub fn alloc(&mut self, order: usize) -> Option<usize> {
if order > MAX_ORDER { if order > MAX_ORDER {
return None; return None;
} }
// Find the smallest available order ≥ requested.
let found_order = (order..=MAX_ORDER) 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.flist_pop(found_order)
let block_idx = self.free_lists[found_order] .expect("buddy: free_list non-empty but flist_pop returned None");
.pop()
.expect("buddy: free_list non-empty but pop() returned None");
// Debit: we removed a 2^found_order block from free.
self.free_pages -= 1 << found_order; 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; let mut cur_order = found_order;
while cur_order > order { while cur_order > order {
cur_order -= 1; cur_order -= 1;
let buddy_idx = block_idx + (1 << cur_order); 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; 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) 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)> { pub fn alloc_pages(&mut self, page_count: usize) -> Option<(usize, usize)> {
if page_count == 0 { if page_count == 0 {
return None; return None;
@@ -198,17 +159,6 @@ impl BuddyAllocator {
self.alloc(order).map(|idx| (idx, order)) 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) { pub fn free(&mut self, mut block_idx: usize, mut order: usize) {
debug_assert!( debug_assert!(
order <= MAX_ORDER, order <= MAX_ORDER,
@@ -224,67 +174,60 @@ impl BuddyAllocator {
self.total_pages 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 { while order < MAX_ORDER {
let buddy_idx = block_idx ^ (1 << 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) { let buddy_end = match buddy_idx.checked_add(1 << order) {
Some(e) => e, Some(e) => e,
None => break, None => break,
}; };
if buddy_end > self.total_pages { if buddy_end > self.total_pages {
break; break;
} }
// Search for the buddy in the free list for this order. if self.flist_contains(order, buddy_idx) {
// `swap_remove` is O(1) and order-preserving is not required. self.flist_remove(order, buddy_idx);
if let Some(pos) = self.free_lists[order].iter().position(|&b| b == buddy_idx) { self.free_pages -= 1 << order;
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.
block_idx = block_idx.min(buddy_idx); block_idx = block_idx.min(buddy_idx);
order += 1; order += 1;
// Continue trying to merge at the next level.
} else { } 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_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] #[inline]
pub fn free_pages(&self) -> usize { pub fn free_pages(&self) -> usize {
self.free_pages self.free_pages
} }
/// Total pages in the managed range.
#[inline] #[inline]
pub fn total_pages(&self) -> usize { pub fn total_pages(&self) -> usize {
self.total_pages self.total_pages
} }
/// `true` if the allocator has no free pages.
#[inline] #[inline]
pub fn is_exhausted(&self) -> bool { pub fn is_exhausted(&self) -> bool {
self.free_pages == 0 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] { 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)
})
} }
} }

View File

@@ -12,8 +12,8 @@ pub mod pm_router;
/// Initialise the physical memory manager. /// Initialise the physical memory manager.
/// ///
/// Must be called before any heap allocation or VMM operation. /// Must be called before any heap allocation or VMM operation.
pub fn init_pmm(memmap: &limine::response::MemoryMapResponse, hhdm_offset: u64) { pub fn init_pmm(memmap: &limine::response::MemoryMapResponse) {
unsafe { pmm::BitmapPMM::init(memmap, hhdm_offset); } unsafe { pmm::BitmapPMM::init(memmap); }
} }
/// Initialise CPU features required by the VMM (INVPCID detection). /// 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 /// Initialise the kernel address space record (after PMM, heap, and the
/// initial P4 page table have been set up in `kmain`). /// initial P4 page table have been set up in `kmain`).
pub fn init_vmm(pml4_phys: address::PhysAddr, hhdm_offset: u64) { pub fn init_vmm(pml4_phys: address::PhysAddr) {
vmm::init_kernel_space(pml4_phys, hhdm_offset); vmm::init_kernel_space(pml4_phys);
} }
/// Physical memory statistics: `(used_pages, total_pages)`. /// Physical memory statistics: `(used_pages, total_pages)`.

View File

@@ -40,16 +40,15 @@ impl PageTable {
phys: PhysAddr, phys: PhysAddr,
size: u64, size: u64,
flags: PageTableFlags, flags: PageTableFlags,
hhdm: u64,
) { ) {
let pages = size.div_ceil(4096); let pages = size.div_ceil(4096);
for i in 0..pages { for i in 0..pages {
let offset = i * 4096; 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<PageTableFlags> { pub fn get_flags(&self, virt: VirtAddr) -> Option<PageTableFlags> {
let p4_idx = ((virt.0 >> 39) & 0x1FF) as usize; let p4_idx = ((virt.0 >> 39) & 0x1FF) as usize;
let p3_idx = ((virt.0 >> 30) & 0x1FF) as usize; let p3_idx = ((virt.0 >> 30) & 0x1FF) as usize;
let p2_idx = ((virt.0 >> 21) & 0x1FF) as usize; let p2_idx = ((virt.0 >> 21) & 0x1FF) as usize;
@@ -59,7 +58,7 @@ impl PageTable {
($entry:expr) => {{ ($entry:expr) => {{
let e = $entry; let e = $entry;
if e & PageTableFlags::PRESENT.bits() == 0 { return None; } if e & PageTableFlags::PRESENT.bits() == 0 { return None; }
unsafe { &*PhysAddr(e & PTE_ADDR_MASK).to_virt(hhdm).as_mut_ptr::<PageTable>() } unsafe { &*PhysAddr(e & PTE_ADDR_MASK).to_virt().as_mut_ptr::<PageTable>() }
}}; }};
} }
@@ -82,8 +81,8 @@ impl PageTable {
Some(PageTableFlags::from_bits_truncate(p1e)) Some(PageTableFlags::from_bits_truncate(p1e))
} }
pub fn update_flags(&mut self, virt: VirtAddr, flags: PageTableFlags, hhdm: u64) -> Result<(), ()> { pub fn update_flags(&mut self, virt: VirtAddr, flags: PageTableFlags) -> Result<(), ()> {
let Some(p1) = self.walk_to_p1_mut(virt, hhdm, false) else { return Err(()); }; let Some(p1) = self.walk_to_p1_mut(virt, false) else { return Err(()); };
let p1_idx = ((virt.0 >> 12) & 0x1FF) as usize; let p1_idx = ((virt.0 >> 12) & 0x1FF) as usize;
let entry = p1.entries[p1_idx]; let entry = p1.entries[p1_idx];
@@ -107,10 +106,9 @@ impl PageTable {
virt: VirtAddr, virt: VirtAddr,
phys: PhysAddr, phys: PhysAddr,
flags: PageTableFlags, flags: PageTableFlags,
hhdm: u64,
) { ) {
let p1 = self 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"); .expect("map_page: OOM allocating intermediate page-table pages");
let p1_idx = ((virt.0 >> 12) & 0x1FF) as usize; 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 /// Does **not** free the underlying physical frame — that is the caller's
/// responsibility (e.g. `VmaBacking::do_unmap`). /// responsibility (e.g. `VmaBacking::do_unmap`).
/// Silently returns if any level is not present (idempotent). /// Silently returns if any level is not present (idempotent).
pub fn unmap_page(&mut self, virt: VirtAddr, hhdm: u64) { pub fn unmap_page(&mut self, virt: VirtAddr) {
let Some(p1) = self.walk_to_p1_mut(virt, hhdm, false /* no create */) else { let Some(p1) = self.walk_to_p1_mut(virt, false /* no create */) else {
return; // already absent — nothing to do return; // already absent — nothing to do
}; };
let p1_idx = ((virt.0 >> 12) & 0x1FF) as usize; let p1_idx = ((virt.0 >> 12) & 0x1FF) as usize;
@@ -144,7 +142,7 @@ impl PageTable {
/// ///
/// Handles 1 GiB and 2 MiB huge pages transparently. /// Handles 1 GiB and 2 MiB huge pages transparently.
/// Returns `None` if any level is absent or the page is not present. /// Returns `None` if any level is absent or the page is not present.
pub fn translate(&self, virt: VirtAddr, hhdm: u64) -> Option<PhysAddr> { pub fn translate(&self, virt: VirtAddr) -> Option<PhysAddr> {
let p4_idx = ((virt.0 >> 39) & 0x1FF) as usize; let p4_idx = ((virt.0 >> 39) & 0x1FF) as usize;
let p3_idx = ((virt.0 >> 30) & 0x1FF) as usize; let p3_idx = ((virt.0 >> 30) & 0x1FF) as usize;
let p2_idx = ((virt.0 >> 21) & 0x1FF) as usize; let p2_idx = ((virt.0 >> 21) & 0x1FF) as usize;
@@ -155,7 +153,7 @@ impl PageTable {
($entry:expr) => {{ ($entry:expr) => {{
let e = $entry; let e = $entry;
if e & PageTableFlags::PRESENT.bits() == 0 { return None; } if e & PageTableFlags::PRESENT.bits() == 0 { return None; }
unsafe { &*PhysAddr(e & PTE_ADDR_MASK).to_virt(hhdm).as_mut_ptr::<PageTable>() } unsafe { &*PhysAddr(e & PTE_ADDR_MASK).to_virt().as_mut_ptr::<PageTable>() }
}}; }};
} }
@@ -209,16 +207,16 @@ impl PageTable {
/// ///
/// If `create` is `false` and any intermediate entry is absent, returns `None`. /// If `create` is `false` and any intermediate entry is absent, returns `None`.
/// If `create` is `true`, allocates missing intermediate pages from the PMM. /// 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> -> Option<&mut Self>
{ {
let p4_idx = ((virt.0 >> 39) & 0x1FF) as usize; let p4_idx = ((virt.0 >> 39) & 0x1FF) as usize;
let p3_idx = ((virt.0 >> 30) & 0x1FF) as usize; let p3_idx = ((virt.0 >> 30) & 0x1FF) as usize;
let p2_idx = ((virt.0 >> 21) & 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 p3 = self.get_or_create_next_table(p4_idx, create)?;
let p2 = p3.get_or_create_next_table(p3_idx, hhdm, create)?; let p2 = p3.get_or_create_next_table(p3_idx, create)?;
p2.get_or_create_next_table(p2_idx, hhdm, create) p2.get_or_create_next_table(p2_idx, create)
} }
/// Return a mutable reference to the next-level table at `index`. /// Return a mutable reference to the next-level table at `index`.
@@ -229,7 +227,6 @@ impl PageTable {
fn get_or_create_next_table( fn get_or_create_next_table(
&mut self, &mut self,
index: usize, index: usize,
hhdm: u64,
create: bool, create: bool,
) -> Option<&mut Self> { ) -> Option<&mut Self> {
let entry = self.entries[index]; let entry = self.entries[index];
@@ -237,7 +234,7 @@ impl PageTable {
if !create { return None; } if !create { return None; }
let pt_phys = pmm_alloc().expect("VMM: OOM allocating page-table page"); 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::<u8>(), 0, 4096); } unsafe { core::ptr::write_bytes(pt_virt.as_mut_ptr::<u8>(), 0, 4096); }
// Install with USER so both kernel and user pages can live under it; // 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); let next_phys = PhysAddr(self.entries[index] & PTE_ADDR_MASK);
Some(unsafe { &mut *next_phys.to_virt(hhdm).as_mut_ptr::<Self>() }) Some(unsafe { &mut *next_phys.to_virt().as_mut_ptr::<Self>() })
} }
} }

View File

@@ -94,7 +94,6 @@ pub enum PMRequest {
size_pages: usize, size_pages: usize,
channel_id: u16, channel_id: u16,
}, },
/// Sentinel — never pushed onto the queue; result of unpack(0).
None, None,
} }
@@ -160,30 +159,18 @@ impl PMRequest {
// Response type // Response type
/// Result returned by `PMActor::process_messages()` for each completed request.
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
pub struct PMResponse { pub struct PMResponse {
/// Channel to route this response to. `0` = discard.
pub channel_id: u16, pub channel_id: u16,
/// The actual outcome.
pub result: PMResult, pub result: PMResult,
} }
/// Outcome of a single PM operation. /// Outcome of a single PM operation.
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
pub enum PMResult { pub enum PMResult {
/// Memory was allocated. `cap` is the strong capability to the region.
/// `order` is the buddy order — **must** be passed back to `Free`.
Allocated { cap: Capability, order: usize }, Allocated { cap: Capability, order: usize },
/// The actor had insufficient free pages.
/// Future: ballooning subsystem intercepts this and retries.
OutOfMemory { size_pages: usize }, OutOfMemory { size_pages: usize },
/// A fixed sub-region was carved successfully.
Carved { cap: Capability }, Carved { cap: Capability },
/// Free completed (no capability issued — memory returned to buddy pool).
Freed { pages_returned: usize }, Freed { pages_returned: usize },
} }
@@ -194,7 +181,6 @@ pub enum PMResult {
/// `pop` — exactly one consumer, no locking on the read side. /// `pop` — exactly one consumer, no locking on the read side.
pub struct PMActorQueue { pub struct PMActorQueue {
buffer: [AtomicU64; QUEUE_SIZE], buffer: [AtomicU64; QUEUE_SIZE],
/// Pad to separate producer-written `tail` from consumer-read `head`.
_pad0: [u8; 64], _pad0: [u8; 64],
head: AtomicUsize, head: AtomicUsize,
_pad1: [u8; 64], _pad1: [u8; 64],
@@ -269,15 +255,10 @@ impl PMActorQueue {
/// Only `process_messages()` ever mutates `buddy` and `queue.head`. /// Only `process_messages()` ever mutates `buddy` and `queue.head`.
/// This is enforced by taking `&mut self` on `process_messages`. /// This is enforced by taking `&mut self` on `process_messages`.
pub struct PMActor { pub struct PMActor {
/// Unique identity within the actor federation.
pub actor_id: u64, pub actor_id: u64,
/// Root strong capability over the entire managed physical range.
pub root_untyped: Capability, pub root_untyped: Capability,
/// `(inclusive_start, exclusive_end)` physical addresses.
pub managed_range: (PhysAddr, PhysAddr), pub managed_range: (PhysAddr, PhysAddr),
/// Inbox — producers write here, actor reads here.
queue: PMActorQueue, queue: PMActorQueue,
/// Local buddy allocator. Only ever touched in `process_messages`.
buddy: BuddyAllocator, buddy: BuddyAllocator,
} }
@@ -298,7 +279,7 @@ impl PMActor {
root_untyped: root_cap, root_untyped: root_cap,
managed_range: (start, PhysAddr(start.0 + total_pages as u64 * PAGE_SIZE)), managed_range: (start, PhysAddr(start.0 + total_pages as u64 * PAGE_SIZE)),
queue: PMActorQueue::new(), queue: PMActorQueue::new(),
buddy: BuddyAllocator::new(total_pages), buddy: BuddyAllocator::new(total_pages, start.0),
} }
} }
@@ -325,10 +306,16 @@ impl PMActor {
/// `buddy` and `queue.head` are mutated — no other thread touches them. /// `buddy` and `queue.head` are mutated — no other thread touches them.
/// The only shared state is the queue's `tail`, which is written by producers /// The only shared state is the queue's `tail`, which is written by producers
/// via `AtomicUsize::compare_exchange_weak`, never by this path. /// via `AtomicUsize::compare_exchange_weak`, never by this path.
/// Maximum requests to process in a single `process_messages` call.
/// Prevents kernel starvation when the inbox is deep.
const MAX_MESSAGES_PER_CALL: usize = 64;
pub fn process_messages(&mut self) -> Vec<PMResponse> { pub fn process_messages(&mut self) -> Vec<PMResponse> {
let mut responses = Vec::new(); let mut responses = Vec::new();
let mut remaining = Self::MAX_MESSAGES_PER_CALL;
while let Some(req) = self.queue.pop() { while let Some(req) = self.queue.pop() {
remaining -= 1;
let resp = match req { let resp = match req {
PMRequest::Allocate { size_pages, token_sig, channel_id } => { PMRequest::Allocate { size_pages, token_sig, channel_id } => {
self.handle_allocate(size_pages, token_sig, channel_id) self.handle_allocate(size_pages, token_sig, channel_id)
@@ -342,12 +329,13 @@ impl PMActor {
PMRequest::None => continue, PMRequest::None => continue,
}; };
// Only push responses that need routing.
// Free responses (channel_id == 0) are still pushed so callers can
// audit completion if needed; they may simply drop them.
if let Some(r) = resp { if let Some(r) = resp {
responses.push(r); responses.push(r);
} }
if remaining == 0 {
break;
}
} }
responses responses
@@ -434,9 +422,6 @@ impl PMActor {
self.buddy.free(local_frame_idx, order); self.buddy.free(local_frame_idx, order);
// Free never needs routing — return None to skip Vec push, or push with
// channel_id=0 for audit purposes. We skip to avoid unnecessary allocation.
// Callers that need a Free-complete signal should use a separate mechanism.
None None
} }
@@ -446,7 +431,6 @@ impl PMActor {
size_pages: usize, size_pages: usize,
channel_id: u16, channel_id: u16,
) -> Option<PMResponse> { ) -> Option<PMResponse> {
// Validate offset + size within range.
let range_pages = self.buddy.total_pages(); let range_pages = self.buddy.total_pages();
if offset_pages >= range_pages if offset_pages >= range_pages
|| size_pages == 0 || size_pages == 0

View File

@@ -69,12 +69,21 @@ pub fn init() {
#[inline(always)] #[inline(always)]
pub fn get_router() -> &'static PMRouter { pub fn get_router() -> &'static PMRouter {
// INVARIANT: `is_ready` is set once in `init()` and never cleared.
// The Acquire load on `is_ready` synchronises-with the Release store in
// `init()`, making the `Option::Some` write visible. Because nothing
// ever writes `None` or clears `is_ready`, the TOCTOU window between
// the load and the `unwrap_unchecked` is safe.
if ROUTER.is_ready.load(Ordering::Acquire) { if ROUTER.is_ready.load(Ordering::Acquire) {
unsafe { unsafe {
(*ROUTER.inner.get()).as_ref().unwrap_unchecked() (*ROUTER.inner.get()).as_ref().unwrap_unchecked()
} }
} else { } else {
panic!("FATAL: PMRouter is accessed before initialization!") #[cold]
fn not_initialized() -> ! {
panic!("FATAL: PMRouter is accessed before initialization!")
}
not_initialized()
} }
} }
@@ -131,9 +140,10 @@ impl PMRouter {
let channel = &self.channels[id as usize]; let channel = &self.channels[id as usize];
while channel.state.load(Ordering::Acquire) != STATE_READY { while channel.state.load(Ordering::Acquire) != STATE_READY {
core::hint::spin_loop(); // HLT-ожидание вместо busy-wait: CPU останавливается до ближайшего
// TODO: Для "Focus Mode" и полноценного планировщика: // прерывания (timer tick, IPI от route_responses и т.д.).
// scheduler::yield_to_actor(); // Когда появится планировщик — заменить на yield_to_actor().
unsafe { core::arch::asm!("hlt", options(nomem, nostack, preserves_flags)); }
} }
let result = unsafe { let result = unsafe {

View File

@@ -1,20 +1,36 @@
use crate::mem::address::PhysAddr; use crate::mem::address::{PhysAddr, get_hhdm};
use crate::mem::allocator::Locked; use crate::mem::allocator::Locked;
pub const PAGE_SIZE: u64 = 4096; pub const PAGE_SIZE: u64 = 4096;
pub struct BitmapPMM { pub struct BitmapPMM {
bitmap: &'static mut [u8], bitmap: &'static mut [u8],
l1_bitmap: &'static mut [u64],
ref_counts: &'static mut [u16], ref_counts: &'static mut [u16],
total_pages: usize, total_pages: usize,
used_pages: usize, used_pages: usize,
/// Byte index hint: next search starts here to amortise O(N) scans. last_word: usize,
last_byte: usize,
} }
pub static PMM: Locked<Option<BitmapPMM>> = Locked::new(None); pub static PMM: Locked<Option<BitmapPMM>> = Locked::new(None);
impl BitmapPMM { 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)] #[allow(dead_code)]
pub fn used_pages(&self) -> usize { self.used_pages } pub fn used_pages(&self) -> usize { self.used_pages }
#[allow(dead_code)] #[allow(dead_code)]
@@ -22,7 +38,7 @@ impl BitmapPMM {
#[allow(dead_code)] #[allow(dead_code)]
pub fn free_pages(&self) -> usize { self.total_pages.saturating_sub(self.used_pages) } 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() let max_addr = mmap.entries().iter()
.map(|e| e.base + e.length) .map(|e| e.base + e.length)
.max() .max()
@@ -32,7 +48,10 @@ impl BitmapPMM {
let bitmap_size = total_pages.div_ceil(8); let bitmap_size = total_pages.div_ceil(8);
let ref_counts_size = total_pages * core::mem::size_of::<u16>(); let ref_counts_size = total_pages * core::mem::size_of::<u16>();
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() let meta_phys = mmap.entries().iter()
.find(|e| { .find(|e| {
@@ -42,8 +61,10 @@ impl BitmapPMM {
.map(|e| e.base) .map(|e| e.base)
.expect("PMM: no usable region large enough for metadata"); .expect("PMM: no usable region large enough for metadata");
let bitmap_ptr = (meta_phys + hhdm_offset) as *mut u8; let hhdm = get_hhdm();
let ref_counts_ptr = (meta_phys + hhdm_offset + bitmap_size as u64) as *mut u16; 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) }; let bitmap = unsafe { core::slice::from_raw_parts_mut(bitmap_ptr, bitmap_size) };
bitmap.fill(0xFF); bitmap.fill(0xFF);
@@ -51,12 +72,16 @@ impl BitmapPMM {
let ref_counts = unsafe { core::slice::from_raw_parts_mut(ref_counts_ptr, total_pages) }; let ref_counts = unsafe { core::slice::from_raw_parts_mut(ref_counts_ptr, total_pages) };
ref_counts.fill(1); 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 { let mut pmm = Self {
bitmap, bitmap,
l1_bitmap,
ref_counts, ref_counts,
total_pages, total_pages,
used_pages: total_pages, used_pages: total_pages,
last_byte: 0, last_word: 0,
}; };
for entry in mmap.entries() { for entry in mmap.entries() {
@@ -77,8 +102,6 @@ impl BitmapPMM {
*PMM.lock() = Some(pmm); *PMM.lock() = Some(pmm);
} }
//Core operations
pub fn free_frame(&mut self, phys_addr: PhysAddr) { pub fn free_frame(&mut self, phys_addr: PhysAddr) {
let idx = (phys_addr.0 / PAGE_SIZE) as usize; let idx = (phys_addr.0 / PAGE_SIZE) as usize;
if idx >= self.total_pages { return; } if idx >= self.total_pages { return; }
@@ -91,8 +114,11 @@ impl BitmapPMM {
if self.ref_counts[idx] == 0 { if self.ref_counts[idx] == 0 {
self.bitmap[byte] &= !(1 << bit); self.bitmap[byte] &= !(1 << bit);
self.used_pages -= 1; self.used_pages -= 1;
if byte < self.last_byte { self.last_byte = byte; }
let word_idx = idx / 64;
self.l1_update_word(word_idx);
if word_idx < self.last_word { self.last_word = word_idx; }
} }
} }
} }
@@ -107,10 +133,13 @@ impl BitmapPMM {
if self.bitmap[byte] & (1 << bit) == 0 { if self.bitmap[byte] & (1 << bit) == 0 {
self.bitmap[byte] |= 1 << bit; self.bitmap[byte] |= 1 << bit;
self.ref_counts[idx] = 1; self.ref_counts[idx] = 1;
self.used_pages += 1; self.used_pages += 1;
} else if self.ref_counts[idx] == 0 { } else if self.ref_counts[idx] == 0 {
self.ref_counts[idx] = 1; 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) { pub fn inc_ref_frame(&mut self, phys_addr: PhysAddr) {
@@ -126,31 +155,45 @@ impl BitmapPMM {
} }
pub fn alloc_frame(&mut self) -> Option<PhysAddr> { pub fn alloc_frame(&mut self) -> Option<PhysAddr> {
let len = self.bitmap.len(); let l1_len = self.l1_bitmap.len();
for pass in 0..2usize { for pass in 0..2usize {
let (from, to) = if pass == 0 { let (from, to) = if pass == 0 {
(self.last_byte, len) (self.last_word / 64, l1_len)
} else { } else {
(0, self.last_byte) (0, self.last_word / 64)
}; };
for byte_idx in from..to { for l1_idx in from..to {
if self.bitmap[byte_idx] == 0xFF { continue; } let l1_word = self.l1_bitmap[l1_idx];
if l1_word == 0 { continue; }
for bit in 0..8u8 { let word_offset = l1_word.trailing_zeros() as usize;
if self.bitmap[byte_idx] & (1 << bit) == 0 { let word_idx = l1_idx * 64 + word_offset;
let page_idx = byte_idx * 8 + bit as usize;
if page_idx >= self.total_pages { return None; }
self.bitmap[byte_idx] |= 1 << bit; let word = unsafe { self.read_word(word_idx) };
self.ref_counts[page_idx] = 1; if word == !0u64 {
self.used_pages += 1; self.l1_bitmap[l1_idx] &= !(1 << word_offset);
self.last_byte = byte_idx; continue;
return Some(PhysAddr(page_idx as u64 * PAGE_SIZE));
}
} }
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));
} }
} }
@@ -175,9 +218,11 @@ impl BitmapPMM {
for i in run_start..run_start + count { for i in run_start..run_start + count {
self.bitmap[i / 8] |= 1 << (i % 8); self.bitmap[i / 8] |= 1 << (i % 8);
self.ref_counts[i] = 1; self.ref_counts[i] = 1;
let word_idx = i / 64;
self.l1_update_word(word_idx);
} }
self.used_pages += count; 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)); return Some(PhysAddr(run_start as u64 * PAGE_SIZE));
} }
} else { } else {
@@ -189,8 +234,6 @@ impl BitmapPMM {
} }
} }
//Module-level convenience functions
pub fn alloc_frame() -> Option<PhysAddr> { pub fn alloc_frame() -> Option<PhysAddr> {
PMM.lock().as_mut()?.alloc_frame() PMM.lock().as_mut()?.alloc_frame()
} }
@@ -218,7 +261,3 @@ pub fn get_stats() -> (usize, usize) {
(0, 0) (0, 0)
} }
} }

View File

@@ -56,7 +56,7 @@ fn local_tlb_flush_asid(asid: u16) {
unsafe { unsafe {
core::arch::asm!( core::arch::asm!(
"invpcid {ty}, [{desc}]", "invpcid {ty}, [{desc}]",
ty = in(reg) 1u64, // type 1 = single-context flush ty = in(reg) 1u64,
desc = in(reg) &desc, desc = in(reg) &desc,
options(nostack, preserves_flags), options(nostack, preserves_flags),
); );
@@ -69,24 +69,14 @@ fn local_tlb_flush_asid(asid: u16) {
// Error type // Error type
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum VmError { pub enum VmError {
/// PMM returned `None` — no physical frames available.
OutOfMemory, OutOfMemory,
/// The requested virtual range overlaps an existing VMA.
RegionOverlap, RegionOverlap,
/// No VMA covers the given address.
RegionNotFound, RegionNotFound,
/// Address or size is not a multiple of 4096.
InvalidAlignment, InvalidAlignment,
/// Size is zero, or `virt + size` would overflow.
InvalidRange, InvalidRange,
/// Write fault on a read-only VMA, or exec fault on a NX VMA.
PermissionDenied, PermissionDenied,
/// Page-fault in a non-lazy (already-eager or fixed) region — hardware bug
/// or an exploit attempt; the faulting task must be killed.
UnexpectedFault, UnexpectedFault,
/// Address outside the x86-64 canonical range.
NonCanonical, NonCanonical,
/// ASID pool exhausted (> 4094 simultaneous address spaces).
AsidExhausted, AsidExhausted,
} }
@@ -143,23 +133,10 @@ impl VmaFlags {
#[derive(Debug)] #[derive(Debug)]
pub enum VmaBacking { pub enum VmaBacking {
/// Anonymous pages (stack, heap, BSS).
/// Index `i` → frame for `virt_start + i * 4096`. `None` = not yet faulted in.
Anonymous(Vec<Option<PhysAddr>>), Anonymous(Vec<Option<PhysAddr>>),
/// Fixed physical range. Frames are **not** freed on unmap.
/// Used for MMIO, identity-mapped RAM, framebuffer, DMA buffers.
Physical(PhysAddr), Physical(PhysAddr),
/// Zero-copy borrow of another actor's frames.
///
/// `phys_base` is the physical address of the first page; the mapping covers
/// exactly `(virt_end - virt_start) / 4096` pages.
/// Frames are owned by `owner_cap` and **never** freed by this VMA.
Shared { Shared {
/// Token of the capability that owns the frames.
owner_cap: u64, owner_cap: u64,
/// Physical base (first page of the shared region).
phys_base: PhysAddr, phys_base: PhysAddr,
}, },
} }
@@ -251,13 +228,13 @@ impl AsidAllocator {
} }
} }
} }
None // pool truly exhausted None
} }
/// Return an ASID to the pool. /// Return an ASID to the pool.
fn free(&mut self, asid: u16) { fn free(&mut self, asid: u16) {
if asid == 0 || asid >= 4095 { if asid == 0 || asid >= 4095 {
return; // sentinel values — never freed return;
} }
let word = asid as usize / 32; let word = asid as usize / 32;
let bit = asid as usize % 32; let bit = asid as usize % 32;
@@ -329,14 +306,12 @@ pub struct AddressSpace {
pub pml4_phys: PhysAddr, pub pml4_phys: PhysAddr,
/// Sorted (by virt_start), non-overlapping VMA list. /// Sorted (by virt_start), non-overlapping VMA list.
regions: Vec<VmaRegion>, regions: Vec<VmaRegion>,
/// HHDM offset for dereferencing page-table pages.
hhdm: u64,
} }
impl AddressSpace { impl AddressSpace {
#[inline] #[inline]
unsafe fn pml4_raw(&self) -> *mut PageTable { unsafe fn pml4_raw(&self) -> *mut PageTable {
self.pml4_phys.to_virt(self.hhdm).as_mut_ptr::<PageTable>() self.pml4_phys.to_virt().as_mut_ptr::<PageTable>()
} }
fn check_overlap(&self, start: VirtAddr, end: VirtAddr) -> Result<(), VmError> { fn check_overlap(&self, start: VirtAddr, end: VirtAddr) -> Result<(), VmError> {
@@ -349,10 +324,9 @@ impl AddressSpace {
} }
pub fn clone_for_fork(&mut self, child_cap_token: u64) -> Result<Self, VmError> { pub fn clone_for_fork(&mut self, child_cap_token: u64) -> Result<Self, VmError> {
let mut child = AddressSpace::new(self.hhdm)?; let mut child = AddressSpace::new()?;
let child_pml4 = unsafe { &mut *child.pml4_raw() }; let child_pml4 = unsafe { &mut *child.pml4_raw() };
let parent_pml4 = unsafe { &mut *self.pml4_raw() }; let parent_pml4 = unsafe { &mut *self.pml4_raw() };
let hhdm = self.hhdm;
for region in &mut self.regions { for region in &mut self.regions {
let mut child_region = VmaRegion { let mut child_region = VmaRegion {
@@ -373,10 +347,10 @@ impl AddressSpace {
match &mut region.backing { match &mut region.backing {
VmaBacking::Physical(base) => { 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, .. } => { 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) => { VmaBacking::Anonymous(frames) => {
let cow_needed = region.flags.contains(VmaFlags::WRITE); let cow_needed = region.flags.contains(VmaFlags::WRITE);
@@ -400,10 +374,10 @@ impl AddressSpace {
page_flags.remove(PageTableFlags::WRITABLE); page_flags.remove(PageTableFlags::WRITABLE);
page_flags.insert(PageTableFlags::COW); 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); child_frames[i] = Some(*frame);
} }
} }
@@ -431,12 +405,12 @@ impl AddressSpace {
} }
/// Unmap pages + free frames (if owned) for one region. /// 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 pml4 = unsafe { &mut *pml4 };
let own = region.backing.owns_frames(); let own = region.backing.owns_frames();
for i in 0..region.pages() { for i in 0..region.pages() {
let virt = VirtAddr(region.virt_start.0 + i as u64 * 4096); let virt = VirtAddr(region.virt_start.0 + i as u64 * 4096);
pml4.unmap_page(virt, hhdm); pml4.unmap_page(virt);
if own { if own {
if let Some(frame) = region.backing.phys_for_page(i) { if let Some(frame) = region.backing.phys_for_page(i) {
pmm::free_frame(frame); pmm::free_frame(frame);
@@ -446,7 +420,6 @@ impl AddressSpace {
} }
fn do_revoke_by_token(&mut self, cap_token: u64) { fn do_revoke_by_token(&mut self, cap_token: u64) {
let hhdm = self.hhdm;
let pml4 = unsafe { self.pml4_raw() }; let pml4 = unsafe { self.pml4_raw() };
let indices: Vec<usize> = self.regions let indices: Vec<usize> = self.regions
@@ -458,7 +431,7 @@ impl AddressSpace {
for idx in indices.into_iter().rev() { for idx in indices.into_iter().rev() {
let region = self.regions.remove(idx); let region = self.regions.remove(idx);
Self::do_unmap(pml4, &region, hhdm); Self::do_unmap(pml4, &region);
} }
} }
@@ -479,34 +452,24 @@ impl AddressSpace {
impl AddressSpace { impl AddressSpace {
/// Allocate a fresh, empty address space with a zeroed PML4. /// Allocate a fresh, empty address space with a zeroed PML4.
pub fn new(hhdm: u64) -> Result<Self, VmError> { pub fn new() -> Result<Self, VmError> {
let pml4_phys = pmm::alloc_frame().ok_or(VmError::OutOfMemory)?; let pml4_phys = pmm::alloc_frame().ok_or(VmError::OutOfMemory)?;
unsafe { unsafe {
core::ptr::write_bytes(pml4_phys.to_virt(hhdm).as_mut_ptr::<u8>(), 0, 4096); core::ptr::write_bytes(pml4_phys.to_virt().as_mut_ptr::<u8>(), 0, 4096);
} }
Ok(Self { Ok(Self {
asid: alloc_asid()?, asid: alloc_asid()?,
pml4_phys, pml4_phys,
regions: Vec::new(), regions: Vec::new(),
hhdm,
}) })
} }
/// Wrap an already-active PML4 (bootloader-provided kernel table). /// Wrap an already-active PML4 (bootloader-provided kernel table).
/// ASID 0 = kernel "no PCID tagging"; it is never returned to the pool. /// 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 { pub fn from_active(pml4_phys: PhysAddr, asid: u16) -> Self {
Self { asid, pml4_phys, regions: Vec::new(), hhdm } Self { asid, pml4_phys, regions: Vec::new() }
} }
// ─── Anonymous / fixed mapping ─────────────────────────────────────────
/// Map `size` bytes of virtual space starting at `virt`.
///
/// | `phys` | `VmaFlags::LAZY` | Behaviour |
/// |-----------|------------------|-----------------------------------------------|
/// | `Some(p)` | any | Fixed physical (MMIO / identity / DMA) |
/// | `None` | not set | Eager anonymous — allocate + zero + map now |
/// | `None` | set | Lazy anonymous — map frames on first fault |
pub fn map_region( pub fn map_region(
&mut self, &mut self,
virt: VirtAddr, virt: VirtAddr,
@@ -526,11 +489,10 @@ impl AddressSpace {
let pml4 = unsafe { &mut *self.pml4_raw() }; let pml4 = unsafe { &mut *self.pml4_raw() };
let page_flags = flags.to_page_flags(); let page_flags = flags.to_page_flags();
let page_count = (size / 4096) as usize; let page_count = (size / 4096) as usize;
let hhdm = self.hhdm;
let backing = match phys { let backing = match phys {
Some(base) => { Some(base) => {
pml4.map_region(virt, base, size, page_flags, hhdm); pml4.map_region(virt, base, size, page_flags);
VmaBacking::Physical(base) VmaBacking::Physical(base)
} }
None if flags.contains(VmaFlags::LAZY) => { None if flags.contains(VmaFlags::LAZY) => {
@@ -543,13 +505,12 @@ impl AddressSpace {
for i in 0..page_count { for i in 0..page_count {
let frame = pmm::alloc_frame().ok_or(VmError::OutOfMemory)?; let frame = pmm::alloc_frame().ok_or(VmError::OutOfMemory)?;
unsafe { unsafe {
core::ptr::write_bytes(frame.to_virt(hhdm).as_mut_ptr::<u8>(), 0, 4096); core::ptr::write_bytes(frame.to_virt().as_mut_ptr::<u8>(), 0, 4096);
} }
pml4.map_page( pml4.map_page(
VirtAddr(virt.0 + i as u64 * 4096), VirtAddr(virt.0 + i as u64 * 4096),
frame, frame,
page_flags, page_flags,
hhdm,
); );
frames.push(Some(frame)); frames.push(Some(frame));
} }
@@ -589,7 +550,6 @@ impl AddressSpace {
let pml4 = unsafe { &mut *self.pml4_raw() }; let pml4 = unsafe { &mut *self.pml4_raw() };
let page_flags = flags.to_page_flags(); let page_flags = flags.to_page_flags();
let hhdm = self.hhdm;
// Eagerly map all pages — the physical addresses are already known. // Eagerly map all pages — the physical addresses are already known.
for i in 0..page_count { for i in 0..page_count {
@@ -597,7 +557,6 @@ impl AddressSpace {
VirtAddr(virt.0 + i as u64 * 4096), VirtAddr(virt.0 + i as u64 * 4096),
PhysAddr(phys_base.0 + i as u64 * 4096), PhysAddr(phys_base.0 + i as u64 * 4096),
page_flags, page_flags,
hhdm,
); );
} }
@@ -617,7 +576,6 @@ impl AddressSpace {
/// Returns `Ok(())` if the fault was a valid lazy demand-page (caller should /// Returns `Ok(())` if the fault was a valid lazy demand-page (caller should
/// `iretq` to retry). Returns `Err` for illegal accesses. /// `iretq` to retry). Returns `Err` for illegal accesses.
pub fn handle_fault(&mut self, fault_addr: VirtAddr, write: bool) -> Result<(), VmError> { 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 idx = self.find_idx(fault_addr).ok_or(VmError::RegionNotFound)?;
let (virt_start, region_flags) = { let (virt_start, region_flags) = {
@@ -633,7 +591,7 @@ impl AddressSpace {
let page_virt = VirtAddr(virt_start.0 + page_idx as u64 * 4096); let page_virt = VirtAddr(virt_start.0 + page_idx as u64 * 4096);
let pml4 = unsafe { &mut *self.pml4_raw() }; 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)); let is_cow = current_pte_flags.map_or(false, |f| f.contains(PageTableFlags::COW));
if write && is_cow { if write && is_cow {
@@ -645,10 +603,12 @@ impl AddressSpace {
let old_frame = frames[page_idx].expect("COW fault on unmapped page"); let old_frame = frames[page_idx].expect("COW fault on unmapped page");
let new_frame = pmm::alloc_frame().ok_or(VmError::OutOfMemory)?; let new_frame = pmm::alloc_frame().ok_or(VmError::OutOfMemory)?;
debug_assert!(old_frame != new_frame, "COW: old and new frame are the same!");
unsafe { unsafe {
core::ptr::copy_nonoverlapping( core::ptr::copy_nonoverlapping(
old_frame.to_virt(hhdm).as_ptr::<u8>(), old_frame.to_virt().as_ptr::<u8>(),
new_frame.to_virt(hhdm).as_mut_ptr::<u8>(), new_frame.to_virt().as_mut_ptr::<u8>(),
4096, 4096,
); );
} }
@@ -659,7 +619,7 @@ impl AddressSpace {
target_flags.remove(PageTableFlags::COW); target_flags.remove(PageTableFlags::COW);
target_flags.insert(PageTableFlags::WRITABLE); 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); pmm::free_frame(old_frame);
@@ -681,7 +641,7 @@ impl AddressSpace {
let frame = pmm::alloc_frame().ok_or(VmError::OutOfMemory)?; let frame = pmm::alloc_frame().ok_or(VmError::OutOfMemory)?;
unsafe { unsafe {
core::ptr::write_bytes(frame.to_virt(hhdm).as_mut_ptr::<u8>(), 0, 4096); core::ptr::write_bytes(frame.to_virt().as_mut_ptr::<u8>(), 0, 4096);
} }
frames[page_idx] = Some(frame); frames[page_idx] = Some(frame);
@@ -690,7 +650,7 @@ impl AddressSpace {
target_flags.remove(PageTableFlags::WRITABLE); target_flags.remove(PageTableFlags::WRITABLE);
} }
pml4.map_page(page_virt, frame, target_flags, hhdm); pml4.map_page(page_virt, frame, target_flags);
Ok(()) Ok(())
} }
@@ -699,7 +659,7 @@ impl AddressSpace {
let idx = self.find_idx(virt).ok_or(VmError::RegionNotFound)?; let idx = self.find_idx(virt).ok_or(VmError::RegionNotFound)?;
let region = self.regions.remove(idx); let region = self.regions.remove(idx);
let pml4 = unsafe { self.pml4_raw() }; let pml4 = unsafe { self.pml4_raw() };
Self::do_unmap(pml4, &region, self.hhdm); Self::do_unmap(pml4, &region);
tlb_flush_asid(self.asid); tlb_flush_asid(self.asid);
Ok(()) Ok(())
} }
@@ -709,7 +669,6 @@ impl AddressSpace {
/// Hardware access is terminated before this function returns. /// Hardware access is terminated before this function returns.
/// Shared-backed VMAs are unmapped from the PT without freeing frames. /// Shared-backed VMAs are unmapped from the PT without freeing frames.
pub fn revoke_by_token(&mut self, cap_token: u64) { pub fn revoke_by_token(&mut self, cap_token: u64) {
let hhdm = self.hhdm;
let pml4 = unsafe { self.pml4_raw() }; let pml4 = unsafe { self.pml4_raw() };
let indices: Vec<usize> = self.regions let indices: Vec<usize> = self.regions
@@ -723,7 +682,7 @@ impl AddressSpace {
for idx in indices.into_iter().rev() { for idx in indices.into_iter().rev() {
let region = self.regions.remove(idx); let region = self.regions.remove(idx);
Self::do_unmap(pml4, &region, hhdm); Self::do_unmap(pml4, &region);
} }
tlb_flush_asid(self.asid); tlb_flush_asid(self.asid);
@@ -733,7 +692,7 @@ impl AddressSpace {
/// Walk the live page table to translate `virt` → physical address. /// Walk the live page table to translate `virt` → physical address.
pub fn translate(&self, virt: VirtAddr) -> Option<PhysAddr> { pub fn translate(&self, virt: VirtAddr) -> Option<PhysAddr> {
unsafe { (*self.pml4_raw()).translate(virt, self.hhdm) } unsafe { (*self.pml4_raw()).translate(virt) }
} }
//Activation //Activation
@@ -767,10 +726,9 @@ impl AddressSpace {
impl Drop for AddressSpace { impl Drop for AddressSpace {
fn drop(&mut self) { fn drop(&mut self) {
let hhdm = self.hhdm;
let pml4 = unsafe { self.pml4_raw() }; let pml4 = unsafe { self.pml4_raw() };
while let Some(region) = self.regions.pop() { while let Some(region) = self.regions.pop() {
Self::do_unmap(pml4, &region, hhdm); Self::do_unmap(pml4, &region);
} }
pmm::free_frame(self.pml4_phys); pmm::free_frame(self.pml4_phys);
// Return the ASID to the pool so it can be reused by future processes. // Return the ASID to the pool so it can be reused by future processes.
@@ -793,7 +751,15 @@ pub fn tlb_flush_asid(asid: u16) {
let active_cpus = ACTIVE_CPUS_MASK.load(Ordering::Acquire); let active_cpus = ACTIVE_CPUS_MASK.load(Ordering::Acquire);
let current_core = crate::cpu::lapic::current_core_id(); let current_core = crate::cpu::lapic::current_core_id();
let target_mask = active_cpus & !(1u64 << current_core); // u64 can only represent cores 063. APIC IDs may be >= 64 (CPUID
// returns up to 255), so guard the shift to avoid UB (panic in debug,
// wrap in release). When SMP with >64 cores is implemented, switch to
// a wider mask or a dynamic list of active APIC IDs.
let target_mask = if current_core < 64 {
active_cpus & !(1u64 << current_core)
} else {
active_cpus
};
if target_mask == 0 { if target_mask == 0 {
return; return;
@@ -830,7 +796,12 @@ pub fn handle_tlb_shootdown_ipi() {
local_tlb_flush_asid(asid); local_tlb_flush_asid(asid);
let current_core = crate::cpu::lapic::current_core_id(); let current_core = crate::cpu::lapic::current_core_id();
SHOOTDOWN_ACK.fetch_or(1u64 << current_core, Ordering::AcqRel); // Only cores < 64 can ACK in a u64 mask. Cores >= 64 are not
// representable; their ACK would wrap and corrupt the mask.
// TODO: widen to u128 or use per-core ACK slots for >64 core SMP.
if current_core < 64 {
SHOOTDOWN_ACK.fetch_or(1u64 << current_core, Ordering::AcqRel);
}
} }
// Global kernel address space // Global kernel address space
@@ -840,6 +811,6 @@ pub static KERNEL_SPACE: Locked<Option<AddressSpace>> = Locked::new(None);
/// ///
/// ASID 0 = PCID 0 = kernel (no per-process PCID tagging). /// ASID 0 = PCID 0 = kernel (no per-process PCID tagging).
/// Must be called after `BitmapPMM::init` and `init_cpu_features`. /// Must be called after `BitmapPMM::init` and `init_cpu_features`.
pub fn init_kernel_space(pml4_phys: PhysAddr, hhdm: u64) { pub fn init_kernel_space(pml4_phys: PhysAddr) {
*KERNEL_SPACE.lock() = Some(AddressSpace::from_active(pml4_phys, 0, hhdm)); *KERNEL_SPACE.lock() = Some(AddressSpace::from_active(pml4_phys, 0));
} }