https://os.phil-opp.com/paging-implementation/#allocating-frames x86分页实现全过程解析(rust的x86_64 crate)

main中使用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
    //引导加载程序中获取到物理内存偏移
    let phys_mem_offset = VirtAddr::new(boot_info.physical_memory_offset);
    let mut mapper = unsafe { memory::init(phys_mem_offset) };
    //let mut frame_allocator = memory::EmptyFrameAllocator;
    let mut frame_allocator = unsafe {
        BootInfoFrameAllocator::init(&boot_info.memory_map)
    };

    // map an unused page
    let page = Page::containing_address(VirtAddr::new(0xdeadbeaf000));
    memory::create_example_mapping(page, &mut mapper, &mut frame_allocator);

    // write the string `New!` to the screen through the new mapping
    let page_ptr: *mut u64 = page.start_address().as_mut_ptr();
    unsafe { page_ptr.offset(400).write_volatile(0x_f021_f077_f065_f04e)};

上述代码中phys_mem_offset物理内存偏移本质是虚拟地址VirtAddr。 mapper是OffsetPageTable对象,OffsetPageTable是x86_64 crate目前提供的三种类型之一(都实现了Mapper接口),这里我用前面的phys_mem_offset初始化它

1
2
3
4
pub unsafe fn init(physical_memory_offset: VirtAddr) -> OffsetPageTable<'static> {
    let level_4_table = active_level_4_table(physical_memory_offset);
    OffsetPageTable::new(level_4_table, physical_memory_offset)
}

题外:OffsetPageTable从源码上看是对MappedPageTable的又一层封装,所以相对来讲MappedPageTable更灵活:

1
2
3
The OffsetPageTable type assumes that the complete physical memory is mapped to the virtual address space at some offset. 
The MappedPageTable is a bit more flexible: It only requires that each page table frame is mapped to the virtual address space at a calculable address. 
Finally, the RecursivePageTable type can be used to access page table frames through recursive page tables.

返回的是phys_to_virt,一个页表的虚拟地址指针:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//https://docs.rs/x86_64/0.9.6/src/x86_64/structures/paging/mapper/mapped_page_table.rs.html#574
pub trait PhysToVirt {
    /// Translate the given physical frame to a virtual page table pointer.
    fn phys_to_virt(&self, phys_frame: PhysFrame) -> *mut PageTable;
}

impl<'a> Mapper<Size4KiB> for OffsetPageTable<'a> {


//https://docs.rs/x86_64/0.9.6/src/x86_64/structures/paging/mapper/offset_page_table.rs.html#39
#[derive(Debug)]
struct PhysOffset {
    offset: VirtAddr,
}

impl PhysToVirt for PhysOffset {
    #[inline]
    fn phys_to_virt(&self, frame: PhysFrame) -> *mut PageTable {
        let phys = frame.start_address().as_u64();
        let virt = self.offset + phys;
        virt.as_mut_ptr()
    }
}

Mapper

map_to

文档告诉我们map_to有四个参数:我们要映射的页面、页面应该映射到的帧、页面表项的一组标志和一个帧分配器。需要帧分配器,因为映射给定页可能需要创建其他页表,而这些页表需要未使用的帧作为后备存储。 map_to方法按以下方式创建缺少的页表:

  • 从传递的帧分配器分配未使用的帧。
  • 将帧归零以创建新的空页表。
  • 将较高级别表的条目映射到该帧。
  • 继续下一个表级别。

纵观map_to的实现,前提是已经有虚拟地址对应的页表存在且p1[page.p1_index()]对应的页表项为unused,才会去给对应的虚拟地址在1级页表中分配物理地址(页帧)即添加一个页表项,成功后用flush把这次映射刷进tlb中完成缓存:

1
2
p1[page.p1_index()].set_frame(frame.frame(), flags);
Ok(MapperFlush::new(page))

页帧分配器(frame allocator)

可以参考linux的页框分配器。 帧分配器用于创建新的页表,如果帧分配器为none,那么无法映射新的页表了,只能使用现有页表了,否则就会报错。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
use bootloader::bootinfo::MemoryRegionType;

impl BootInfoFrameAllocator {
    /// Returns an iterator over the usable frames specified in the memory map.
    fn usable_frames(&self) -> impl Iterator<Item = UnusedPhysFrame> {
        // get usable regions from memory map
        let regions = self.memory_map.iter();
        let usable_regions = regions
            .filter(|r| r.region_type == MemoryRegionType::Usable);
        // map each region to its address range
        let addr_ranges = usable_regions
            .map(|r| r.range.start_addr()..r.range.end_addr());
        // transform to an iterator of frame start addresses
        let frame_addresses = addr_ranges.flat_map(|r| r.step_by(4096));
        // create `PhysFrame` types from the start addresses
        let frames = frame_addresses.map(|addr| PhysFrame::containing_address(PhysAddr::new(addr)));
        // we know that the frames are really unused
        frames.map(|f| unsafe { UnusedPhysFrame::new(f) })
    }
}

此函数使用组合迭代器方法将初始内存映射转换为可用物理帧的迭代器。frame allocator最后返回的就是一个可用的叶框物理地址,在map_to中的作用就是某级页表如果未分配(也就是上一级的页表用索引未找到相应的页表项),那么可以用那个索引在上一级页表上创建个页表项用于指向这个新分配的物理页框,也就是完成新建页表。