sbrk() 的作用
sbrk()
系统是做什么的,可以参考这里,简要来说,就是调整用户程序的堆(heap)顶位置,表现为对堆大小的增大或缩小。
rCore 的地址空间布局与调整
这里的 sbrk()
实现仅与用户虚拟地址空间有关。参考这里。
对于 rCore 原始布局,在低地址部分,从低地址往高地址依次为:.text
,.rodata
,.data
,.bss
,guard page
,user stack
,随后才是 heap
:

但我对这个布局不太满意,于是通过修改 mm::memory_set::MemorySet::from_elf()
,将空间布局修改为如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| /// High 256GB /// +-------------------+ /// | Trampoline Code | /// +-------------------+ <- TRAMPOLINE /// | TrapContext | /// +-------------------+ <- TRAP_CONTEXT, user_stack_top /// | User Stack | /// +-------------------+ <- user_stack_bottom /// | ... | /// /// Low 256GB /// | ... | /// +-------------------+ /// | Heap Memory | /// +-------------------+ <- heap_bottom /// | .bss | /// +-------------------+ /// | .data | /// +-------------------+ /// | .rodata | /// +-------------------+ /// | .text | /// +-------------------+ <- BASE_ADDRESS (0x10000 va)
|
heap 紧挨着 bss 段,而 user stack 调整到高地址处。这么做的原因是当物理地址理论足够的情况下,可以方便 heap 和 user stack 的拓展,同时由于二者位置相差大,不太可能重叠,而原实现相较于此,user stack 的大小限制为 8K(即两个 PAGE SIZE),由由于其位置的特殊性,上有 heap 空间,下有 guard page,kernel 段,难以拓展。这个修改很简单,我们的重点不在这里。
sbrk()
与其他的系统调用实现一样,先添加系统调用接口,再实现具体的逻辑。
1 2 3 4 5 6 7
| pub fn sys_sbrk(size: i32) -> isize { if let Some(old_brk) = change_program_brk(size) { return old_brk as isize } else { -1 } }
|
顺着调用链,我们来到 task::task::TaskControlBlock::change_brk()
,这是实际的 sbrk 的实现。主要的逻辑为根据判断传入的参数的正负判断堆空间的缩减,并更新相应的堆顶指针:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| let old_brk = self.program_brk; let new_brk = self.program_brk as isize + size as isize; let result = if size < 0 { self.memory_set.shrink_to(...) } else { self.memory_set.append_to(...) }; if result { self.program_brk = new_brk as usize; Some(old_brk) } else { None }
|
缩小和扩大 heap 空间分别对应 shrink_to
和 append_to
两个函数,而二者的逻辑较为相似,这里以 append_to
为例:
1 2 3 4 5 6
| pub fn append_to(&mut self, page_table: &mut PageTable, new_end: VirtPageNum) { for vpn in VPNRange::new(self.vpn_range.get_end(), new_end) { self.map_one(page_table, vpn); } self.vpn_range = VPNRange::new(self.vpn_range.get_start(), new_end); }
|
即朴实地为每一页在 PageTable 里创建一个映射,并没有什么花里胡哨的东西。
lazy alloc
lazy alloc 可以概括为:只标记,需要时分配。
具体来说:
- 当调用
sbrk()
申请拓展 heap 空间,内核只增加堆顶指针(在这里是 TaskControlBlock::program_brk
)的值,不做实际的内存分配,添加映射。用户程序访问这段内存会引起 PageFault,我们在处理该 Trap 的时候通过 stval
寄存器判断该地址是否位于 heap_bottom
和 program_brk
之间,若是,分配实际内存,添加页表映射;
- 当调用
sbrk()
申请缩小 heap 空间,需要减小 program_brk
的值,同时收回原堆顶和现堆顶之间的 page。
拓展 PageTable
由于原实现中 PageTable::map()
和 PageTable::unmap()
在接口方面实现较为有限,我将其进行了拓展,将
1 2
| pub fn map(&mut self, vpn: VirtPageNum, ppn: PhysPageNum, flags: PTEFlags, mut frame: Option<FrameTracker>); pub fn unmap(&mut self, vpn: VirtPageNum, dealloc: bool);
|
修改为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| pub fn map(&mut self, args: MapArgs) { let MapArgs { vpn, ppn, flags, mut frame } = args; } pub fn unmap(&mut self, args: UnmapArgs) { let vpn = args.vpn; let pte = match self.find_pte(vpn) { Some(pte) if pte.is_valid() => pte, _ => if args.panic { panic!("vpn {:?} should mapped but not", vpn); } else { return; } }; }
|
目前,MapArgs
和 UnmapArgs
中字段足够使用:
1 2 3 4 5 6 7 8 9 10 11 12
| pub struct MapArgs { vpn: VirtPageNum, ppn: PhysPageNum, flags: PTEFlags, frame: Option<FrameTracker>, }
pub struct UnmapArgs { vpn: VirtPageNum, dealloc: bool, panic: bool, }
|
这两个内以设计模式中的 Builder 设计,使用一系列的 with_...
成员函数调整参数。
为了便于控制后续课程中 lazy_alloc 的使用与否,为 lazy alloc 新增一个 feature 并使用条件编译:
1 2 3 4 5
|
[features] default = ["sbrk_lazy_alloc"] sbrk_lazy_alloc = []
|
Trap
trap_handler
中为 lazy alloc 添加的入口:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| Trap::Exception(Exception::StoreFault) | Trap::Exception(Exception::StorePageFault) | Trap::Exception(Exception::LoadFault) | Trap::Exception(Exception::LoadPageFault) => { let tcb = get_current_tcb_ref(); let ok = if stval >= tcb.heap_bottom && stval < tcb.program_brk { #[cfg(feature = "sbrk_lazy_alloc")] { lazy_alloc_page(stval.into()) } #[cfg(not(feature = "sbrk_lazy_alloc"))] { false }
|
当判断 stval
中的地址为 heap 中的有效地址时,使用 lazy_alloc_page()
分配一页内存,实现较为简单,这里不过分说明。
change_brk()
我们的重点是 change_brk()
的修改:
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
| pub fn change_brk(&mut self, size: i32) -> Option<usize> { let old_brk = self.program_brk; let new_brk = self.program_brk as isize + size as isize; if new_brk < self.heap_bottom as isize { return None; } let ok; cfg_if! { if #[cfg(feature = "sbrk_lazy_alloc")] { ok = true; if size < 0 { self.memory_set .remove_framed_area( VirtAddr::from(new_brk as usize), VirtAddr::from(self.program_brk), ); } } else { } } if ok { self.program_brk = new_brk as usize; Some(old_brk) } else { None } }
|
我们注意到,当 size > 0
我们仅仅增加了 program_brk
的值就返回,其他什么事也不做,而当该值小于 0,只增加了一点微不足道的操作:释放减少的那段空间。
至于为什么我拓展了 PageTable 的参数,下面以 remove_framed_area
为例:
1 2 3 4 5 6 7 8 9 10 11 12 13
| pub fn remove_framed_area( &mut self, start_va: VirtAddr, end_va: VirtAddr, ) { for vpn in VPNRange::new(start_va.ceil(), end_va.floor()) { self.page_table.unmap( UnmapArgs::builder(vpn) .with_dealloc(true) .with_panic(false), ) } }
|
可以看到我们通过链式构造一步一步对 UnmapArgs
做出了要求,我们要求释放物理地址,并且保证不会 panic。这里我们为什么要保证不会 panic?由于原实现中的 unmap()
,设计默认认为传给它的参数一定经过映射,当无法找到最后一级页表时,会认为是内核设计出现了 bug,于是直接通过 unwarp()
进行了 panic。
而这里我们释放 heap 空间,由于 lazy alloc 的存在,我们并不能事先知道哪些 page 被映射了而哪些没有,我们又不想付出额外的存储代价,于是选择对这段释放的空间的每一 page 进行遍历,其中有很大概率会遇见由于从未读写而未进行分配的 page,这样直接作为参数传给原来的 unmap()
必然会导致内核 panic,于是抽象出了 unmap()
的参数,为其添加 panic
字段,用于提示 unmap()
是否能够 panic。
最终的实现见 #876012b