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