summaryrefslogtreecommitdiff
path: root/docs/specs/pmm.md
blob: 5703e7abda046afe18c0ff637245de8921eb9f3e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
# 物理内存管理器 (Physical Memory Manager)

## 1. 目标 (Objective)

为 LumeOS 提供对物理内存页帧(Frame)的分配与回收服务。本模块基于伙伴系统(Buddy System)算法管理所有可用物理内存,并引入 Per-CPU 缓存(PCP)机制以降低多核场景下的锁竞争。PMM 是 Slab 分配器和 VMM 的底层基石。

## 2. 前置依赖 (Dependencies)

- **全局初始化契约 (`global_init.md`):** `pmm_init()` 在 `fdt_init()` 之后、`slab_init()` 之前被 BSP 调用。
- **设备树契约 (`fdt.md`):** 依赖 `fdt_early_get_mem_info()` 获取物理内存的基址和大小。
- **并发同步契约 (`spinlock.md`):** 依赖 `Spinlock` 实现对全局伙伴系统的互斥访问。
- **架构抽象契约 (`arch_abstraction.md`):** 依赖 `arch::cpu::id()` 获取当前 CPU 编号以访问 Per-CPU 缓存;依赖 `arch::cpu::intr_off()/intr_on()` 保护 PCP 操作免受中断重入。
- **项目约定 (`00_conventions.md`):** 遵循所有命名、错误处理和 API 设计规范。

## 3. 核心数据结构 (Data Structures)

### 3.1 物理页帧描述符 (`struct Frame`)

为了极致地压缩元数据内存占用,`struct Frame` 采用 `union` 设计,并严格保证 `refcount` 独立于 `union` 之外,以支持 COW。

```cpp
// 定义于 include/lume/frame.h
#include <lume/types.h>
#include <lume/list.h>

class KmemCache; // 前向声明

enum class FrameState : uint8 {
    Free,       // 空闲,位于 Buddy 或 PCP 链表中
    Allocated,  // 已分配给上层(Slab, VMM)
    Slab,       // 作为 Slab 页使用
};

struct Frame {
    uint32 refcount;    // 原子引用计数(独立于 union)
    FrameState state;   // 页帧当前状态
    uint8 order;        // 伙伴系统中的阶数 (0-10 for 4KB-4MB)

    union {
        // 当 state == FrameState::Free 时
        struct {
            list_node free_link; // 用于挂载到 Buddy 或 PCP 的空闲链表
        } free;

        // 当 state == FrameState::Slab 时
        struct {
            KmemCache* cache;   // 指向所属的 Slab 缓存
            uint32 obj_count;   // 已分配对象数量
        } slab;
    };
};
```

### 3.2 Per-CPU 单页缓存 (`struct PerCpuCache`)

每个 CPU 私有的单页帧空闲链表。**访问 PCP 时必须关闭中断**,防止定时器中断处理函数在同一 CPU 上重入 PCP 操作导致链表损坏。

```cpp
// 定义于 mm/pmm.cc (模块私有)
inline constexpr int kPcpHighWatermark = 64;  // PCP 缓存上限
inline constexpr int kPcpBatchSize = 32;      // 每次 refill/drain 的批量大小

struct PerCpuCache {
    list_node free_list; // 单页帧空闲链表头
    uint32 count;        // 当前缓存的页帧数量
};
```

### 3.3 伙伴系统常量

```cpp
// 定义于 include/lume/pmm.h
inline constexpr int kMaxOrder = 11; // 阶数 0..10 → 4KB..4MB
```

## 4. 公共 API (Public APIs)

所有分配函数在失败时返回 `nullptr`。

```cpp
// === 初始化 ===
void pmm_init();  // BSP 调用,从 FDT 发现内存并初始化伙伴系统

// === 单页帧操作 (Fast Path via PCP) ===
Frame* pmm_alloc_frame();       // 分配一个 4KB 页帧
void pmm_free_frame(Frame* f);  // 释放一个 4KB 页帧

// === 多页帧操作 (Slow Path, 直接伙伴系统) ===
Frame* pmm_alloc_frames(uint8 order);         // 分配 2^order 个连续页帧
void pmm_free_frames(Frame* f, uint8 order);  // 释放 2^order 个连续页帧

// === 引用计数操作 (原子, 无锁) ===
void frame_incref(Frame* f);  // 原子递增引用计数
void frame_decref(Frame* f);  // 原子递减,降至 0 时自动释放

// === 地址转换 ===
uint64 frame_to_pa(const Frame* f);   // Frame* → 物理地址
Frame* pa_to_frame(uint64 pa);        // 物理地址 → Frame*
uint64 frame_to_va(const Frame* f);   // Frame* → 内核虚拟地址
```

## 5. 并发与锁契约 (Concurrency & Locks)

- **持有锁:** 模块内部持有一把 `Spinlock` (`g_pmm_lock`)。

- **锁的作用域:** `g_pmm_lock` **仅保护**全局伙伴系统 (`g_free_areas[]`)。

- **PCP 保护机制:** PCP 操作**必须在中断关闭状态下**执行。这不是"无锁"——而是通过关中断防止同一 CPU 上的重入(定时器中断 → IRQ handler → pmm_alloc_frame → 损坏 PCP 链表)。关中断由 `pmm_alloc_frame()`/`pmm_free_frame()` 内部自动管理。

- **加锁慢速路径:**
    - 当 PCP 为空时,`refill_pcp()` 获取 `g_pmm_lock` 从伙伴系统批量取页。
    - 当 PCP 达到 `kPcpHighWatermark` 时,`drain_pcp()` 获取 `g_pmm_lock` 归还半数页帧。
    - `pmm_alloc_frames()` 和 `pmm_free_frames()` 因直接操作伙伴系统,**始终需要**获取 `g_pmm_lock`。

- **原子操作:** `frame_incref()` / `frame_decref()` 使用 `__atomic_fetch_add/sub` + `__ATOMIC_ACQ_REL`。

## 6. 错误路径 (Error Paths)

- **初始化失败:** `pmm_init()` 若无法从 FDT 找到可用内存,或无法为 `frame_map` 分配空间,则 `kernel_panic()`。

- **分配失败:** `pmm_alloc_*()` 在物理内存耗尽时返回 `nullptr`。上层负责处理。

- **释放错误:** `pmm_free_*()` 若接收到 `nullptr`,立即 `kernel_panic()`。

## 7. 状态机 / 时序图 (State Machine / Sequence)

### 7.1 单页分配时序 (Fast & Slow Path)

```
[pmm_alloc_frame() on CPU X]
   │
   ├─> 1. 关闭中断 (防止同 CPU 重入)
   │
   ├─> 2. 获取 pcp = &g_pcp[cpu_id]
   │
   ├─> 3. if (pcp->count > 0) ?
   │       ├── [是 (Fast Path)]
   │       │     ├─ 3.1 从 pcp->free_list 弹出一个 Frame
   │       │     ├─ 3.2 设置 state=Allocated, refcount=1
   │       │     └─ 3.3 恢复中断, return Frame
   │       │
   │       └── [否 (Slow Path)]
   │             ├─ 4.1 LockGuard guard(g_pmm_lock);
   │             ├─ 4.2 refill_pcp(pcp);  // 批量从 Buddy 取 kPcpBatchSize 页
   │             ├─ 4.3 释放锁
   │             └─ 4.4 回到步骤 3 重试
   │
   ▼
[恢复中断, 返回 Frame* 或 nullptr]
```

### 7.2 初始化时序

```
[pmm_init() — BSP 单核期]
   │
   ├─> 1. fdt_early_get_mem_info(&base, &size)
   │      └─ 获取物理内存基址和大小
   │
   ├─> 2. 计算 frame_map 位置 (紧跟 _kernel_end 之后, 页对齐)
   │      └─ frame_map 大小 = num_frames * sizeof(Frame)
   │
   ├─> 3. 清零 frame_map
   │
   ├─> 4. 初始化 Spinlock 和 Buddy 空闲链表头
   │
   ├─> 5. 初始化所有 PCP 链表头
   │
   └─> 6. 将所有空闲页帧 (frame_map 之后) 按最大对齐阶数插入 Buddy
```

### 7.3 写时复制 (COW) 引用计数流程

```
[父进程 fork() 创建子进程]
   │
   ├─> 1. 遍历父进程所有可写 VMA
   │
   ├─> 2. for each VMA:
   │      ├─ 2.1 复制父进程的 PTE 到子进程页表
   │      ├─ 2.2 清除父、子 PTE 的 'Write' 权限位
   │      ├─ 2.3 设置父、子 PTE 的 'COW' 软件标志位
   │      └─ 2.4 frame_incref(pa_to_frame(PTE->paddr));
   │
   ▼
[子进程尝试写入共享页面]
   │
   ├─> 1. 触发 Store Page Fault
   │
   ├─> 2. VMM 陷阱处理:
   │      ├─ 2.1 检查 PTE,确认是合法的 COW Fault
   │      ├─ 2.2 old_frame = pa_to_frame(PTE->paddr)
   │      ├─ 2.3 if (old_frame->refcount == 1) ?
   │      │       ├── [是] 直接恢复 Write 权限,无需拷贝
   │      │       └── [否] 分配新页, 拷贝, 修改 PTE, frame_decref(old)
   │
   ▼
[返回用户态,写操作成功]
```

## 8. 已知缺陷与未来挑战 (Known Weaknesses & Future Challenges)

### 8.1 内存回收

当前 API 在物理内存耗尽时仅返回 `nullptr`,缺乏主动回收机制。未来需引入内存回收器。

### 8.2 frame_map 存储

当前设计假设单一连续 `frame_map` 数组,适用于 QEMU 线性内存。对于多内存区域的真实硬件,需演进为稀疏内存模型。