feat: docs & ARCH 2.2, 2.3, 2.4
This commit is contained in:
103
kernel/docs/memory/address-translation.md
Normal file
103
kernel/docs/memory/address-translation.md
Normal 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.
|
||||
211
kernel/docs/memory/allocator.md
Normal file
211
kernel/docs/memory/allocator.md
Normal 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
267
kernel/docs/memory/buddy.md
Normal 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.
|
||||
82
kernel/docs/memory/introduction.md
Normal file
82
kernel/docs/memory/introduction.md
Normal 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
|
||||
```
|
||||
196
kernel/docs/memory/paging.md
Normal file
196
kernel/docs/memory/paging.md
Normal 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.
|
||||
132
kernel/docs/memory/physical-memory.md
Normal file
132
kernel/docs/memory/physical-memory.md
Normal 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 на этапе инициализации и больше к нему не
|
||||
обращаются.
|
||||
258
kernel/docs/memory/pm-actor.md
Normal file
258
kernel/docs/memory/pm-actor.md
Normal 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 глубок.
|
||||
189
kernel/docs/memory/pm-router.md
Normal file
189
kernel/docs/memory/pm-router.md
Normal 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
267
kernel/docs/memory/vmm.md
Normal 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 1–4094: пользовательские.
|
||||
|
||||
`alloc()` — Two-pass: от hint до 4094, затем от 1 до hint.
|
||||
Пропускает полностью занятые 32-битные слова.
|
||||
|
||||
## Основные операции AddressSpace
|
||||
|
||||
### `new()` — создание пустого пространства
|
||||
|
||||
1. Аллоцирует фрейм из PMM для PML4.
|
||||
2. Обнуляет его (все entry = not present).
|
||||
3. Выделяет ASID из ASID_ALLOC.
|
||||
4. Пустой список regions.
|
||||
|
||||
### `from_active(pml4_phys, asid)` — обёртка существующего PML4
|
||||
|
||||
Используется для kernel space (ASID 0).
|
||||
|
||||
### `map_region(virt, phys, size, flags, cap_token)` — отображение
|
||||
|
||||
```rust
|
||||
pub fn map_region(&mut self, virt: VirtAddr, phys: Option<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
|
||||
}
|
||||
```
|
||||
Reference in New Issue
Block a user