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
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
|
# SLUB 分配器 (SLUB Allocator)
## 1. 目标 (Objective)
在物理内存管理器 (PMM) 之上,为内核提供高效的字节级内存分配器。采用 **SLUB** 设计(Simple List of Unfull Blocks),通过复用 PMM 的 `Frame` 描述符存储 per-page 元数据、将空闲链表嵌入对象体内、以及 Per-CPU 活跃页机制,实现低碎片、低锁竞争的小对象分配。本模块是内核 `operator new` 的直接后端。
**为何选择 SLUB 而非经典 Slab:**
- `Frame` 结构已包含 `cache` 指针和 `obj_count` 字段,无需额外的 per-page `struct Slab` 头部。
- 省去 full/partial/empty 三链表管理,仅需一个 partial 链表。
- Per-CPU 缓存仅需一个"当前活跃页"指针,无需动态分配对象数组(避免 slab_init 的鸡生蛋问题)。
- Linux 自 2.6.23 起默认 SLUB,6.8 已完全移除经典 Slab。
## 2. 前置依赖 (Dependencies)
- **全局初始化契约 (`global_init.md`):** `slab_init()` 在 `pmm_init()` 之后、`vmm_init()` 之前被 BSP 调用。
- **物理内存契约 (`pmm.md`):** 依赖 `pmm_alloc_frame()` / `pmm_free_frame()` 以及 `Frame` 描述符中的 `slab.cache` 和 `slab.obj_count` 字段。还依赖 `pa_to_frame()` / `frame_to_pa()` 进行地址转换。
- **并发同步契约 (`spinlock.md`):** 依赖 `Spinlock` 保护全局 partial 链表。
- **架构抽象契约 (`arch_abstraction.md`):** 依赖 `arch::cpu::id()` 访问 Per-CPU 缓存;依赖 `arch::cpu::intr_off()/intr_on()` 保护 Per-CPU 操作免受中断重入。
- **项目约定 (`00_conventions.md`):** 遵循所有命名和类型规范。
## 3. 核心数据结构 (Data Structures)
### 3.1 空闲对象链表(嵌入式)
空闲对象的前 `sizeof(void*)` 字节存储下一个空闲对象的指针。对象被分配后,该区域归用户使用。因此**最小对象尺寸 = `sizeof(void*)` = 8 字节**。
```
[ Free Object ] [ Free Object ] [ Free Object ]
+-------------+ +-------------+ +-------------+
| next ---------.------->| next ---------.------->| next = NULL |
| (unused) | | (unused) | | (unused) |
+-------------+ +-------------+ +-------------+
```
### 3.2 Frame 描述符中的 Slab 元数据
复用 `include/lume/frame.h` 中已有的字段(无需新增结构体):
```cpp
// 当 Frame.state == FrameState::Slab 时有效
struct {
KmemCache* cache; // 此页属于哪个 KmemCache
uint32 obj_count; // 此页上已分配的对象数量
} slab;
```
- 此外,需要在 `Frame` 的 `free` union 成员中增加一个 `void* freelist` 字段,或者直接将 freelist 头指针嵌入 union。为保持 Frame 结构简洁,**freelist 头指针存储在页面固定偏移处(页起始的前 8 字节)**,不侵入 Frame 结构。
**修正方案:** 在 `Frame` 的 `slab` union 成员中增加 `void* freelist` 字段:
```cpp
struct {
KmemCache* cache; // 所属缓存
uint32 obj_count; // 已分配对象数
void* freelist; // 页内空闲对象链表头
} slab;
```
### 3.3 Per-CPU 活跃页
每个 CPU 对每个 KmemCache 维护一个"当前活跃页"(cpu_slab)。分配时优先从此页的 freelist 弹出对象,无需获取全局锁。
```cpp
// 定义于 mm/slab.h (模块私有)
struct PerCpuSlab {
Frame* active; // 当前 CPU 正在使用的 slab 页(可能为 nullptr)
};
```
### 3.4 内核内存缓存 (`class KmemCache`)
```cpp
// 定义于 mm/slab.h
class KmemCache {
public:
KmemCache() = default;
void init(const char* name, uint32 obj_size, uint32 obj_align);
void* alloc();
void free(void* obj);
private:
Frame* get_partial(); // 从 partial 链表取一个页
void put_partial(Frame* f); // 将页放入 partial 链表
Frame* new_slab(); // 从 PMM 申请新页并格式化
Spinlock lock_; // 保护 partial 链表
list_node partial_; // 部分填充的 slab 页链表
PerCpuSlab cpu_slab_[kMaxCPUs]; // Per-CPU 活跃页
const char* name_ = "uninit";
uint32 obj_size_ = 0; // 对齐后的对象尺寸
uint32 obj_align_ = 0; // 对齐要求
uint32 objs_per_slab_ = 0; // 每页可容纳的对象数
};
```
### 3.5 通用尺寸缓存
```cpp
// 预创建的 2^N 尺寸缓存: 8, 16, 32, 64, 128, 256, 512, 1024, 2048
inline constexpr int kNumSizeClasses = 9;
inline constexpr uint32 kSizeClasses[kNumSizeClasses] = {
8, 16, 32, 64, 128, 256, 512, 1024, 2048
};
extern KmemCache g_size_caches[kNumSizeClasses];
```
## 4. 公共 API (Public APIs)
```cpp
// === 初始化 ===
void slab_init(); // 初始化所有通用尺寸缓存
// === 对象缓存 API ===
void kmem_cache_init(KmemCache* cache, const char* name,
uint32 size, uint32 align);
void* kmem_cache_alloc(KmemCache* cache);
void kmem_cache_free(KmemCache* cache, void* obj);
// === 通用分配 API ===
void* kmalloc(uint32 size); // 自动选择尺寸缓存
void kfree(void* ptr); // 通过 Frame 反查 cache,自动归还
// === C++ Runtime ===
void* operator new(size_t size);
void* operator new[](size_t size);
void operator delete(void* ptr) noexcept;
void operator delete[](void* ptr) noexcept;
void operator delete(void* ptr, size_t) noexcept;
void operator delete[](void* ptr, size_t) noexcept;
```
## 5. 并发与锁契约 (Concurrency & Locks)
- **持有锁:** 每个 `KmemCache` 持有一把 `Spinlock lock_`。
- **锁的作用域:** `lock_` **仅保护** `partial_` 链表。
- **Per-CPU 保护:** 访问 `cpu_slab_[cpu_id]` 时必须关闭中断(同 PMM 的 PCP 策略),防止中断重入。
- **快速路径(关中断,无锁):** 从当前 CPU 的 `active` 页的 `freelist` 弹出/压入对象。
- **慢速路径(需要锁):**
- alloc:当 `active` 页无空闲对象时,获取 `lock_` 从 `partial_` 取页(或调用 PMM 新建)。
- free:当归还对象使一个页变为完全空闲时,获取 `lock_` 归还 PMM。
- **与 PMM 交互:** `new_slab()` 调用 `pmm_alloc_frame()` 时**不持有** `lock_`(先释放锁,申请页,再获取锁挂入链表),避免 Slab lock → PMM lock 嵌套。
## 6. 错误路径 (Error Paths)
- **初始化失败:** `slab_init()` 若 PMM 无法提供页面,`kernel_panic()`。
- **分配失败:** `kmalloc()` / `kmem_cache_alloc()` 在 OOM 时返回 `nullptr`。
- **释放错误:** `kfree(nullptr)` 为 no-op。传入非法指针(不属于任何 slab 页)时 `kernel_panic()`。
## 7. 状态机 / 时序图 (State Machine / Sequence)
### 7.1 `kmem_cache_alloc` 时序
```
[kmem_cache_alloc(cache) on CPU X]
│
├─> 1. 关中断
├─> 2. active = cache->cpu_slab_[X].active
│
├─> 3. if (active && active->slab.freelist != NULL) ?
│ ├── [是 (Fast Path)]
│ │ ├─ obj = active->slab.freelist
│ │ ├─ active->slab.freelist = *(void**)obj
│ │ ├─ active->slab.obj_count++
│ │ ├─ 恢复中断
│ │ └─ return obj
│ │
│ └── [否 (Slow Path)]
│ ├─ 4.1 将 active (若满) 从 cpu_slab 移出
│ ├─ (满页不需要跟踪,但其 Frame 中 cache 指针仍有效)
│ ├─ 4.2 恢复中断
│ ├─ 4.3 LockGuard guard(cache->lock_)
│ ├─ 4.4 new_active = get_partial() // 从 partial 链表取
│ │ └─ 若无,释放锁 → new_slab() → 重新获取锁
│ ├─ 4.5 释放锁
│ ├─ 4.6 关中断
│ ├─ 4.7 cache->cpu_slab_[X].active = new_active
│ ├─ 4.8 从 new_active->slab.freelist 弹出 obj
│ ├─ 4.9 恢复中断
│ └─ return obj
│
▼
[返回 obj* 或 nullptr]
```
### 7.2 `kfree` 时序
```
[kfree(ptr)]
│
├─> 1. pa = ptr 的物理地址 (对于 identity/higher-half,减去偏移)
├─> 2. frame = pa_to_frame(pa & ~(kPageSize-1))
├─> 3. cache = frame->slab.cache
│
├─> 4. 关中断
├─> 5. 将 obj 压入 frame->slab.freelist
├─> 6. frame->slab.obj_count--
│
├─> 7. if (obj_count == 0) ?
│ ├── [是:页完全空闲]
│ │ ├─ 若此页是当前 CPU 的 active,清除 active
│ │ ├─ 恢复中断
│ │ ├─ LockGuard: 从 partial_ 链表移除 (如果在链表中)
│ │ └─ pmm_free_frame(frame) // 归还 PMM
│ │
│ └── [否]
│ ├─ 若此页不是任何 CPU 的 active 且不在 partial,
│ │ 则需获取锁放入 partial (满→部分满的过渡)
│ └─ 恢复中断
│
▼
[完成]
```
## 8. 已知缺陷与未来挑战
### 8.1 缓存收缩
当前不主动收缩 partial 链表。未来可增加 `kmem_cache_shrink()` 供内存回收线程调用。
### 8.2 大对象分配
大于 2048 字节的分配直接退化为 `pmm_alloc_frames()`。当前由 `kmalloc` 透明处理。
|