Asterinas 内存管理
Asterinas 内存管理
研究Asterinas 对内存管理主要从以下几个方面展开:
•Asterinas对物理内存的管理方式
•Asterinas对进程虚拟地址空间的管理方式
•Asterinas虚拟地址空间与物理地址空间的映射管理方式
Asterinas对物理内存的管理方式
Asterinas kernel对于物理内存信息的获取
在操作系统的启动过程当中,物理内存等信息是bootloader传递给操作系统内核的,在X86上是由UEFI或者传统的BIOS,在UEFI与操作系统之间可以通过多种方式来传递这些信息,比如multiboot与multiboot2两个标准。
我们简单追踪一下物理内存信息如何传递到Asterinas kernel当中的,这样我们需要简单的追踪一下这个系统的启动流程。
a.linker.ld链接脚本直接指定整个操作系统编译生成镜像的入口:
Plain Text
ENTRY(__multiboot_boot)
OUTPUT_ARCH(i386:x86-64)
OUTPUT_FORMAT(elf64-x86-64)
b.通过__multiboot_boot 到__multiboot2_entry跳入RUST代码,然后进入init_memory_regions处理从multiboot传递过来的内存信息:
Rust
fn init_memory_regions(memory_regions: &'static Once<Vec>) {
let mut regions = Vec::::new();
let mb2_info: &BootInformation<'_> = MB2_INFO.get().unwrap(); //获取multiboot2头信息
// Add the regions returned by Grub.
let memory_regions_tag = mb2_info
.memory_map_tag()
.expect("Memory region not found from the Multiboot2 header!");
let num_memory_regions = memory_regions_tag.memory_areas().len();//获取multiboot2头中的内存信息
for i in 0..num_memory_regions {
let start = memory_regions_tag.memory_areas()[i].start_address();
let end = memory_regions_tag.memory_areas()[i].end_address();
let area_typ: MemoryRegionType = memory_regions_tag.memory_areas()[i].typ().into();
let region = MemoryRegion::new(
start.try_into().unwrap(),
(end - start).try_into().unwrap(),
area_typ,
);
regions.push(region); //将物理内存信息保存起来,每块内存的首地址,长度与类型。
}
Asterinas kernel对于物理内存信息的管理
Asterinas kernel在获取到物理内存信息之后,是这样来管理这些物理内存的:
a.构建一个全局性质的FrameAllocator对象来负责管理这些物理内存:
Rust
pub(crate) fn init(regions: &[MemoryRegion]) {
let mut allocator = FrameAllocator::<32>::new();
for region in regions.iter() {
if region.typ() == MemoryRegionType::Usable {
// Make the memory region page-aligned, and skip if it is too small.
let start: usize = region.base().align_up(PAGE_SIZE) / PAGE_SIZE;
let end = (region.base() + region.len()).align_down(PAGE_SIZE) / PAGE_SIZE;
if end <= start {
continue;
}
allocator.add_frame(start, end);
info!(
"Found usable region, start:{:x}, end:{:x}",
region.base(),
region.base() + region.len()
);
}
}
FRAME_ALLOCATOR.call_once(|| SpinLock::new(allocator));
}
这段代码将每块物理内存的地址与长度信息,转成了物理页编号,后续内核就是通过这个物理页号来管理物理内存页的分配:
Rust
let start: usize = region.base().align_up(PAGE_SIZE) / PAGE_SIZE;
let end = (region.base() + region.len()).align_down(PAGE_SIZE) / PAGE_SIZE;
b.在FrameAllocator中会把每个空的内存段拆成从1,2,4,8,16,32等等这样2的指数个连续物理内存页来管理物理内存。这段代码逻辑写得比较巧妙:
Rust
/// Add a range of frame number [start, end) to the allocator
pub fn add_frame(&mut self, start: usize, end: usize) {
assert!(start <= end);
let mut total = 0;
let mut current_start = start;
while current_start < end {
let lowbit = if current_start > 0 {
current_start & (!current_start + 1)
} else {
32
};
let size: usize = min(
min(lowbit, prev_power_of_two(end - current_start)),
1 << (ORDER - 1),
);
total += size;
self.free_list[size.trailing_zeros() as usize].insert(current_start);
current_start += size;
}
self.total += total;
}
c.在FrameAllocator中分配物理内存也是会按照用户要求的分配物理页的个数向上按照2个指数取整,比如用户要求分配30个物理页,会实际分配32个物理页,即2的5次方。
Rust
/// Allocate a range of frames of the given size from the allocator. The size must be a power of
/// two. The allocated range will have alignment equal to the size.
fn alloc_power_of_two(&mut self, size: usize) -> Option {
let class: usize = size.trailing_zeros() as usize;
for i in class...self.free_list.len() {
// Find the first non-empty size class
if !self.free_list[i].is_empty() {
// Split buffers
for j in (class + 1...i + 1).rev() {
if let Some(block_ref) = self.free_list[j].iter().next() {
let block = *block_ref;
self.free_list[j - 1].insert(block + (1 << (j - 1)));
self.free_list[j - 1].insert(block);
self.free_list[j].remove(&block);
} else {
return None;
}
}
let result = self.free_list[class].iter().next().clone();
if let Some(result_ref) = result {
let result = *result_ref;
self.free_list[class].remove(&result);
self.allocated += size;
return Some(result);
} else {
return None;
}
}
}
None
}
Asterinas对进程虚拟地址空间的管理方式
在Asterinas系统中,对于应用进程的虚拟地址空间会有这么几个对象概念,分别是:
•VMAR:Virtual Memory Address,负责管理整个进程的虚拟地址空间。
•VmSpace:Virtual Memory Space, 这个会去负责做具体的映射工作,从虚拟地址映射到具体的物理内存。
•VMO:Virtual Memory Object,这个对象会去分配具体的物理内存,在Asterinas中定义是VmFrames。
简单描述下Asterinas管理分配内存方式,由VMAR来分配下虚拟地址空间,然后从FrameAllocator申请出满足要求的物理内存页,最后由VmSpace来执行具体的映射工作。
接下来我们来逐步分析下这些过程:
创建VMAR
在进程创建的时候,创建进程的VMAR:
Rust
pub fn new_root() -> Arc {
let mut free_regions = BTreeMap::new();
let root_region = FreeRegion::new(ROOT_VMAR_LOWEST_ADDR...ROOT_VMAR_HIGHEST_ADDR);
free_regions.insert(root_region.start(), root_region);
let vmar_inner = VmarInner {
is_destroyed: false,
child_vmar_s: BTreeMap::new(),
vm_mappings: BTreeMap::new(),
free_regions,
};
Vmar_::new(vmar_inner, VmSpace::new(), 0, ROOT_VMAR_HIGHEST_ADDR, None)
}
进程的虚拟地址空间中空余的部分是:
Rust
const ROOT_VMAR_LOWEST_ADDR: Vaddr = 0x0010_0000;
const ROOT_VMAR_HIGHEST_ADDR: Vaddr = 0x1000_0000_0000;
创建VmSpace:
在创建进程的VMAR的同时,创建VmSpace:
Rust
pub fn new() -> Self {
Self {
memory_set: Arc::new(Mutex::new(MemorySet::new())),
}
}
在VmSpace中创建MemorySet对象:
Rust
pub fn new() -> Self {
let mut page_table: PageTable = PageTable::<PageTableEntry, UserMode>::new(PageTableConfig {
address_width: super::page_table::AddressWidth::Level4,
});
let mapped_pte = crate::arch::mm::ALL_MAPPED_PTE.lock();
for (index, pte) in mapped_pte.iter() {
// Safety: These PTEs are all valid PTEs fetched from the initial page table during memory initialization.
unsafe {
page_table.add_root_mapping(*index, pte);
}
}
Self {
pt: page_table,
areas: BTreeMap::new(),
}
}
pub fn new(config: PageTableConfig) -> Self {
let root_frame = VmAllocOptions::new(1).alloc_single().unwrap();
Self {
root_paddr: root_frame.start_paddr(),
tables: vec![root_frame],
config,
_phantom: PhantomData,
}
}
首先分配出一个物理页用来存放PT表。这个表是该进程的PML4表。这个详细的我们在后续的虚拟地址空间与物理地址空间的映射管理方式再来详细分析。
分配WMO对像
根据需要使用的物理内存数量去分配WMO对像。我们以进程heap堆分配为例子来分析下,在用户进程创建的时候,会默认创建一个用户堆:
Rust
pub const fn new() -> Self {
UserHeap {
heap_base: USER_HEAP_BASE,
heap_size_limit: USER_HEAP_SIZE_LIMIT,
current_heap_end: AtomicUsize::new(USER_HEAP_BASE),
}
}
用户堆的虚拟地址空间的起点与大小为:
Rust
pub const USER_HEAP_BASE: Vaddr = 0x0000_0000_1000_0000;
pub const USER_HEAP_SIZE_LIMIT: usize = PAGE_SIZE * 1000,
分配vmo,首先把需要分配的大小针对页帧大小向上对其,然后进行分配:
Rust
fn alloc_vmo_(size: usize, flags: VmoFlags, pager: Option<Arc>) -> Result<Vmo_> {
let size = size.align_up(PAGE_SIZE);
let committed_pages = committed_pages_if_continuous(flags, size)?;
let vmo_inner = VmoInner {
pager,
size,
committed_pages,
inherited_pages: None,
is_cow: false,
};
Ok(Vmo_ {
flags,
inner: Mutex::new(vmo_inner),
})
}
将需要分配的内存大小转换成需要分配的内存页的数量
Rust
fn committed_pages_if_continuous(flags: VmoFlags, size: usize) -> Result<BTreeMap<usize, VmFrame>> {
if flags.contains(VmoFlags::CONTIGUOUS) {
// if the vmo is continuous, we need to allocate frames for the vmo
let frames_num = size / PAGE_SIZE;
let frames = VmAllocOptions::new(frames_num)
.is_contiguous(true)
.alloc()?;
let mut committed_pages = BTreeMap::new();
for (idx, frame) in frames.into_iter().enumerate() {
committed_pages.insert(idx * PAGE_SIZE, frame);
}
Ok(committed_pages)
} else {
// otherwise, we wait for the page is read or write
Ok(BTreeMap::new())
}
}
根据具体的页面数量进行分配:
Rust
/// Allocate a range of frames from the allocator, returning the first frame of the allocated
/// range.
pub fn alloc(&mut self, count: usize) -> Option<usize> {
let size = count.next_power_of_two();
self.alloc_power_of_two(size)
}
具体分配算法,在前面关于asterinas对于物理内存的管理中提到过:Asterinas 内存管理,FrameAllocator会将内存页面数量按照1,2,4,8,64等这样的2的指数倍形式分类存放,在分配的时候也是首先找到需要分配物理页数量最接近的2的指数倍量。
Rust
/// Allocate a range of frames of the given size from the allocator. The size must be a power of
/// two. The allocated range will have alignment equal to the size.
fn alloc_power_of_two(&mut self, size: usize) -> Option {
let class = size.trailing_zeros() as usize;
for i in class...self.free_list.len() {
// Find the first non-empty size class
if !self.free_list[i].is_empty() {
// Split buffers
for j in (class + 1...i + 1).rev() {
if let Some(block_ref) = self.free_list[j].iter().next() {
let block = *block_ref;
self.free_list[j - 1].insert(block + (1 << (j - 1)));
self.free_list[j - 1].insert(block);
self.free_list[j].remove(&block);
} else {
return None;
}
}
let result = self.free_list[class].iter().next().clone();
if let Some(result_ref) = result {
let result = *result_ref;
self.free_list[class].remove(&result);
self.allocated += size;
return Some(result);
} else {
return None;
}
}
}
None
}
在WMO对象分配完成后,其实也就是分配了具体的物理页了,接下来需要做的就是需要将分配的物理页映射到虚拟地址空间当中去。
Asterinas虚拟地址空间与物理地址空间的映射管理方式
在这个部分里,我们来根据Asterinas系统来分析下在X86平台上如何进行物理内存地址与虚拟内存地址的映射工作。
X86平台的内存映射机制
在X86平台上有专门一套虚拟内存与物理内存的映射机制,如下图所示,简单描述下就是,在X86平台上,是采用了4级映射机制,虽然是64位系统,但是地址的低48位是用来寻址的,其中CR3寄存器用来存放PML4表的地址,PML4表存放在一个内存帧当中,默认是4K大小,PML4一共有512项,48位地址的高9位代表是在PML4表中的项数,每一项是一个8字节的指针值,指向的是对应的PDE表地址,同样的,一个PDE表也是4K大小,也有512项,48位地址中接下来的9位代表的是具体在PDE表中的某一项的,这一项也是一个8字节的指针,指向的是PTE表,类似的,PTE指向的是一个具体的物理页地址,这样就完成了虚拟地址到物理地址的映射转换,而48位中的低12位则是在4K页面内的偏移。(注:X86平台上内存映射还有其他种模式,这里只介绍最简单的模式)。
Asterinas系统在X86平台上对于页表的管理方式
我们首先从缺页中断开始跟踪,缺页中断是在当我们访问某个虚拟地址,但是这个虚拟地址没有具体的物理内存映射的时候会产生,而在Asterinas系统中由handle_page_fault来负责处理,在这里我们先忽略掉Asterinas异常处理流程,直接从handle_page_fault函数开始。
Rust
pub fn handle_page_fault(
&self,
page_fault_addr: Vaddr,
not_present: bool,
write: bool,
) -> Result<()> {
let vmo_offset = self.vmo_offset() + page_fault_addr - self.map_to_addr();
if vmo_offset >= self.vmo.size() {
return_errno_with_message!(Errno::EACCES, "page fault addr is not backed up by a vmo");
}
let page_idx = vmo_offset / PAGE_SIZE;
if write {
self.vmo.check_rights(Rights::WRITE)?;
} else {
self.vmo.check_rights(Rights::READ)?;
}
let required_perm = if write { VmPerm::W } else { VmPerm::R };
self.check_perm(&page_idx, &required_perm)?;
let frame = self.vmo.get_committed_frame(page_idx, write)?;
// If read access to cow vmo triggers page fault, the map should be readonly.
// If user next tries to write to the frame, another page fault will be triggered.
let is_readonly = self.vmo.is_cow_child() && !write;
self.map_one_page(page_idx, frame, is_readonly)
}
该函数的处理逻辑是这样的:
a.首先找到这个产生缺页中断的地址与某一个已经映射的虚拟地址区间首地址之间的差值:
Rust
let vmo_offset = self.vmo_offset() + page_fault_addr - self.map_to_addr();
b.将这个差值转成对应的内存帧页编号
Rust
let page_idx = vmo_offset / PAGE_SIZE;
c.分配一个新的物理内存帧
Rust
let frame = self.vmo.get_committed_frame(page_idx, write)?;
d.映射这个物理内存帧,page_idx为当前需要映射区间的虚拟内存页表号, VmFrame是需要映射的物理内存帧。
Rust
pub(super) fn map_one_page(
&self,
page_idx: usize,
frame: VmFrame,
is_readonly: bool,
) -> Result<()> {
let parent = self.parent.upgrade().unwrap();
let vm_space = parent.vm_space();
self.inner
.lock()
.map_one_page(&self.vmo, vm_space, page_idx, frame, is_readonly)
}
e.遍历一遍PML4表,寻找下需要映射的虚拟地址在这个四级映射表中有没有对应的映射项。
Rust
unsafe fn do_map(
&mut self,
vaddr: Vaddr,
paddr: Paddr,
flags: T::F,
) -> Result<(), PageTableError> {
let last_entry = self.page_walk(vaddr, true).unwrap();
trace!(
"Page Table: Map vaddr:{:x?}, paddr:{:x?}, flags:{:x?}",
vaddr,
paddr,
flags
);
if last_entry.is_used() && last_entry.flags().is_present() {
return Err(PageTableError::InvalidModification);
}
last_entry.update(paddr, flags);
tlb_flush(vaddr);
Ok(())
}
f.page_walk的逻辑十分清晰而且易读,主要做了这几个动作:
Rust
fn page_walk(&mut self, vaddr: Vaddr, create: bool) -> Option<&mut T> {
let mut count = self.config.address_width as usize;
debug_assert!(size_of::() * (T::page_index(vaddr, count) + 1) <= PAGE_SIZE);
// Safety: The offset does not exceed the value of PAGE_SIZE.
// It only change the memory controlled by page table.
let mut current: &mut T = unsafe {
&mut *(paddr_to_vaddr(self.root_paddr + size_of::() * T::page_index(vaddr, count))
as *mut T)
};
while count > 1 {
if !current.flags().is_present() {
if !create {
return None;
}
// Create next table
let frame = VmAllocOptions::new(1).alloc_single().unwrap();
// Default flags: read, write, user, present
let flags = T::F::new()
.set_present(true)
.set_accessible_by_user(true)
.set_readable(true)
.set_writable(true);
current.update(frame.start_paddr(), flags);
self.tables.push(frame);
}
if current.flags().is_huge() {
break;
}
count -= 1;
debug_assert!(size_of::<T>() * (T::page_index(vaddr, count) + 1) <= PAGE_SIZE);
// Safety: The offset does not exceed the value of PAGE_SIZE.
// It only change the memory controlled by page table.
current = unsafe {
&mut *(paddr_to_vaddr(
current.paddr() + size_of::<T>() * T::page_index(vaddr, count),
) as *mut T)
};
}
Some(current)
}
首先找出需要映射的虚拟地址在PML4表中的位置:
Rust
let mut current: &mut T = unsafe {
&mut *(paddr_to_vaddr(self.root_paddr + size_of::() * T::page_index(vaddr, count))
as *mut T)
};
其次如果PML4表项指向的PDPTE页表不存在,则重新分配一个物理内存页用来当成PDPTE页表,并且将新分配的PDPTE页的物理地址写入PML4页对应的位置中。
Rust
if !current.flags().is_present() {
if !create {
return None;
}
// Create next table
let frame = VmAllocOptions::new(1).alloc_single().unwrap();
// Default flags: read, write, user, present
let flags = T::F::new()
.set_present(true)
.set_accessible_by_user(true)
.set_readable(true)
.set_writable(true);
current.update(frame.start_paddr(), flags);
self.tables.push(frame);
}
类似的逻辑循环处理PDE与PTE页,最终找到在PTE页中的位置。
g.将新分配的物理页的地址更新进PTE页表中:
Rust
unsafe fn do_map(
&mut self,
vaddr: Vaddr,
paddr: Paddr,
flags: T::F,
) -> Result<(), PageTableError> {
let last_entry = self.page_walk(vaddr, true).unwrap();
trace!(
"Page Table: Map vaddr:{:x?}, paddr:{:x?}, flags:{:x?}",
vaddr,
paddr,
flags
);
if last_entry.is_used() && last_entry.flags().is_present() {
return Err(PageTableError::InvalidModification);
}
last_entry.update(paddr, flags);
tlb_flush(vaddr);
Ok(())
}
由此完成虚拟地址与物理地址的映射关系。
Asterinas系统进程切换刷新PML4表
Asterinas在做进程调度的时候需要刷新CR3寄存器,使用本进程的PML4表:
Rust
/// Starts executing in the user mode. Make sure current task is the task in UserMode
.
///
/// The method returns for one of three possible reasons indicated by UserEvent
.
/// 1. The user invokes a system call;
/// 2. The user triggers an exception;
/// 3. The user triggers a fault.
///
/// After handling the user event and updating the user-mode CPU context,
/// this method can be invoked again to go back to the user space.
pub fn execute(&mut self) -> UserEvent {
unsafe {
self.user_space.vm_space().activate();
}
debug_assert!(Arc::ptr_eq(&self.current, &Task::current()));
self.context.execute()
}
实际刷新:
Rust
pub unsafe fn activate(&self) {
#[cfg(target_arch = "x86_64")]
crate::arch::x86::mm::activate_page_table(
self.memory_set.lock().pt.root_paddr(),
x86_64::registers::control::Cr3Flags::PAGE_LEVEL_CACHE_DISABLE,
);
}
Asterinas kernel中虚拟地址空间与物理地址空间的映射
在Asterinas 启动阶段进入kernel之前会提前做好虚拟地址空间与物理地址空间的映射,简而言之则是维护自己的PML4表,这样我们才能从前面章节看到的,当分配好物理地址的时候,直接可以用paddr_to_vaddr函数将物理地址转成虚拟地址。我们来看下这个kernel的这个PML4表映射的建立过程:
在汇编代码中直接分配好内存,这会随着kernel的二进制镜像被装在进内存而直接分配(占用)好。
Rust
// The page tables and the stack
.align 4096
.global boot_page_table_start
boot_page_table_start:
boot_pml4:
.skip 4096
boot_pdpt:
.skip 4096
boot_pd:
boot_pd_0g_1g:
.skip 4096
boot_pd_1g_2g:
.skip 4096
boot_pd_2g_3g:
.skip 4096
boot_pd_3g_4g:
.skip 4096
boot_pt:
.skip 4096 * 512 * 4
boot_pd_32g:
.skip 4096
boot_pt_32g:
.skip 4096 * 512
boot_page_table_end:
建立PML4表映射:
Rust
// PML4: 0x00000000_00000000 ~ 0x00000000_3fffffff
// 0x00000000_40000000 ~ 0x00000000_7fffffff
// 0x00000000_80000000 ~ 0x00000000_bfffffff
// 0x00000000_c0000000 ~ 0x00000000_ffffffff
lea edi, [boot_pml4]
lea eax, [boot_pdpt + (PTE_PRESENT | PTE_WRITE | PTE_GLOBAL)]
mov dword ptr [edi], eax
mov dword ptr [edi + 4], 0
// PML4: 0xffff8000_00000000 ~ 0xffff8000_3fffffff
// 0xffff8000_40000000 ~ 0xffff8000_7fffffff
// 0xffff8000_80000000 ~ 0xffff8000_bfffffff
// 0xffff8000_c0000000 ~ 0xffff8000_ffffffff
// 0xffff8008_00000000 ~ 0xffff8008_3fffffff
lea edi, [boot_pml4 + 0x100 * 8]
lea eax, [boot_pdpt + (PTE_PRESENT | PTE_WRITE | PTE_GLOBAL)]
mov dword ptr [edi], eax
mov dword ptr [edi + 4], 0
// PML4: 0xffffffff_80000000 ~ 0xffffffff_bfffffff
// 0xffffffff_c0000000 ~ 0xffffffff_ffffffff
lea edi, [boot_pml4 + 0x1ff * 8]
lea eax, [boot_pdpt + (PTE_PRESENT | PTE_WRITE | PTE_GLOBAL)]
mov dword ptr [edi], eax
mov dword ptr [edi + 4], 0
// PDPT: 0x00000000_00000000 ~ 0x00000000_3fffffff
lea edi, [boot_pdpt]
lea eax, [boot_pd_0g_1g + (PTE_PRESENT | PTE_WRITE | PTE_GLOBAL)]
mov dword ptr [edi], eax
mov dword ptr [edi + 4], 0
// PDPT: 0x00000000_40000000 ~ 0x00000000_7fffffff
lea edi, [boot_pdpt + 0x1 * 8]
lea eax, [boot_pd_1g_2g + (PTE_PRESENT | PTE_WRITE | PTE_GLOBAL)]
mov dword ptr [edi], eax
mov dword ptr [edi + 4], 0
// PDPT: 0x00000000_80000000 ~ 0x00000000_bfffffff
lea edi, [boot_pdpt + 0x2 * 8]
lea eax, [boot_pd_2g_3g + (PTE_PRESENT | PTE_WRITE | PTE_GLOBAL)]
mov dword ptr [edi], eax
mov dword ptr [edi + 4], 0
// PDPT: 0x00000000_c0000000 ~ 0x00000000_ffffffff
lea edi, [boot_pdpt + 0x3 * 8]
lea eax, [boot_pd_3g_4g + (PTE_PRESENT | PTE_WRITE | PTE_GLOBAL)]
mov dword ptr [edi], eax
mov dword ptr [edi + 4], 0
// 1000 00000|000 100000|00 0000000|0 00000000 000
// PDPT: 0xffff8008_00000000 ~ 0xffff8008_3fffffff
lea edi, [boot_pdpt + 0x20 * 8]
lea eax, [boot_pd_32g + (PTE_PRESENT | PTE_WRITE | PTE_GLOBAL)]
mov dword ptr [edi], eax
mov dword ptr [edi + 4], 0
// PDPT: 0xffffffff_80000000 ~ 0xffffffff_bfffffff
lea edi, [boot_pdpt + 0x1fe * 8]
lea eax, [boot_pd_0g_1g + (PTE_PRESENT | PTE_WRITE | PTE_GLOBAL)]
mov dword ptr [edi], eax
mov dword ptr [edi + 4], 0
// PDPT: 0xffffffff_c0000000 ~ 0xffffffff_ffffffff
lea edi, [boot_pdpt + 0x1ff * 8]
lea eax, [boot_pd_1g_2g + (PTE_PRESENT | PTE_WRITE | PTE_GLOBAL)]
mov dword ptr [edi], eax
mov dword ptr [edi + 4], 0
// Page Directory: map to low 1 GiB * 4 space
lea edi, [boot_pd]
lea eax, [boot_pt + (PTE_PRESENT | PTE_WRITE | PTE_GLOBAL)]
mov ecx, 512 * 4 // (of entries in PD) * (number of PD)
write_pd_entry:
mov dword ptr [edi], eax
mov dword ptr [edi + 4], 0
add eax, 0x1000 // +4KiB to next table
add edi, 8
loop write_pd_entry
// Page Directory: map to 1 GiB space offset 32GiB
lea edi, [boot_pd_32g]
lea eax, [boot_pt_32g + (PTE_PRESENT | PTE_WRITE | PTE_GLOBAL)]
mov ecx, 512 // (of entries in PD)
write_pd_32g_entry:
mov dword ptr [edi], eax
mov dword ptr [edi + 4], 0x0
add eax, 0x1000 // +4KiB to next table
add edi, 8
loop write_pd_32g_entry
// Page Table: map to low 1 GiB * 4 space
lea edi, [boot_pt]
mov eax, (PTE_PRESENT | PTE_WRITE | PTE_GLOBAL) // Offset 0
mov ecx, 512 * 512 * 4 // (of entries in PT) * (number of PT) * (number of PD)
write_pt_entry:
mov dword ptr [edi], eax
mov dword ptr [edi + 4], 0
add eax, 0x1000 // +4KiB
add edi, 8
loop write_pt_entry
// Page Table: map to 1 GiB space offset 32GiB
lea edi, [boot_pt_32g]
mov eax, (PTE_PRESENT | PTE_WRITE | PTE_GLOBAL) // Offset 0x8_00000000 but should write to high 32bits
mov ecx, 512 * 512 // (of entries in PT) * (number of PT)
write_pt_32g_entry:
mov dword ptr [edi], eax
mov dword ptr [edi + 4], 0x8 // Offset 0x8_00000000
add eax, 0x1000 // +4KiB
add edi, 8
loop write_pt_32g_entry
jmp enable_long_mode
简单描述下这个kernel中实现的PML4表,一共有一张PML4表,一张PDPT表,4张pd表,512*4张PT表,其他的我们暂时忽略一下。PML4表的512项中有3项被使用,分别是第0项,第256项根第511项。而这三项都是指向同一个PDPT页表。而PDPT页表中的512个列表项被使用了4个,分别指向了4张PD表。也就是说同一个物理地址被映射到三个不同的虚拟地址上去了,这三个虚拟地址的区别就是48位地址值中的高9位有区别。而这个区别就是:
Rust
pub const PHYS_OFFSET: usize = 0xFFFF800000000000;
其中48位地址的高9位是等于128,
Rust
1000 0000 0
整个PML4表大致如下图所示:
而整个虚拟地址与物理地址是值对应的,因为PML4是从第0项,PDPT表也是从第0项,对应到的物理地址也是从0地址开始。
Rust
// Page Directory: map to low 1 GiB * 4 space
lea edi, [boot_pd]
lea eax, [boot_pt + (PTE_PRESENT | PTE_WRITE | PTE_GLOBAL)]
mov ecx, 512 * 4 // (of entries in PD) * (number of PD)
write_pd_entry:
mov dword ptr [edi], eax
mov dword ptr [edi + 4], 0
add eax, 0x1000 // +4KiB to next table
add edi, 8
loop write_pd_entry
而在kernel启动过程中,kernel的PML4表中的三项会被保存在全局变量中:
Rust
pub fn init() {
let (page_directory_base, _) = x86_64::registers::control::Cr3::read();
let page_directory_base = page_directory_base.start_address().as_u64() as usize;
// Safety: page_directory_base is read from Cr3, the address is valid.
let p4: &mut [PageTableEntry] = unsafe { table_of::<PageTableEntry>(page_directory_base).unwrap() };
// Cancel mapping in lowest addresses.
p4[0].clear();
let mut map_pte = ALL_MAPPED_PTE.lock();
for (i, p4_i) in p4.iter().enumerate().take(512) {
if p4_i.flags().contains(PageTableFlags::PRESENT) {
map_pte.insert(i, *p4_i);
}
}
}
而每个进程在创建自己的PML4表的时候,会将Kernel全局PML4表复制到进程的PML4表里:
Rust
pub fn new() -> Self {
let mut page_table = PageTable::<PageTableEntry, UserMode>::new(PageTableConfig {
address_width: super::page_table::AddressWidth::Level4,
});
let mapped_pte = crate::arch::mm::ALL_MAPPED_PTE.lock();
for (index, pte) in mapped_pte.iter() {
// Safety: These PTEs are all valid PTEs fetched from the initial page table during memory initialization.
unsafe {
page_table.add_root_mapping(*index, pte);
}
}
Self {
pt: page_table,
areas: BTreeMap::new(),
}
}
因此在kernel中paddr_to_vaddr:
Rust
/// Convert physical address to virtual address using offset, only available inside aster-frame
pub(crate) fn paddr_to_vaddr(pa: usize) -> usize {
pa + PHYS_OFFSET
}
对于Kernel而言,它在每个进程中的地址空间是一致的。kernel也没有自己单独的PML4表了,只会随着进程切换而切换进程的PML4表。但是进程的PML4表的部分会有kernel空间的映射,这样当进程陷入kernel中的时候,还是可以通过虚拟地址去访问物理地址。
至此大致完成Asterinas系统中对于内存的管理的分析了。