Chapter 4 内存管理
内存布局
所谓OS内核无非就是一个裸机上跑的程序,首先要确定的就是程序本身的内存布局问题。
说到内存布局那就是 linker 的工作范围了,linker script启动。
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
| // @linker.ld OUTPUT_ARCH(riscv) ENTRY(_start) BASE_ADDRESS = 0x80200000; // 程序加载的起始地址
SECTIONS { . = BASE_ADDRESS; skernel = .; // 定义标签 skernel -> 0x80200000
stext = .; // 定义标签 stext 代码段开始 .text : { // .text 段,把之前的几个段放这里 *(.text.entry) . = ALIGN(4K); strampoline = .; // 定义标签 strampoline 跳板页 *(.text.trampoline); . = ALIGN(4K); *(.text .text.*) }
. = ALIGN(4K); etext = .; // 定义标签 etext 代码段结束 srodata = .; // 定义标签 srodata 只读数据段开始 .rodata : { *(.rodata .rodata.*) *(.srodata .srodata.*) }
. = ALIGN(4K); erodata = .; // 定义标签 erodata 只读数据段结束 sdata = .; // 定义标签 sdata 数据段开始 .data : { *(.data .data.*) *(.sdata .sdata.*) }
. = ALIGN(4K); edata = .; // 定义标签 edata 数据段结束 sbss_with_stack = .; // 定义标签 sbss_with_stack 堆栈段开始 .bss : { // .bss 段 *(.bss.stack) sbss = .; *(.bss .bss.*) *(.sbss .sbss.*) }
. = ALIGN(4K); ebss = .; // 定义标签 ebss 堆栈段结束 ekernel = .; // 定义标签 ekernel 内核结束
/DISCARD/ : { // 丢弃段 *(.eh_frame) } }
|
所以,rcore的内存布局:
- skernel(0x80200000) - srodata: 代码段
- srodata - erodata: 只读数据段
- sdata - edata: 数据段
- sbss_with_stack - sbss: 栈
- sbss - ebss: bss段
- ekernel: 内核结束
在 entry.asm
中定义了 _start
符号,这是程序的入口地址
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| .section .text.entry .globl _start _start: la sp, boot_stack_top call rust_main ; 调用 rust_main 函数
; 下面定义一些全局变量 ; boot_stack_lower_bound 是堆栈的起始地址,大小为 16 个 Page = 64KB ; boot_stack_top 是堆栈的结束地址 .section .bss.stack .globl boot_stack_lower_bound boot_stack_lower_bound: .space 4096 * 16 .globl boot_stack_top boot_stack_top:
|
Rust语言的入口是 rust_main 函数,这个函数在 main.rs 中定义
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| #[no_mangle]
pub fn rust_main() -> ! { clear_bss(); kernel_log_info(); mm::init(); println!("[kernel] back to world!"); mm::remap_test(); trap::init(); trap::enable_timer_interrupt(); timer::set_next_trigger(); task::run_first_task(); panic!("Unreachable in rust_main!"); }
|
内存管理子系统
内存管理子系统主要包括:
- heap_allocator: 堆内存管理
- frame_allocator: 页表管理
- KERNEL_SPACE: 内核空间
1 2 3 4 5
| pub fn init() { heap_allocator::init_heap(); frame_allocator::init_frame_allocator(); KERNEL_SPACE.exclusive_access().activate(); }
|
第一部分内核堆其实就是一个巨大的全局数组,会被编译器放在 .bss 段,然后通过堆内存管理器进行管理。
1 2 3 4 5 6 7 8 9 10 11
| pub const KERNEL_HEAP_SIZE: usize = 0x200_0000;
static mut HEAP_SPACE: [u8; KERNEL_HEAP_SIZE] = [0; KERNEL_HEAP_SIZE];
pub fn init_heap() { unsafe { HEAP_ALLOCATOR .lock() .init(HEAP_SPACE.as_ptr() as usize, KERNEL_HEAP_SIZE); } }
|
第二部分是物理页管理,负责管理计算机上从 ekernel 到 MEMORY_END 之间的物理内存。物理内存是分页管理的,页大小固定是4k。
PhysPageNum
物理页号PhysAddr
物理地址VirtPageNum
虚拟页号VirtAddr
虚拟地址
1 2 3 4 5 6 7 8 9 10 11 12
| pub const MEMORY_END: usize = 0x88000000;
pub fn init_frame_allocator() { extern "C" { fn ekernel(); } FRAME_ALLOCATOR.exclusive_access().init( PhysAddr::from(ekernel as usize).ceil(), PhysAddr::from(MEMORY_END).floor(), ); }
|
第三部分页表管理,记录内核以及进程的地址空间
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 struct MemorySet { page_table: PageTable, areas: Vec<MapArea>, }
pub struct PageTable { root_ppn: PhysPageNum, frames: Vec<FrameTracker>, }
pub struct MapArea { vpn_range: VPNRange, data_frames: BTreeMap<VirtPageNum, FrameTracker>, map_type: MapType, map_perm: MapPermission, }
pub type VPNRange = SimpleRange<VirtPageNum>;
pub struct FrameTracker { pub ppn: PhysPageNum, }
|
MapArea
结构表示一段连续的虚拟内存映射,用于快捷修改page_table
。
内存映射分为两种类型:
Identical
:虚拟页号X映射到物理页号XFramed
:虚拟页映射到临时分配的物理页
相关方法:
MapArea::map(self, page_table)
修改page_table
,执行自身所描述的映射MapArea::unmap(self, page_table)
修改page_table
,取消自身所描述的映射MapArea::append_to(self, page_table, new_end)
修改page_table
,更新自身描述的映射的结束位置MapArea::shrink_to(self, page_table, new_end)
修改page_table
,更新自身描述的映射的结束位置
跳板页映射:把地址空间最高的地址映射到 strampoline 页
1 2 3 4 5 6 7 8 9
| pub const TRAMPOLINE: usize = usize::MAX - PAGE_SIZE + 1;
fn map_trampoline(&mut self) { self.page_table.map( VirtAddr::from(TRAMPOLINE).into(), PhysAddr::from(strampoline as usize).into(), PTEFlags::R | PTEFlags::X, ); }
|
Chapter 6 文件系统
与内存类似,硬盘也被分成固定大小的块,按照 block_id
读写。rCore采用的块大小为512字节。
块读写层
BlockDevice::read_block(&self, block_id: usize, buf)
从硬盘上读取一个块BlockDevice::write_block(&self, block_id: usize, buf)
在硬盘上写入一个块
目前只有一个 VirtIOBlock
实现了这个 trait
硬盘块缓存
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| pub struct BlockCache { cache: [u8; BLOCK_SZ], block_id: usize, block_device: Arc<dyn BlockDevice>, modified: bool, }
impl BlockCache { pub fn read<T, V>(&self, offset: usize, f: impl FnOnce(&T) -> V) -> V { f(self.get_ref(offset)) }
pub fn modify<T, V>(&mut self, offset: usize, f: impl FnOnce(&mut T) -> V) -> V { f(self.get_mut(offset)) } }
|
提供了read
和modify
两个API用于读和写一个硬盘块,读和写的操作需要作为一个FnOnce
回调函数传入。
缓存块的管理内部使用了一个队列:
1 2 3 4 5 6 7 8 9 10 11
| pub struct BlockCacheManager { queue: VecDeque<(usize, Arc<Mutex<BlockCache>>)>, }
impl BlockCacheManager { pub fn get_block_cache( &mut self, block_id: usize, block_device: Arc<dyn BlockDevice>, ) -> Arc<Mutex<BlockCache>> { ... } }
|
get_block_cache(block_id, block_device)
用于获取对应块的 BlockCache
。
EasyFS 文件系统
1 2 3 4 5 6 7 8 9 10 11 12
| pub struct EasyFileSystem { pub block_device: Arc<dyn BlockDevice>, pub inode_bitmap: Bitmap, pub data_bitmap: Bitmap, inode_area_start_block: u32, data_area_start_block: u32, }
|
文件系统结构:
[0]
:超级块;[1, 1+inode_bitmap_blocks)
:inode位图区- 大小:共
inode_bitmap_blocks
个块,每个块大小是 512 字节,能够表示 4k 个状态位 - 记录每一个
inode
是否被使用
[inode_area_start_block, ]
:inode区- 保存位图区能表示数量的
inode
inode
连续存储,总大小取整到block边界inode
按顺序编号,从0开始
[...]
:data位图区[data_area_start_block, total_blocks)
:data区
超级块:
1 2 3 4 5 6 7 8
| pub struct SuperBlock { magic: u32, pub total_blocks: u32, pub inode_bitmap_blocks: u32, pub inode_area_blocks: u32, pub data_bitmap_blocks: u32, pub data_area_blocks: u32, }
|
inode结构设计:
1 2 3 4 5 6 7 8 9
| #[repr(C)] pub struct DiskInode { pub size: u32, pub direct: [u32; INODE_DIRECT_COUNT], pub indirect1: u32, pub indirect2: u32, type_: DiskInodeType, }
|
inode采用了经典的三级索引设计,有28个直接条目;1个间接索引,包含128个条目;1个二级间接索引,包含128个间接索引,等于128*128
个直接条目。
Easyfs中的0
号inode
是文件系统的根节点。
Easyfs没有多级目录设计。
Chapter 8 同步机制
死锁检测:
- 用一个全局变量记录每个
task
的hold
和wait
集合,即当前持有的资源(及数量)和正在等待的资源。 - 在尝试获取资源(即加锁)时,进行死锁检测。
死锁检测算法就是对未来的执行状态进行模拟:找到下一个可以执行的任务,模拟执行,然后释放它拥有的资源,循环。当所有任务都可以执行,说明不存在死锁;否则就是存在死锁。