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

1056 lines
53 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Глоссарий: термины операционных систем и ядра Elyz
> Эта статья — **минимальная база** для понимания всей документации ядра.
> Если вы не знаете, что такое MMU, TLB или Page Fault — начните здесь.
> Термины сгруппированы по темам: от фундаментальных к специфическим.
---
## 1. Фундаментальные концепции
### Процессор и память: базовая модель
Представьте, что процессор (CPU) — это человек, который выполняет инструкции
из блокнота (оперативная память, RAM). У него есть две проблемы:
1. **Скорость**: RAM медленная. Процессор в 100+ раз быстрее.
2. **Изоляция**: программы не должны видеть память друг друга.
Для решения этих проблем существуют MMU и TLB.
---
### MMU (Memory Management Unit)
**Что это?** MMU — это аппаратный блок внутри процессора, который
автоматически преобразует **виртуальные адреса** (которыми оперирует
программа) в **физические адреса** (реальная ячейка RAM).
**Зачем?** Без MMU каждая программа работала бы с реальными физическими
адресами. Программа А может случайно прочитать данные программы Б.
Одна программа может испортить данные другой. С MMU каждая программа
думает, что она одна в памяти, и работает со своими «виртуальными» адресами.
**Как это работает упрощённо:**
```
Программа: "запиши 42 по адресу 0x1000"
MMU: "адрес 0x1000 у этой программы → реальный адрес 0x7F00"
RAM: запись по адресу 0x7F00
```
MMU использует **таблицы страниц** (page tables) для этого преобразования.
В x86-64 это 4-уровневая структура: PML4 → PDPT → PD → PT (см. Paging).
**Где в коде:** `src/mem/paging.rs``PageTable`, которая представляет
одну таблицу (512 entry). `walk_to_p1_mut()` проходит все 4 уровня.
---
### TLB (Translation Lookaside Buffer)
**Что это?** TLB — это крошечный, но очень быстрый кэш внутри процессора,
который запоминает результаты трансляции адресов. Если MMU — это
переводчик, то TLB — его записная книжка.
**Зачем?** Каждый раз, когда программа читает или пишет память, MMU должен
пройти через 4 уровня таблиц страниц, чтобы перевести виртуальный адрес
в физический. Это **дорого** (4 обращения к RAM вместо 1). TLB запоминает
пары «виртуальный адрес → физический адрес», поэтому для часто
используемых страниц трансляция занимает 1 такт, а не 100+.
**Проблема:** Когда ядро меняет таблицы страниц (например, отображает
новую страницу), TLB содержит **устаревшие** записи. Их нужно сбросить
(flush). Для этого есть инструкции:
- `INVLPG [addr]` — сбросить одну запись для addr.
- Запись в CR3 — сбросить **весь** TLB.
- `INVPCID` — сбросить TLB для конкретного PCID (более тонко).
**Где в коде:** `src/mem/paging.rs``INVLPG` после map/unmap.
`src/mem/vmm.rs``tlb_flush_asid()`, `local_tlb_flush_asid()`,
`tlb_flush_all()`, `handle_tlb_shootdown_ipi()`.
---
### Page Fault (исключение #PF, вектор 14)
**Что это?** Это аппаратное исключение, которое происходит, когда
процессор не может преобразовать виртуальный адрес через MMU.
Причины: страница не отображена, нарушение прав, Copy-on-Write и т.д.
**Как работает:**
1. Программа пытается обратиться к адресу.
2. MMU проверяет таблицы — записи нет (или нет прав).
3. MMU сохраняет адрес сбоя в регистр **CR2**.
4. Процессор пушит error code и номер вектора (14) на стек.
5. Процессор переходит в обработчик (через IDT).
6. Обработчик решает: выделить страницу? COW? Или убить процесс?
**Зачем?** Page Fault — это **инструмент**, а не только ошибка:
- **Demand paging**: не выделяем страницу, пока она не понадобится.
Первый доступ → Page Fault → аллоцируем. Экономит память.
- **Copy-on-Write**: при fork страницы разделяются. При записи → fault →
копируем. Экономит время и память.
- **Lazy mapping**: регион зарезервирован (VMA есть), страниц нет.
Fault → alloc + map.
**Error code (что в нём):**
```
Bit 0 (P): 0 = страница не присутствует, 1 = защита (была, но нельзя)
Bit 1 (W): 0 = чтение, 1 = запись
Bit 2 (U): 0 = supervisor (ядро), 1 = user
Bit 3 (RSVD): нарушение зарезервированных битов
Bit 4 (ID): 0 = обычный fault, 1 = instruction fetch
```
**Где в коде:** `src/cpu/interrupts.rs``rust_page_fault_handler()`.
Читает CR2, проверяет error code, вызывает VMM.
---
### CR2 (Control Register 2)
**Что это?** Регистр процессора x86-64, который автоматически заполняется
**адресом, вызвавшим Page Fault**. При каждом #PF процессор сохраняет
виртуальный адрес, к которому пыталась обратиться программа.
**Зачем?** Обработчик Page Fault должен знать, какой адрес вызвал fault,
чтобы выделить правильную страницу.
**Как читается в коде:**
```rust
asm!("mov {}, cr2", out(reg) fault_addr);
```
`src/cpu/interrupts.rs:265-266`
> **Примечание:** Только чтение. Запись в CR2 возможна, но не нужна.
> CR2 — это read-only диагностический регистр.
---
### STI (Set Interrupt Flag) / CLI (Clear Interrupt Flag)
**Что это?** Инструкции процессора x86-64, которые управляют флагом
прерываний `IF` в регистре RFLAGS.
- `STI` (STart Interrupts) — **разрешает** маскируемые прерывания.
Процессор начинает реагировать на внешние прерывания (таймер, клавиатура).
- `CLI` (CLear Interrupts) — **запрещает** маскируемые прерывания.
Процессор игнорирует внешние прерывания.
**Зачем в ядре?**
- На этапе инициализации ядро работает с `CLI` (без прерываний), чтобы
никто не помешал последовательности настройки.
- Когда всё готово — `STI`, и ядро начинает обрабатывать прерывания.
**Где в коде:** `src/main.rs:150``asm!("sti")` после инициализации
IDT, VMM, PM Router. `hcf()``asm!("cli")`.
---
### HCF (Halt and Catch Fire) — остановка CPU
**Что это?** Состояние, в котором процессор перестаёт выполнять
программу, ожидая внешнего прерывания. Реализуется инструкцией `HLT`.
**Зачем?**
- Когда ядру больше нечего делать (нет процессов).
- При панике (panic): `CLI + HLT` — процессор остановлен навсегда.
- Ожидание в цикле: вместо busy-wait (сжигает 100% CPU) используем HLT.
**Где в коде:**
```rust
fn hcf() -> ! {
unsafe { asm!("cli"); } // запретить прерывания
loop {
unsafe { asm!("hlt"); } // остановить CPU
}
}
```
`src/main.rs:253-257`
---
## 2. Процессор и прерывания
### CPU Subsystem (подсистема процессора)
**Что это?** Совокупность кода ядра, который управляет процессором:
- IDT — таблица обработчиков прерываний.
- Interrupt Handlers — сами обработчики.
- LAPIC — контроллер прерываний.
- Системные регистры (CR0, CR2, CR3, CR4).
**Зачем?** Процессор — это не просто «вычислитель». Он генерирует
события (исключения, прерывания), и ядро должно на них реагировать.
Подсистема CPU — это прослойка между аппаратурой и остальным ядром.
**Где в коде:** `src/cpu/` — модули `idt.rs`, `interrupts.rs`, `lapic.rs`.
---
### IDT (Interrupt Descriptor Table)
**Что это?** Таблица в памяти, которая говорит процессору:
«Когда случится событие с номером N, перейди по адресу A_N».
Аналог — оглавление книги: «Если вам нужна глава 14, откройте стр. 100».
**Размер:** 256 entry × 16 байт = 4096 байт (ровно 1 страница).
**Каждая entry содержит:**
- Адрес обработчика (64 бита, разбит на 3 части из-за совместимости).
- Селектор сегмента кода (0x28 для ring 0 ядра).
- Тип вентиля: Interrupt Gate (0xE) или Trap Gate (0xF).
Разница: Interrupt Gate автоматически запрещает прерывания (CLI) при входе.
**Загрузка:** инструкция `LIDT` (Load IDT) с указателем на `IdtPtr`
(6 байт: 2 — размер, 4 — адрес).
**Где в коде:** `src/cpu/idt.rs``IdtEntry`, `InterruptDescriptorTable`,
`IdtPtr`. `src/cpu/interrupts.rs` — глобальная `static mut IDT`.
---
### Interrupt Handlers (обработчики прерываний)
**Что это?** Функции, которые вызываются процессором при прерываниях
и исключениях. Каждый обработчик — это ассемблерный «мостик» (stub),
который сохраняет контекст, вызывает Rust-функцию и возвращается.
**Анатомия обработчика:**
```asm
; Ассемблерный stub
vector_14_handler:
push rax ; сохранить регистры (контекст)
push rcx
push rdx
...
mov rdi, [rsp+15*8] ; error code как аргумент 1
call rust_handler ; вызов Rust функции
add rsp, 8 ; очистить error code
pop ... ; восстановить регистры
iretq ; возврат в прерванный код
```
**Почему два этапа (asm + Rust)?**
- Rust не умеет напрямую работать с сохранением/восстановлением регистров.
- Соглашение C (и Rust) требует определённого состояния стека.
- Ассемблер — единственный способ правильно войти и выйти.
**Типы обработчиков в Elyz:**
- **Early** (вектора 0-31): HALT с сообщением — при ранних сбоях до VMM.
- **Page Fault** (14): полноценный — COW, lazy, или паника.
- **Double Fault** (8): всегда паника.
- **GPF** (13): всегда паника.
- **TLB Shootdown** (0xFD): межпроцессорный TLB flush.
**Где в коде:** `src/cpu/interrupts.rs``exception_stub!()` макрос,
`early_stub` макрос, `rust_page_fault_handler`, `rust_gpf_handler`,
`rust_double_fault_handler`.
---
### LAPIC (Local Advanced Programmable Interrupt Controller)
**Что это?** Встроенный в каждое ядро процессора контроллер прерываний.
Он:
- Принимает прерывания от устройств (через системный контроллер APIC).
- Отправляет межпроцессорные прерывания (IPI).
- Имеет таймер (не используется в текущей версии).
- Обрабатывает EOI (End Of Interrupt).
**Зачем?** Без LAPIC каждое прерывание шло бы на единственный CPU.
С LAPIC можно распределять прерывания между ядрами.
**MMIO:** LAPIC программируется через Memory-Mapped I/O по физическому
адресу `0xFEE0_0000` + HHDM offset. Обращение через `write_volatile`.
**Регистры:**
- `0x0B0` — EOI: запись 0 подтверждает завершение обработки.
- `0x300` — ICR (Interrupt Command Register): для отправки IPI.
- `0x020` — IRR (In-Service Register): какие прерывания обрабатываются.
**Где в коде:** `src/cpu/lapic.rs`.
---
### IPI (Inter-Processor Interrupt)
**Что это?** Прерывание, которое одно ядро отправляет другому (или всем).
**Зачем?** Используется для **TLB shootdown**: когда одно ядро меняет
page tables, оно должно сообщить другим ядрам, чтобы они сбросили TLB.
**Как работает в Elyz:**
1. Ядро A хочет flush TLB для ASID 5.
2. Ядро A пишет в LAPIC ICR: вектор TLB_SHOOTDOWN (0xFD),
destination = All Excluding Self.
3. LAPIC отправляет IPI всем ядрам, кроме A.
4. Каждое ядро B получает прерывание → вызывает
`rust_tlb_shootdown_handler()``local_tlb_flush_asid(5)`.
5. Каждое ядро B пишет ACK и отправляет EOI.
6. Ядро A ждёт ACK от всех ядер, затем продолжает.
**Где в коде:** `src/cpu/lapic.rs``broadcast_ipi_exclude_self()`.
`src/mem/vmm.rs``tlb_flush_asid()`, `handle_tlb_shootdown_ipi()`.
---
## 3. Управление памятью: фундамент
### Физическая vs Виртуальная память
**Физическая память** — это реальные микросхемы RAM. У неё есть адреса
начиная с 0 и до максимума (например, 16 GiB).
**Виртуальная память** — это иллюзия, которую создаёт MMU для каждой
программы. Программа видит «свой» адрес с 0 до 2^48 (256 TiB), но
за каждым виртуальным адресом стоит какой-то физический.
```
Виртуальные адреса программы A:
0x0000 ─────────────────► физический 0x1000 (через MMU)
0x1000 ─────────────────► физический 0x5000
0xFFFF_8000_0000_0000 ─► физический 0x0000 (HHDM — отображение ядра)
Виртуальные адреса программы B:
0x0000 ─────────────────► физический 0x3000 (другой!)
0x1000 ─────────────────► физический 0x7000
```
**Преимущества виртуальной памяти:**
1. **Изоляция**: программа A не видит программу B.
2. **Безопасность**: ядро живёт в higher half, пользователь не может
к нему обратиться (из-за флага USER/SUPERVISOR в PTE).
3. **Эффективность**: можно отобразить только часть программы, остальное
подкачивать по требованию (demand paging).
4. **Упрощение**: каждая программа думает, что у неё вся память с 0.
---
### Paging / 4-уровневые таблицы (Page Tables)
**Что это?** Структура данных, которую использует MMU для трансляции
адресов. В x86-64 это 4 уровня:
```
Виртуальный адрес (64 бита, используются 48):
┌─────────┬─────────┬─────────┬─────────┬─────────┐
│ PML4 │ PDPT │ PD │ PT │ offset │
│ 9 бит │ 9 бит │ 9 бит │ 9 бит │ 12 бит │
└────┬────┴────┬────┴────┬────┴────┬────┴─────────┘
│ │ │ │
▼ ▼ ▼ ▼
PML4 ──► PDPT ──► PD ──► PT ──► Фрейм (4 KiB)
(P4) (P3) (P2) (P1)
```
- **Каждая таблица**: 512 entry × 8 байт = 4096 байт (1 страница).
- **9 бит на уровень**: 512 вариантов, 2^9 = 512.
- **12 бит offset**: 4096 байт внутри страницы.
- **Итого**: 48 бит = 256 TiB адресуемого пространства.
**Entry (PTE, Page Table Entry):**
```
Bit 63: NX (No Exec) — защита от исполнения
Bits 51:12: Физический адрес следующей таблицы (или фрейма)
Bits 11:0: Флаги (PRESENT, WRITABLE, USER, ACCESSED, DIRTY...)
```
**Huge Pages:**
- 2 MiB: если P2 entry имеет PS (Page Size) = 1.
- 1 GiB: если P3 entry имеет PS = 1.
- Используются для отображения больших областей (HHDM).
**Где в коде:** `src/mem/paging.rs``PageTable`, `PageTableFlags`,
`walk_to_p1_mut()`, `translate()`, `map_page()`, `unmap_page()`.
---
### PML4 (Page Map Level 4)
**Что это?** Самый верхний (корневой) уровень таблиц страниц.
Физический адрес PML4 хранится в регистре **CR3**.
**Зачем?** Каждое адресное пространство имеет свой PML4. Когда
процессор переключается между задачами, он загружает новый CR3,
и MMU начинает транслировать адреса по-новому.
**PCID (Process Context Identifier):** В младшие 12 бит CR3 можно
записать номер контекста (ASID). Это позволяет TLB хранить записи
для нескольких процессов одновременно, не сбрасывая TLB при каждом
переключении. Elyz использует PCID для kernel (ASID=0) и процессов
(ASID 1-4094).
**Где в коде:** `src/mem/vmm.rs:708-716``AddressSpace::activate()`.
---
### HHDM (Higher-Half Direct Map)
**Что это?** Область виртуальной памяти (обычно начинается с
`0xFFFF_8000_0000_0000`), где **вся физическая память отображена 1:1
с фиксированным смещением**.
**Формула:** `virt_addr = phys_addr + HHDM_OFFSET`
**Зачем?** Когда ядру нужно прочитать или записать физический фрейм
(например, обнулить страницу, прочитать Page Table), оно не может
использовать физический адрес напрямую — процессор работает только
с виртуальными адресами. HHDM даёт простой способ получить доступ
к любому физическому адресу: просто добавь HHDM_OFFSET.
**Где используется:**
- `PhysAddr::to_virt()``VirtAddr(self.0 + get_hhdm())`
- `PageTable` доступ: `PhysAddr(pte & PTE_ADDR_MASK).to_virt().as_mut_ptr()`
- `BitmapPMM`: обнуление фреймов.
- `BuddyAllocator`: intrusive list (next/prev на странице).
**Где в коде:** `src/mem/address.rs``HHDM_OFFSET`, `init_hhdm()`,
`get_hhdm()`, `PhysAddr::to_virt()`.
---
### Address Space (адресное пространство)
**Что это?** Полное описание всех виртуальных адресов, доступных
программе (или ядру). Включает:
- **PML4** (CR3) — аппаратная структура для трансляции.
- **VMA list** — список регионов (программное описание, что где отображено).
**В Elyz:**
```rust
pub struct AddressSpace {
pub asid: u16, // PCID для TLB
pub pml4_phys: PhysAddr, // адрес корневой таблицы в CR3
regions: Vec<VmaRegion>, // отсортированный список VMA
}
```
**Зачем?** Переключение между процессами = переключение между
AddressSpace → загрузка нового CR3 + TLB flush.
**Где в коде:** `src/mem/vmm.rs``AddressSpace` (методы: `new()`,
`from_active()`, `map_region()`, `unmap_region()`, `handle_fault()`,
`clone_for_fork()`, `activate()`, `revoke_by_token()`).
---
### VMA (Virtual Memory Area) и VMA-деревья
**Что это?** Регион непрерывного виртуального адресного пространства
с едиными правами доступа. Аналог — «отрезок» памяти.
**Пример:** программа просит 1 MiB памяти. Ядро создаёт VMA:
```
VmaRegion {
virt_start: 0x7F00_0000,
virt_end: 0x7F10_0000, // 1 MiB позже
flags: READ | WRITE | USER,
cap_token: 0x1234,
backing: Anonymous(Vec<Option<PhysAddr>>),
}
```
**Типы VMA (backing):**
- **Anonymous**: own-фреймы, выделенные из PMM. При unmap → free.
- **Physical**: непрерывный физический диапазон (например, framebuffer).
- **Shared**: zero-copy mapping чужих фреймов (capability-based).
**VMA-деревья:** VMA хранятся в сортированном списке (Vec), по
virt_start. Поиск: binary search (`partition_point`). В будущем
может быть заменено на дерево для O(log N) вставки/удаления.
**Где в коде:** `src/mem/vmm.rs``VmaRegion`, `VmaBacking`.
Управление: `find_idx()`, `insert_sorted()`, `map_region()`.
---
### KERNEL SPACE (пространство ядра)
**Что это?** Глобальное адресное пространство ядра. В Elyz:
```rust
pub static KERNEL_SPACE: Locked<Option<AddressSpace>> = Locked::new(None);
```
**Особенности:**
- Использует ASID = 0 (PCID 0, kernel).
- Живёт постоянно (не освобождается).
- Page Fault handler работает в контексте KERNEL_SPACE.
- Содержит:
- HHDM (вся физическая память отображена).
- Код ядра (текст).
- Кучу (heap, slab allocator).
- Page tables ядра.
**Зачем отдельно?** Чтобы ядро могло обрабатывать Page Fault'ы,
оно должно знать, какие VMA у его собственного AddressSpace.
Kernel space — это AddressSpace ядра для себя.
**Где в коде:** `src/mem/vmm.rs:809`. Инициализация `init_kernel_space()`.
---
## 4. Менеджеры физической памяти
### PMM (Physical Memory Manager)
**Что это?** Компонент ядра, который отслеживает, какие физические
фреймы (страницы по 4 KiB) свободны, а какие заняты.
**Основные операции:**
- `alloc_frame()` — «дай мне один свободный фрейм».
- `free_frame(addr)` — «этот фрейм больше не нужен».
- `alloc_contiguous(n)` — «дай n последовательных фреймов» (для DMA).
**Зачем?** Без PMM каждое выделение памяти было бы вручную. PMM
автоматизирует и централизует этот процесс.
---
### BitmapPMM (Bitmap Physical Memory Manager)
**Что это?** Реализация PMM через битовую карту (bitmap). Каждый бит
в bitmap представляет один фрейм (4 KiB):
- `1` = фрейм занят.
- `0` = фрейм свободен.
**Сколько нужно памяти для bitmap:**
Если у нас 16 GiB RAM → 16 GiB / 4 KiB = 4 млн фреймов.
4 млн бит = 512 KiB для bitmat.
**Трёхуровневое ускорение:**
```
L1 (уровень 1): массив u64
Каждый бит = одно слово из L0
1 = в этом слове есть хотя бы один свободный фрейм
L0 (уровень 0): массив u8 (основной bitmap)
Каждый байт = 8 фреймов, каждый бит = 1 фрейм
Ref-counts: массив u16
Счётчик ссылок на каждый фрейм (для COW: несколько
виртуальных отображений могут ссылаться на один фрейм)
```
**Аллокация (alloc_frame):**
1. Ищем L1-слово с ненулевым битом (есть свободные фреймы).
2. Вычисляем, какое L0-слово соответствует этому биту.
3. Если L0-слово == !0 (все заняты) — очищаем бит L1, ищем дальше.
4. `(!word).trailing_zeros()` — находим свободный бит в L0-слове.
5. Возвращаем физический адрес: `page_idx * 4096`.
**Почему не отдельный L0-слово?** L1 позволяет пропускать целые
группы по 64 слова (4096 фреймов), где нет ни одного свободного.
**Ref-counts:**
- `free_frame()` уменьшает счётчик. Если стал 0 — очищает бит.
- `inc_ref_frame()` увеличивает счётчик (для COW).
- `lock_frame()` — принудительное занятие (для метаданных PMM).
**Где в коде:** `src/mem/pmm.rs``BitmapPMM`.
---
### PM Actor (Physical Memory Actor)
**Что это?** Распределённый менеджер памяти. В отличие от глобального
BitmapPMM, у каждого PMActor есть свой **диапазон** физической памяти,
которым он управляет самостоятельно.
**Зачем?**
1. **Изоляция**: актор A не может истощить память актора B.
2. **Масштабирование**: каждый актор может работать на своём ядре.
3. **Предсказуемость**: каждый актор знает свой лимит.
4. **Модель акторов**: запросы приходят асинхронно через очередь.
**Как работает:**
1. PMActor владеет диапазоном: `managed_range = (start_phys, end_phys)`.
2. Внутри использует `BuddyAllocator` для своего диапазона.
3. Получает запросы через lock-free MPSC очередь (PMActorQueue).
4. Обрабатывает (`process_messages()`): Alloc, Free, Carve.
5. Ответы отправляет через PMRouter.
**Где в коде:** `src/mem/pm_manages.rs``PMActor`.
---
### PM Router (Physical Memory Router)
**Что это?** Центральный коммутатор для асинхронной маршрутизации
ответов от PMActor'ов к запросившим потокам.
**Как работает:**
- 65536 предварительно выделенных **каналов**.
- `alloc_channel()`: CAS на стеке свободных каналов (lock-free).
- `route_responses()`: запись результата в канал.
- `wait_for_response()`: HLT-ожидание + чтение результата.
| Операция | Что происходит |
|----------|---------------|
| Выделить канал | `alloc_channel()` → получаем u16 ID |
| Отправить запрос | `actor.submit_request(req)` с channel_id |
| Актёр обрабатывает | `process_messages()` → buddy.alloc() |
| Отправить ответ | `router.route_responses([response])` |
| Получить ответ | `router.wait_for_response(ch)` |
**Где в коде:** `src/mem/pm_router.rs``PMRouter`, `Channel`.
---
### Buddy Allocator (аллокатор пар)
**Что это?** Алгоритм управления памятью, который делит память на
блоки размером `2^order` страниц. Название от английского «buddy»
(товарищ, партнёр) — потому что блоки объединяются со своим
«напарником» (buddy) при освобождении.
**Как работает:**
```
Изначально: один блок 16 страниц (order 4)
alloc(1 страница, order 0):
16 → [8][8] → [4][4][8] → [2][2][4][8] → [1][1][2][4][8]
Выдан порядок 0, блок в начале.
free(первый блок, order 0):
[свободный][свободный][2][4][8]
→ объединяем buddy → [2][2][4][8] → [4][4][8] → [8][8] → [16]
Восстановлен полный блок order 4.
```
**Формула buddy:** `buddy_idx = block_idx XOR (1 << order)`
**Почему эффективно?**
1. **Нет фрагментации**: блоки объединяются обратно.
2. **Быстро**: alloc и free — O(log N) в худшем, O(1) в среднем.
3. **Intrusive list**: next/prev хранятся прямо на страницах.
**Intrusive List в Buddy:**
Вместо отдельного массива указателей, свободные блоки хранят
указатели next/prev прямо на своей физической странице:
```
Физическая страница 0x4000 (свободна):
┌──────────────┬──────────────┬────────────────────┐
│ next = 0x5000│ prev = NULL │ (остаток страницы) │
└──────────────┴──────────────┴────────────────────┘
```
Это требует только HHDM-доступа к странице, никаких дополнительных
аллокаций для структур данных.
**Где в коде:** `src/mem/buddy.rs``BuddyAllocator`.
---
### Intrusive Linked List
**Что это?** Связный список, где указатели next/prev хранятся **внутри**
самих элементов, а не в отдельной структуре.
**Сравнение:**
```
Обычный список:
Node { data, next, prev } — Node выделяется отдельно
list: {... Node A ... Node B ...} — узлы в куче
Интрузивный список:
Page { ... next, prev, ...} — поле next/prev прямо на странице
list: [Page A] ↔ [Page B] ↔ [Page C] — страницы сами себе узлы
```
**Преимущество:** никаких дополнительных аллокаций. Список — это
сами управляемые объекты (страницы).
**Использование в Elyz:**
- `BuddyAllocator`: free_heads[order] — intrusive list свободных блоков.
- `flist_push`, `flist_remove`, `flist_pop` управляют списком.
**Где в коде:** `src/mem/buddy.rs:45-82`.
---
### Slab Allocator (плитный аллокатор)
**Что это?** Аллокатор для **мелких объектов** (8 — 2048 байт) —
куча ядра. Работает так:
1. Есть фиксированные размеры блоков: 8, 16, 32, 64, 128, 256, 512, 1024, 2048.
2. Для каждого размера — список свободных блоков (slab list).
3. Аллокация: берём блок из списка.
4. Деаллокация: возвращаем блок в список.
**Пример:** `alloc(37 байт)`:
- `list_index(37)` → размер 64 (ближайший ≥ 37).
- Если есть блок 64 в list_heads — отдаём.
- Если нет — берём новый блок (bump alloc) размером 64.
**Для больших блоков (> 2048):** используется отдельный freelist.
Если freelist пуст — bump alloc (последовательная раздача из heap).
**Bump alloc:** просто двигаем указатель `next_bump` вперёд.
Быстро, но не переиспользует память (только slab возвращает).
**Отличие от Buddy Allocator:**
| Характеристика | Slab | Buddy |
|---------------|------|-------|
| Размер блоков | Фиксированный (8-2048) | Степени двойки (1,2,4,8… страниц) |
| Для чего | Ядерная куча (Vec, Box) | Физическая память (PMActor) |
| Где память | Виртуальная (heap 0xFFFF_9000...) | Физическая |
| Переиспользование | Да (slab list) | Да (coalesce) |
**Где в коде:** `src/mem/allocator.rs``SlabAllocator`, `ALLOCATOR` (global_allocator).
---
### Heap Init (инициализация кучи)
**Что это?** Выделение и настройка области памяти для кучи ядра.
**В Elyz:**
```rust
let heap_start = 0xFFFF_9000_0000_0000; // виртуальный адрес
let heap_size = 8 * 1024 * 1024; // 8 MiB
// Сначала map'им все страницы кучи (alloc_frame для каждой)
for i in (0..heap_size).step_by(4096) {
let frame = pmm::alloc_frame().expect("OOM");
p4.map_page(VirtAddr(heap_start + i), frame, flags);
}
// Затем инициализируем аллокатор
allocator::ALLOCATOR.lock().init(heap_start, heap_size);
```
**Почему в 2 шага?**
1. Сначала подготавливаем page tables: куча должна быть отображена
физически, иначе при первом `alloc` будет Page Fault.
2. Затем говорим SlabAllocator'у: «вот твоя область, раздавай».
---
## 5. Виртуальная память: продвинутые концепции
### VMM (Virtual Memory Manager)
**Что это?** Компонент ядра, который управляет виртуальными адресными
пространствами (AddressSpace). Его работа:
- Создание/уничтожение AddressSpace.
- Отображение (map) и удаление (unmap) регионов.
- Обработка Page Fault (COW, lazy).
- Fork с Copy-on-Write.
- TLB shootdown при SMP.
**Где в коде:** `src/mem/vmm.rs``AddressSpace`, `VmaRegion`, `VmaBacking`.
---
### Demand Paging (ленивое отображение, lazy mapping)
**Что это?** Стратегия, при которой физическая страница выделяется
только когда программа действительно к ней обращается, а не когда
она запрашивает память.
**Как работает:**
1. Программа: `malloc(1 MiB)` или ядро: `map_region(LAZY)`.
2. Ядро создаёт VMA с флагом LAZY.
3. VMA есть, но Page Table entries = NOT PRESENT.
4. При первом чтении/записи → **Page Fault**.
5. Обработчик: alloc_frame → zero → map_page → return.
6. Программа продолжает, даже не зная, что был fault.
**Зачем?**
- **Экономия памяти**: программа может зарезервировать 1 GiB,
но использовать 1 MiB. Остальное не занимает физическую память.
- **Скорость запуска**: не нужно allocating всё сразу.
**Где в коде:** `src/mem/vmm.rs``VmaFlags::LAZY`, `handle_fault()`
проверяет LAZY и выделяет страницу.
---
### Copy-on-Write (COW, копирование при записи)
**Что это?** Техника, при которой ресурс (страница) разделяется между
несколькими потребителями, пока один из них не попытается изменить
(записать) его. Тогда делается копия.
**Как работает (на примере fork):**
1. Родитель и потомок имеют одинаковые страницы.
2. Вместо копирования всех страниц (дорого!), ядро отображает
**те же** фреймы в AddressSpace потомка.
3. Все PTE помечаются флагом **COW** и WRITABLE снимается.
4. Когда кто-то пишет → Page Fault.
5. Обработчик: alloc_frame → copy(old → new) → map(new) → free(old).
6. Каждый теперь имеет свою копию.
**Зачем?**
- **fork() становится O(1)**: не нужно копировать всё адресное
пространство. Только page tables.
- **Экономия**: если потомок только читает (или exec'ится сразу),
копирования не происходит вообще.
**COW-bit:** Elyz использует бит 9 PTE как кастомный флаг COW
(аппаратура его игнорирует).
**Где в коде:** `src/mem/vmm.rs`:
- `clone_for_fork()`: устанавливает COW на родительские страницы.
- `handle_fault()`: при write + COW → copy.
- `src/mem/paging.rs``PageTableFlags::COW`.
---
### INVPCID (Invalidate PCID)
**Что это?** Инструкция x86-64 для выборочного сброса TLB по PCID
(Process Context Identifier). Более тонкая, чем полная перезапись CR3.
**Типы INVPCID:**
- **Тип 0**: сброс одной страницы в конкретном контексте.
- **Тип 1**: сброс всех записей для одного контекста (используется в Elyz).
- **Тип 2**: сброс всех записей для всех контекстов.
- **Тип 3**: сброс всех записей для всех контекстов, кроме текущего.
**Зачем?** На старых CPU (до Broadwell) INVPCID может отсутствовать.
Elyz проверяет CPUID.07H:EBX[10] и падает на CR3 reload если нет.
**Где в коде:** `src/mem/vmm.rs``INVPCID_SUPPORTED`, `init_cpu_features()`,
`local_tlb_flush_asid()`.
---
### Lazy Region (ленивый регион)
**Что это?** VMA с флагом LAZY. Страницы не выделены заранее,
аллоцируются по требованию при Page Fault.
**Отличие от обычного региона:**
- Обычный: alloc сразу выделяет все фреймы (eager).
- Ленивый: alloc создаёт Vec<None>. Фреймы при fault.
**Где используется:** большие выделения памяти, стеки потоков,
mapped файлы.
---
## 6. Система Capability
### Capability (возможность, дескриптор доступа)
**Что это?** Неподделываемый токен, дающий право выполнить операцию
над объектом. Аналог: ключ от номера в гостинице.
**В традиционных ОС (Linux):**
- У вас есть UID (0 = root, 1000 = user).
- Система проверяет: «есть ли у user 1000 права на этот файл?»
- Это называется **Access Control List (ACL)**.
**В capability-системе:**
- У вас есть capability (структура в памяти ядра).
- Capability говорит: «я даю право ЧИТАТЬ эту страницу».
- Если у вас нет capability — у вас нет доступа. Точка.
**Преимущества:**
- **Принцип минимальных привилегий**: вы можете передать только READ,
без WRITE. С ACL это сложнее.
- **Иерархия**: из capability можно сделать «дочерний» с урезанными правами.
- **Отзыв**: capability можно отозвать (revoke), включая всех «детей».
**Capability в Elyz:**
```rust
pub struct Capability {
pub object: CapObject, // Memory / CNode / PMActor
pub rights: CapRights, // READ / WRITE / EXECUTE / GRANT
pub relation: Relation, // Strong / Borrow / Transfer
pub token_sig: u64, // уникальный ID
}
```
**Где в коде:** `src/cap/descriptor.rs`.
---
### Mint (создание capability-потомка)
**Что это?** Операция создания дочернего capability с урезанными правами.
**Аналогия:** у вас есть ключ-карта от номера (можете открыть дверь,
минибар, сейф). Вы делаете копию, которая открывает только дверь.
**Правила:**
- `final_rights = parent.rights & requested_rights`
- Нельзя расширить права (только сузить).
- Нужен флаг GRANT у родителя.
**Пример:**
```rust
// Есть capability с R|W|X|G на страницу
// Создаём потомка только с R|W
cnode.mint(src=0, dest=10, Relation::Borrow, rights=R|W)?;
```
**Где в коде:** `src/cap/mod.rs:40-80`.
---
### CNode (Capability Node)
**Что это?** Таблица (массив) слотов, каждый слот хранит один capability.
Аналог: таблица открытых файлов в ядре Linux (fd table).
```rust
pub struct CNode {
pub slots: Vec<Locked<CNodeSlot>>,
}
pub struct CNodeSlot {
pub cap: Capability,
pub parent_idx: Option<usize>,
}
```
**Операции:**
- `insert(slot, cap)` — поместить в слот.
- `mint(src, dest, relation, rights)` — создать потомка.
- `revoke(slot)` — отозвать (каскадно).
- `get_cap(slot)` — прочитать.
**Отзыв (revoke):**
1. Найти всех потомков (parent_idx == slot).
2. Рекурсивно revoke каждого потомка.
3. Уничтожить сам capability.
4. Отправить token_sig в MMU_REVOCATION_QUEUE.
**Lock Ranking:** при mint захватываются две блокировки. Чтобы избежать
дедлока — захватываем всегда в порядке возрастания индекса.
**Где в коде:** `src/cap/mod.rs`.
---
## 7. Конкурентность и синхронизация
### Lock-free (без блокировок)
**Что это?** Техника синхронизации, где несколько потоков могут
читать/писать общие данные без мьютексов. Используются атомарные
операции: CAS (Compare-And-Swap), fetch_add, store/load.
**CAS (Compare-And-Swap) в Rust:**
```rust
// Атомарно: если *addr == old, то *addr = new, вернуть Ok
// Иначе: вернуть Err(actual_value)
match atom.compare_exchange_weak(old, new, Ordering::AcqRel, Ordering::Relaxed) {
Ok(_) => {}, // успешно обновили
Err(actual) => {}, // кто-то другой изменил
}
```
**Где используется в Elyz:**
- `PMActorQueue::send()` — несколько продюсеров конкурируют за слот.
- `PM Router::alloc_channel()` — CAS на free_head.
- `RevocationQueue::push()` — несколько CNode могут одновременно пушить.
- `Locked<T>::lock()` — CAS на AtomicBool.
**Преимущества:**
- Нет блокировок → нет deadlock'ов.
- Нет переключения контекста при ожидании.
- Работает в обработчиках прерываний.
---
### MPSC (Multiple Producer, Single Consumer)
**Что это?** Паттерн очереди, где **много** потоков могут писать
(producer), но только **один** поток читает (consumer).
**Где в Elyz:**
- `PMActorQueue`: produce = любой поток/ядро, consume = только PMActor.
- `RevocationQueue`: produce = любой CNode revoke, consume = VMM (page fault).
**Реализация:**
- Producer: CAS на tail (конкуренция).
- Consumer: просто читает head (без CAS, Single Consumer гарантия).
**Почему Single Consumer?**
- Проще: не нужна блокировка на чтение.
- Быстрее: pop() без атомики.
- Соответствует модели: один актор потребляет свои сообщения.
---
## 8. Вопрос архитектуры
### Нужно ли переносить LAPIC и serial в userspace для микроядра?
**Краткий ответ:**
- **Serial (COM1)**: да, должен быть в userspace. Драйвер UART
не требует привилегированного доступа, только порты I/O.
В микроядре он был бы userspace-процессом, получающим символы
через IPC и отправляющим их в порт.
- **LAPIC**: частично — да, частично — нет.
**Подробнее про LAPIC:**
| Функция LAPIC | Микроядро | Почему |
|---------------|-----------|--------|
| EOI (End Of Interrupt) | **Ядро** | EOI — это acknowledging прерывания. Если отдать userspace, процесс может «забыть» отправить EOI, и прерывания зависнут. |
| ICR (IPI отправка) | **Ядро** | IPI может сбить TLB других ядер. Только ядро должно контролировать, когда и кому отправлять IPI. |
| APIC ID | Userspace | Чтение ID — безопасно, полезно для affinity. |
| Timer | Userspace | Таймер LAPIC может быть отдан userspace-процессу (например, драйверу таймера). |
| LVT (остальное) | **Ядро** | LVT настраивает векторы прерываний. Нельзя отдавать userspace. |
**Вывод: LAPIC не может быть полностью в userspace**, но может быть
split-драйвером: часть в ядре (безопасность), часть в userspace.
**Текущее состояние Elyz (гибридное/монолитное):**
На данном этапе Elyz не является микроядром. И LAPIC, и serial
находятся в ядре. Это нормально для early стадии. При переходе
к микроядру:
1. Serial → userspace процесс (драйвер UART).
2. LAPIC → split: EOI/ICR в ядре, timer в userspace.
**Где в коде:** `src/cpu/lapic.rs`, `src/debug/serial.rs`.
---
## 9. Краткий справочник: для чего всё?
| Термин | Для чего |
|--------|----------|
| **MMU** | Преобразует виртуальные адреса в физические. Даёт изоляцию процессов. |
| **TLB** | Кэш трансляции адресов. Ускоряет работу MMU в ~100x. |
| **Page Fault** | Ловит момент, когда страницы нет в памяти. Используется для demand paging и COW. |
| **IDT** | Таблица, которая говорит CPU, какой код вызвать при исключении. |
| **LAPIC** | Контроллер прерываний. Нужен для IPI (TLB shootdown). |
| **HHDM** | Отображение всей физической памяти в higher half. Упрощает доступ к фреймам. |
| **PMM** | Кто даёт и забирает физические фреймы. |
| **BitmapPMM** | Реализация PMM через битовую карту. Быстрый поиск свободных фреймов. |
| **BuddyAllocator** | Аллокатор физической памяти. Объединяет блоки при освобождении — нет фрагментации. |
| **SlabAllocator** | Аллокатор кучи ядра для мелких объектов. Vec, Box, String — через него. |
| **PMActor** | Распределённый менеджер физической памяти. Изоляция — актор A не ест память B. |
| **PM Router** | Асинхронная доставка результатов от акторов к потокам. |
| **AddressSpace** | Виртуальное адресное пространство процесса. PML4 + VMA. |
| **VMA** | Регион памяти с едиными правами. |
| **COW** | Копирование страницы только при записи. Fork без копирования всего. |
| **Demand paging** | Выделение страницы по требованию, а не заранее. Экономит память. |
| **VMM** | Управляет AddressSpace. Создаёт, удаляет, обрабатывает fault. |
| **INVPCID** | Выборочный сброс TLB для одного контекста. Ускоряет context switch. |
| **TLB shootdown** | Рассылка TLB flush на все ядра при изменении page tables. |
| **Capability** | Неподделываемый токен доступа. «Если нет ключа — нет доступа». |
| **Mint** | Создание дочернего capability с урезанными правами. |
| **Revoke** | Отзыв capability и всех его потомков. |
| **CNode** | Таблица capability. Хранит их в слотах. |
| **Lock-free** | Синхронизация без мьютексов, через CAS. Нет дедлоков. |
| **MPSC** | Очередь: много пишут, один читает. Паттерн для акторов. |
| **Intrusive list** | Список, где next/prev в самих элементах. Не нужно доп. аллокаций. |
| **KERNEL_SPACE** | AddressSpace ядра. Page Fault handler работает в нём. |
| **HCF** | Останов CPU (CLI + HLT). Конечная точка после паники. |
| **STI/CLI** | Включить/выключить прерывания. |
| **CR2** | Регистр с адресом, вызвавшим Page Fault. |