内存分配器-字节分配器:Chaos 一点都不 Chaos
分析内存结构
堆中的 Vec<Vec<u8>>
结构数据
使用上述任意分配器,并在 alloc 和 dealloc 函数,中分别打印 Layout。在打印结果中可以看到一些比较特殊的内存大小分别为 96、192、384
这是什么呢?根据 lab1/src/main.rs
中的测例代码分析可得知 let mut items: Vec<Vec<u8>> = Vec::new();
的内层 Vec
将存放在堆内存中。
而 Rust 中的堆数据结构 中的一张图片展示了 Vec 的内存结构为指针、长度和容量,每一部分各占 usize,在 64 位系统中,uszie 大小为 8 字节,即一个空数组内存占用为 24 字节,这与 96 字节仍有出入。
再观察分配情况,在 Indicator: 0
时,96 字节的 alloc 是在第一次 32 字节 alloc 后产生的,猜测是在 items.push
后进行了一次 扩容,alloc_pass
修改为如下代码进行验证:
1 | fn alloc_pass(delta: usize) -> Vec<Vec<u8>> { |
结果为:
1 | Running bumb tests... |
就对上了,24 * 4 = 96
24 * 8 = 192
24 * 16 = 384
同时,至少在这里来说,Vec
初始容量为 0,第一次扩容时为 4,扩容机制为每次容量翻倍。
存放数据的堆空间
再回到测例代码:
1 | fn free_pass(items: &mut Vec<Vec<u8>>, delta: u8) { |
这段代码在检查是否为偶数项,偶数项数组才会被 free_pass
的 remove()
释放。
因此针对这段代码,可以在分配器中设置一个奇偶标志位,当标志位为奇数时将数据从内存的起始位置开始分配,偶数时分配在末尾位置且向起始位置增长。扩容时,不考虑内存扩容时传入的内存地址起始位置而只改变结尾位置时也无需多余操作。
Chaos 真的不 Chaos
整体结构
考虑:
- 堆中的
Vec<Vec<u8>>
数据结构,大小固定为96 + 192 + 384 = 672
字节,位于可分配内存的起始位置 - 奇数项堆,动态增长,紧跟 672 字节
- 偶数项堆,动态增长,位于可分配内存的末尾位置
使用以下结构体来描述整个可分配内存区域
1 | pub struct Chaos { |
内存结构如下:
vec_reserve: (usize, usize, usize)
用作存储先前描述的三片特殊的堆中 Vec 元数据结构,vec_reserve
为固定长度:96 + 192 + 384
字节。
pub start: usize
和 pub end: usize
分别存储 init 或是 add_to_heap 时给定的可分配内存地址范围。
pub head: usize
和 pub tail: usize
分别存储当前奇数堆的末尾位置和偶数堆的起始位置,通过 head - start + vec_reserve
可以得出奇数堆大小,通过 end - tail
则可以得出偶数堆大小。
pub is_even: bool
用于记录上一次分配是奇数还是偶数,在分配的最后将其翻转。
实现
在开始之前,还要确定一个很重要的问题。目前的设计,并没有考虑扩容时分配在本程序堆内存前内存区域的地址,因此使用代码简单判断下:
1 | pub fn add_to_heap(&mut self, start: usize, end: usize) { |
从 add_to_heap
的输出:
1 | Running bumb tests... |
可以看到后续追加的内存与当前程序的堆尾部是连续的,只需要更改 tail
字段扩容当前内存即可。
初始化以及扩容
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19pub unsafe fn init(&mut self, start: usize, size: usize) {
self.vec_reserve = (start, start + 96, start + 96 + 192);
self.start = start + 96 + 192 + 384;
self.head = start + 96 + 192 + 384;
self.end = start + size;
self.tail = start + size;
self.allocated = 96 + 192 + 384;
self.total = self.end - self.start;
}
pub fn add_to_heap(&mut self, start: usize, end: usize) {
self.end = end;
self.tail = end;
self.total += end - start;
}分配
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
34pub fn alloc(&mut self, layout: Layout) -> Result<NonNull<u8>, ()> {
let vec_reserve_ptr = match layout.size() {
96 => Some(self.vec_reserve.0 as *const u8),
192 => Some(self.vec_reserve.1 as *const u8),
384 => Some(self.vec_reserve.2 as *const u8),
_ => None,
};
if vec_reserve_ptr.is_some() {
return Ok(NonNull::new(vec_reserve_ptr.unwrap() as *mut u8).unwrap());
}
// check if memory is overflow
if self.tail - layout.size() <= self.head {
return Err(());
}
let ptr = if self.is_even {
let mem = self.tail - layout.size();
self.tail = mem;
NonNull::new(mem as *mut u8).unwrap()
} else {
let mem = self.head;
self.head = mem + layout.size();
NonNull::new(mem as *mut u8).unwrap()
};
self.is_even = !self.is_even;
self.allocated += layout.size();
Ok(ptr)
}释放
1
2
3
4
5
6
7
8pub fn dealloc(&mut self, pos: NonNull<u8>, layout: Layout) {
if (pos.as_ptr() as usize) < self.start + 96 + 192 + 384 {
return;
}
self.tail +=layout.size();
self.allocated -= layout.size();
}
越界访问
第 32 轮分配时发生了 Unhandled Supervisor Page Fault
,本机的 panic
位置为 0xffffffc0802005fc
,远在可分配的起始地址 0xffffffc08026d2a0
前,猜测本次的 panic
发生在栈区或是发生在从 tail 进行分配或释放时发生的。
说起来很好笑,let mut pool = Vec::new();
和 items 一样,也会占用一些堆区内存,但因为其只写不读,即使内部全部都是无效的堆内存地址也没有问题。但这提醒到了一点:在分配用于 items: Vec<Vec<u8>>
的堆内存时,使用了一个特判:
1 | let vec_reserve_ptr = match layout.size() { |
这几个数字并没有问题,但先前我们忽略了 let a = vec![c; base+delta];
的 base
总是从 32 开始,每次 alloc
翻倍,delta
则每轮增加 1
。恰好, 第 32 轮的第二次 alloc
时那么不恰好的 base = 64
和 delta = 32
,这不是 96
了嘛 uwu,所以到这时候,原本该分配给 items: Vec<Vec<u8>>
的地址被当作了 tail 堆的起始地址,又因为 tail
是 tail = tail - size
取得空闲地址,所以结果是越界了。
根据观察后,在 Layout
中存在字段 align
,align
字段在 items: Vec<Vec<u8>>
分配中总是 8
,而普通分配总是 1
。
因此简单写个判断就可以解决了:
1 | if vec_reserve_ptr.is_some() && layout.align() == 8 { |
处理完这个问题后,算法成功跑到 152 轮。
1 | Indicator: 152 |
预分配:尽可能使用更多的内存
在观察 152 轮和 add_memory
的 log
时发现:
- 本轮的偶数区并没有得到释放。
- 内存从一开始分配起,每次扩容即为前一次空间
x2
,最大为32 MB
。而 qemu 虚拟机分配的是128MB
,只分配到了32MB
,显然还不是极限。
1 | pub fn alloc(&mut self, layout: Layout) -> Result<NonNull<u8>, ()> { Err(()) } |
1 | Indicator: 0 |
因此,我们可以打第一次 alloc 开始就强迫系统预分配一大堆内存给测例。比如在 pub fn alloc(&mut self, layout: Layout) -> Result<NonNull<u8>, ()>
的合理位置添加下面的代码:
1 | // get as much memory as possible |
修改后,轮数为 189。此时,内存分配量到达了 64MB
,这样下来仍有很多剩余空间。
再挤一挤,内存还是有的
看一眼 axalloc
中:
1 | let old_size = balloc.total_bytes(); |
不难发现 axalloc
是依赖于 total_bytes()
来判断该扩容多少内存。
简单一些处理,只需要修改原先的 total_bytes
代码,使得 total_bytes
永远返回 0
。
1 | pub fn total_bytes(&self) -> usize { |
修改后的最终轮数为 245。
也许还会在 我的博客 更新,代码仓库可能会在后续删除,为了不再占用的过多空间,完整代码同样也附带在我的博客里