ROSR 是『RISC-V OS using Rust』的缩写,是由 Stephen Marz 在他的系列博客中提供的操作系统开发教程。
本章描述系统堆内存管理。
前面章节已经描述过我们通过 QEMU 给整个系统提供了 128M 字节内存空间,ELF 文件加载到内存中后除开代码、全局变量、栈等占用的内存之外,其余部分我们都分配了堆。所以堆的部分就由操作系统来管理分配。
管理系统堆空间时,分为 3 个部分:
- 页分配
- 字节分配
- 编程内存管理单元
本 3.1 节主要描述页分配。
按页分配表示以页为粒度来分配内存空间,RISC-V 和大多数架构一样最小页空间占用 4096 字节。按页分配的方法有很多,这里采用描述符分配方式。
在进入正题之前,我们先来看几个重要的参数:
1 2
| PROVIDE(_heap_start = _stack_end); PROVIDE(_heap_size = _memory_end - _heap_start);
|
linker script 提供了 2 个重要符号:堆起始地址(_heap_start
)和堆大小(_heap_size
),这些符号又通过 mem.S
文件作为全局常量传入 rust:
1 2 3 4 5
| .global HEAP_START HEAP_START: .dword _heap_start
.global HEAP_SIZE HEAP_SIZE: .dword _heap_size
|
在 page.rs
的 init
函数中:
1 2 3 4 5 6 7 8 9
| let num_pages = HEAP_SIZE / PAGE_SIZE;
ALLOC_START = align_val( HEAP_START + num_pages * size_of::<Page,>(), PAGE_ORDER, );
|
第一行算出堆内存总共的页数,最后一行算出用于分配的堆内存起始地址。
描述符分配法
每次分配都按照连续页分配,并且只存储首页地址。为了达到此目标必须每页内存都有一个字节大小的描述符。此描述符包含 2 种标志:1)此页是否被分配;2)是否为连续分配内存的最后一页。
由此定义如下数据结构:
1 2 3 4 5 6 7 8 9 10
| #[repr(u8)] pub enum PageBits { Empty = 0, Taken = 1 << 0, Last = 1 << 1, }
pub struct Page { flags: u8, }
|
分配内存页
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
| pub fn alloc(pages: usize) -> *mut u8 { assert!(pages > 0); unsafe { let num_pages = HEAP_SIZE / PAGE_SIZE; let ptr = HEAP_START as *mut Page; for i in 0..num_pages - pages { let mut found = false; if (*ptr.add(i)).is_free() { found = true; for j in i..i + pages { if (*ptr.add(j)).is_taken() { found = false; break; } } } if found { for k in i..i + pages - 1 { (*ptr.add(k)).set_flag(PageBits::Taken); } (*ptr.add(i+pages-1)).set_flag(PageBits::Taken); (*ptr.add(i+pages-1)).set_flag(PageBits::Last); return (ALLOC_START + PAGE_SIZE * i) as *mut u8; } } }
null_mut() }
|
如上为内存页分配函数,函数参数为需要的页数。
第 2 行:判断页数是否有效;
第 4 行:算出堆内存空间总页数;
第 5 行:将堆起始地址类型转换为可变页指针;
第 6~26 行:找出满足要求连续空闲页并分配;
第 8~16 行:在第 i 页为空闲的前提下看其后是否有满足所需页数的连续页;
第 17~25 行:如果找到了满足所需页数的连续空闲页,将除最后一页的内存页标记为占用,最后一页标记为占用以及尾,并返回页指针。
回收内存页
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| pub fn dealloc(ptr: *mut u8) { assert!(!ptr.is_null()); unsafe { let addr = HEAP_START + (ptr as usize - ALLOC_START) / PAGE_SIZE; assert!(addr >= HEAP_START && addr < HEAP_START + HEAP_SIZE); let mut p = addr as *mut Page; while (*p).is_taken() && !(*p).is_last() { (*p).clear(); p = p.add(1); } assert!( (*p).is_last() == true, "Possible double-free detected! (Not taken found \ before last)" ); (*p).clear(); } }
|
如上为内存页回收函数,函数参数为需要回收的页指针。
第 2 行:判断页指针是否有效;
第 4~7 行:算出此页内存所对应的页描述符;
第 8~17 行:依次将所有分配的内存页对应的页描述符标记清零,表示设置为未占用。