feat: docs & ARCH 2.2, 2.3, 2.4

This commit is contained in:
Faynot
2026-07-07 16:40:41 +03:00
parent c092a81331
commit 1bfde637d0
50 changed files with 5113 additions and 1823 deletions

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