lab2
探测物理内存分布和大小的方法
-
操作系统需要知道了解整个计算机系统中的物理内存如何分布的,哪些被可用,哪些不可用。这是因为我们印象中的内存是一个从0开始的大数组,但是实际上内存是由很多ram,rom构成的,所以os要知道什么地址开始是ram(具体可以去查实模式内存布局)。其基本方法是通过BIOS中断调用来帮助完成的。
bootasm.S中新增了一段代码,使用BIOS中断检测物理内存总大小。对比lab1中的bootasm.S和lab2(用的meld)
其实就是多了probe_memory处到finish_probe处的代码部分
-
这段代码就是用了INT 15h BIOS中断获取内存的。15号中断大概是
向es:di:(指向保存地址范围描述符结构的缓冲区)写入信息。(因为15中断是以系统内存映射地址描述符的格式写入的,所以缓冲区必需是以系统内存映射地址描述符所描述的) -
BIOS通过系统内存映射地址描述符(Address Range Descriptor)格式来表示系统物理内存布局,其具体表示如下:
Offset Size Description 00h 8字节 base address #系统内存块基地址 08h 8字节 length in bytes #系统内存大小 10h 4字节 type of address range #内存类型 -
ucore 也构造了一个结构体来映射系统内存映射地址描述符
struct e820map { int nr_map; //map的数量 struct { long long addr; //对应base address long long size; //对应length in bytes long type; //表示内存状态 1 memory, available to OS 2reserved, not available (e.g. system ROM, memory-mapped device) } map[E820MAX]; };到此为止,我们就可以大概知道probe_memory处到finish_probe处的代码功能:向es:di指向的缓冲区(820map)写入内存信息
INT15h BIOS中断的详细调用参数:
eax:e820h:INT 15的中断调用参数; edx:534D4150h (即4个ASCII字符“SMAP”) ,这只是一个签名而已; ebx:如果是第一次调用或内存区域扫描完毕,则为0。 如果不是,则存放上次调用之后的计数值; ecx:保存地址范围描述符的内存大小,应该大于等于20字节; es:di:指向保存地址范围描述符结构的缓冲区,BIOS把信息写入这个结构的起始地址。probe_memory: movl $0, 0x8000 # e820map,就是缓冲区的开始 就是nr_map 初始化为0 xorl %ebx, %ebx # 将ebx清0,这是位操作,ebx作为中断的参数,表示第一次 movw $0x8004, %di # di指向map数组首地址 start_probe: movl $0xE820, %eax # INT 15的中断调用参数; movl $20, %ecx # 保存地址范围描述符的内存大小,应该大于等于20字节; movl $SMAP, %edx # 534D4150h (即4个ASCII字符“SMAP”) ,这只是一个签名而已; int $0x15 # 调用bios中断 jnc cont # 如果该中断执行失败,则CF标志位会置1,此时要通知UCore出错 movw $12345, 0x8000 # 向结构e820map中的成员nr_map中写入特殊信息,报告当前错误 jmp finish_probe # 结束 cont: addw $20, %di # 一个map数组的大小 incl 0x8000 # nr_map++ cmpl $0, %ebx # 是否是内存区域扫描完毕 jnz start_probe #没扫描完,继续扫描 finish_probe:刚开始时我对这个0x8000感到十分困惑,到处找他是个什么东西。后来看网上说0x8000是缓冲区也就是e820map的地址
我就在想为什么?后来我发现是我把因果倒置了,因为在这个boot时期哪来的的e820map结构,汇编语言只是把数据按内存映射地址描述符的格式放在内存0x8000上,把0x8000作为缓冲区仅此而已。什么e820map是我们后面要用到时才出现的结构,用来看物理内存。所以是先由0x8000后有e820map(这仅是我个人的理解而已)

- 在0x8000地址处保存了从BIOS中获得的内存分布信息,此信息按照structe820map的设置来进行填充。这部分信息将在bootloader启动ucore后,由ucore的page_init函数来根
- 据structe820map的memmap(定义了起始地址为0x8000)来完成对整个机器中的物理内存的总体管理。

ucore的地址空间布局
-
在刚开始上电时,寄存器被初始化,指向了bios( 0x000F0000),之后bios把bootloader放在0x00007C00 开始执行
-
在uCore中,CPU先在bootasm.S(实模式)中通过调用BIOS中断,将物理内存的相关描述符写入特定位置
0x8000,然后读入kernel的elf文件至物理地址0x10000、虚拟地址0xC0000000。这个图是我看到别人的博客很好借来的,如果侵删
博客地址:
[]: kiprey.github.io/2020/08/uCo…
-
顺便说一下,为什么elf header与它的section为什么在当前的地址,
-
elf header地址是在bootmain中通过readseg读到0x10000
-
而data section是通过programmer header table 加载到这里的(因为已经打开了a20,可以寻到4gb了)
实验执行流程概述
-
首先在bootloader中已经完成了物理内存的探测,之后再通过kernel_init中调用的pmm_init函数完成基于bootloader探测出的物理内存情况进行物理内存管理初始化工作。之后bootloader不像lab1那样,直接调用kern_init函数,而是先调用位于lab2/kern/init/entry.S中的kern_entry函数。
-
这个时候我就很好奇,系统是怎么实现的?后来在kernel.ld中看到显示的entry是kern_entry说明现在控制权由bootloader到了ucore中的kern_entry手里了
-
可是kern_entry作为ucore的代码,被kernel.ld文件放在了高地址,可是我们现在还是平坦寻址,会出错,所以构建了一个临时映射
-
# labcodes/lab2/kern/init/entry.S kern_entry: # load pa of boot pgdir movl $REALLOC(__boot_pgdir), %eax movl %eax, %cr3 #将页目录表的起始地址存入CR3寄存器中; # enable paging movl %cr0, %eax orl $(CR0_PE | CR0_PG | CR0_AM | CR0_WP | CR0_NE | CR0_TS | CR0_EM | CR0_MP), %eax andl $~(CR0_TS | CR0_EM), %eax movl %eax, %cr0 #把cr0中的CR0_PG标志位设置上,开启分页 # update eip # now, eip = 0x1xxxxx leal next, %eax # set eip = KERNBASE + 0x1xxxxx jmp *%eax #将eip的地址修改为虚拟地址 next: # unmap va 0 ~ 4M, it is temporary mapping xorl %eax, %eax # 将__boot_pgdir的第一个页目录项清零,取消0~4M虚地址的映射 movl %eax, __boot_pgdir # 设置C的内核栈 # set ebp, esp movl $0x0, %ebp # the kernel stack region is from bootstack -- bootstacktop, # the kernel stack size is KSTACKSIZE (8KB)defined in memlayout.h movl $bootstacktop, %esp # now kernel stack is ready , call the first C function # 调用init.c中的kern_init总控函数 call kern_init # .....省略剩余代码 # kernel builtin pgdir # an initial page directory (Page Directory Table, PDT) # These page directory table and page table can be reused! .section .data.pgdir .align PGSIZE __boot_pgdir: .globl __boot_pgdir # map va 0 ~ 4M to pa 0 ~ 4M (temporary) .long REALLOC(__boot_pt1) + (PTE_P | PTE_U | PTE_W) .space (KERNBASE >> PGSHIFT >> 10 << 2) - (. - __boot_pgdir) # pad to PDE of KERNBASE # map va KERNBASE + (0 ~ 4M) to pa 0 ~ 4M .long REALLOC(__boot_pt1) + (PTE_P | PTE_U | PTE_W) .space PGSIZE - (. - __boot_pgdir) # pad to PGSIZE #将虚拟地址0~4M和虚拟地址kernelbase~kernel+4M映射到物理0~4M .set i, 0 # __boot_pt1是一个存在1024个32位long数据的数组,当将其作为页表时其中每一项都代表着一个物理地址映射项 __boot_pt1: .rept 1024 .long i * PGSIZE + (PTE_P | PTE_W) .set i, i + 1 .endr
采用了一个将kernel base开始的4M虚拟地址一一映射到物理内存的临时映射。(这也解释为什么要把虚拟0~4M和kernel base ~kernel+4M映射到同一个地方?因为在开始时,虚拟地址和物理地址就是一一映射的)
最终,离开这个阶段时,虚拟地址、线性地址以及物理地址之间的映射关系为:
lab2 stage 2: virt addr = linear addr = phy addr + 0xC0000000 # 线性地址在0~4MB之内三者的映射关系
-
到这一步,来梳理一下我们干了什么:探测了物理内存并开启了分页机制。那么理所当然的我们现在开始要对物理内存进行分页了。于是便引出我们物理页的数据结构page了
struct Page { int ref; // 当前页被引用的次数,与内存共享有关 uint32_t flags; // 标志位的集合,与eflags寄存器类似 unsigned int property; // 空闲的连续page数量。这个成员只会用在连续空闲page中的第一个page list_entry_t page_link; // 两个分别指向上一个和下一个非连续空闲页的指针。 }; -
所有page都存放在ucore section的后面也就是上文内存分布中的vpt页表。那么为了有效地管理这些小连续内存空闲块。所有的连续内存空闲块可用一个双向链表管理起来,便于分配和释放,为此定义了一个free_area_t数据结构,包含了一个list_entry结构的双向链表指针和记录当前空闲页的个数的无符号整型变量nr_free。其中的链表指针指向了空闲的物理页。
typedef struct { list_entry_t free_list; // the list header unsigned int nr_free; // # of free pages in this free list } free_area_t; -
为了这些页进行管理,我们又定义了一个pmm_manager数据结构
struct pmm_manager { const char *name; //物理内存页管理器的名字 void (*init)(void); //初始化内存管理器 void (*init_memmap)(struct Page *base, size_t n); //初始化管理空闲内存页的数据结构(依据物理内存) struct Page *(*alloc_pages)(size_t n); //分配n个物理内存页 void (*free_pages)(struct Page *base, size_t n); //释放n个物理内存页 size_t (*nr_free_pages)(void); //返回当前剩余的空闲页数 void (*check)(void); //用于检测分配/释放实现是否正确的辅助函数 };经过default_alloc_page(3)之后的内存(对应上文我画的图)
static struct Page * default_alloc_pages(size_t n) { assert(n > 0); //断言判断 n > 0 if (n > nr_free) { return NULL; //要求页数大于空闲页 } struct Page *page = NULL; list_entry_t *le = &free_list; while ((le = list_next(le)) != &free_list) { struct Page *p = le2page(le, page_link); //通过le2page宏实现从free_list指针变成page(块)指针 if (p->property >= n) { page = p; break; } } if (page != NULL) { list_del(&(page->page_link)); //在链表中删除原来指针 if (page->property > n) { struct Page *p = page + n; p->property = page->property - n; list_add(&free_list, &(p->page_link)); //如果要求数小于分给它的页的数量,就做一次切割,并修改新生成的页(块)的property数并把它加入链表 } nr_free -= n; ClearPageProperty(page); } return page; //返回页指针 } -
struct Page *p = le2page(le, page_link); #define le2page(le, member) \ to_struct((le), struct Page, member) #define offsetof(type, member) \ ((size_t)(&((type *)0)->member)) #define to_struct(ptr, type, member) \ ((type *)((char *)(ptr) - offsetof(type, member))) -
offsetof这个宏就是求member(就是page_link)在page中的offset
因为&type *a->member就是求member的地址,可是如果a的地址是0,那么member的地址就变成member在page中的offset(开动你聪明的小脑筋想一想)
offset =& member-&a
如果&a = 0
offset = &member
这里采用了一个利用gcc编译器技术的技巧,即先求得数据结构的成员变量在本宿主数据结构中的偏移量,然后根据成员变量的地址反过来得出属主数据结构的变量的地址。
因为我们手里只有le(它就是page_link)
于是page_link的地址减page_link的offset就得到了page的首地址,在进行地址转换,不就得到page指针了
练习1:实现 first-fit 连续物理内存分配算法
- 到这里是不是感觉都清楚了。要修改物理内存分配算法说到底就是修改pmm_manager
而first-fit都知道
1.空闲分区由地址从小到大排序
2.找到第一个比它大的,分配给它,如果有剩余,就做一次切割
3.归还时,注意合并
- 就是按照这几点修改pmm_manager
1.发现default_init_memmap中用了list_add(&free_list, &(base->page_link));而list_add中调用list_add_after函数,会把新加的页放在free_list后面就是倒置了(地址从大到小)
所以要改为list_add_before(&free_list, &(base->page_link));
2.在default_alloc_pages做切割时也就是 list_add(&free_list, &(p->page_link)); 这一行时,并没有新的指针放在原来的指针的后面,而是放在free_list后面
所以要改为
if (page != NULL) {
if (page->property > n) {
struct Page *p = page + n;
p->property = page->property - n;
SetPageProperty(p); //刚开始我就是完了这件事情
list_add_after(&(page->page_link), &(p->page_link));
}
list_del(&(page->page_link));
nr_free -= n;
ClearPageProperty(page);
}
return page;
3.default_free_pages中默认会在函数末尾处,将待释放的页头插入至链表的第一个节点。
list_add(&free_list, &(base->page_link));
所以我们要该把他放回到原来位置。可是我们怎么找到原来位置,毕竟原来的指针已经被删除了。
但我们可以在链表中找地址大于base + base->property的第一个指针,把它放在指针前面。(因为地址是从小到大排的)
图是承接上文的
le = list_next(&free_list);
while (le != &free_list) {
p = le2page(le, page_link);
if (base + base->property <= p) {
assert(base + base->property != p);
break;
}
le = list_next(le);
}
list_add_before(le, &(base->page_link));
练习2:实现寻找虚拟地址对应的页表项
到现在为止完成了物理地址的探测,实现了对物理地址的分页,并在ucore中开启了分页,也利用pmm_manager和page数据结构实现了对物理内存的管理。并且构建了在kernel base ~ kernel base +4M到0~4M的临时映射。
接下来就是实现虚拟页和物理页帧的地址映射,就是填充页目录表和页表
其大致流程如下:
-
先通过alloc_page获得一个空闲物理页,用于页目录表;
-
调用boot_map_segment函数建立一一映射关系,具体处理过程以页为单位进行设置,
boot_pgdir[PDX(VPT)] = PADDR(boot_pgdir) | PTE_P | PTE_W; //页目录表 boot_map_segment(boot_pgdir, KERNBASE, KMEMSIZE, 0, PTE_W); //以页为单位建立映射关系static void boot_map_segment(pde_t *pgdir, uintptr_t la, size_t size, uintptr_t pa, uint32_t perm) { assert(PGOFF(la) == PGOFF(pa)); size_t n = ROUNDUP(size + PGOFF(la), PGSIZE) / PGSIZE; la = ROUNDDOWN(la, PGSIZE); pa = ROUNDDOWN(pa, PGSIZE); for (; n > 0; n --, la += PGSIZE, pa += PGSIZE) { pte_t *ptep = get_pte(pgdir, la, 1); assert(ptep != NULL); *ptep = pa | PTE_P | perm; } }
建立映射关系说到底就是根据la在页表中找到页表项pte,再把pa映射到该页上。get_pte(pgdir, la, 1);就是在pgdir页表(二级)上根据la找到页表项。(其实刚开始我也有点晕,后来我认为不管怎样我们只要给页就行了,因为有关页目录表是我们自己在get_pte的事情,映射还是关于虚拟页和物理页帧的映射)
有没有感觉很奇怪为什么*ptep = pa | PTE_P 就可以设置页表项。 是因为页表项 前面是地址 后面是状态位。而上面一行代码采用了bitset 就是前面是地址,后面(PTE_P)是状态位
boot_map_segment就实现了kernel的映射
get_pte的规则:
- 计算
la1对应的 PDE 地址。 - 若该 PDE 不存在(PTE 所在的页表不存在)且
create为 不为 0 ,创建页表并设置 PTE。 - 若该 PDE 不存在且
create为 0 ,返回NULL。 - 若该 PDE 存在,直接返回 PTE 虚拟地址。
pde_t *pte = &pgdir[PDX(la)];
if(!(*pte &PTE_P)){
struct Page * page;
if(!create || (page = alloc_page())== NULL)
return NULL;
set_page_ref(page,1);
uintptr_t pa = page2pa(page);
memset(KADDR(pa),0,PGSIZE);
*pte = pa | PTE_U | PTE_W | PTE_P;
}
return &((pte_t *)KADDR(PDE_ADDR(*pte)))[PTX(la)];
有一个要点就是pde_t 指向的pde 它同时是一个pte_t,所以它的[PTX(la)]代表着页
la的布局
但其实就算到了现在我们也只是完成了虚拟地址和pa的映射,但是虚拟地址只有映射上了物理页才可以正常的读写,所以还要介绍两个函数page_insert函数将物理页映射在了页表上,取消映射由page_remove来做,
这个图是通过uderstand生成的
练习3:释放某虚地址所在的页并取消对应二级页表项的映射
其实就是完成page_remove_pte函数
1.当释放一个包含某虚地址的物理内存页时,需要让对应此物理内存页的管理数据结构Page的ref减1;
2.如何物理页空闲,就要释放物理页
3.还需把表示虚地址与物理地址对应关系的二级页表项清除。
4.刷新TLB内的数据
if(*ptep & PTE_P){
struct Page *page = pte2page(*ptep);
if(page_ref_dec(page) == 0)
free_page(page);
*ptep = 0;
tlb_invalidate(pgdir,la);
}
自映射
当页目录与页表建立完成后,如果需要按虚拟地址的地址顺序显示整个页目录表和页表的内容,则要查找页目录表的页目录表项内容,并根据页目录表项内容找到页表的物理地址,再转换成对应的虚地址,然后访问页表的虚地址,搜索整个页表的每个页目录项。这样的过程比较繁琐,而自映射可以改善这个过程
自映射的关键就是
- 把所有的页表(4KB * 1024个)放到连续的4MB 虚拟地址 空间中,并且要求这段空间4MB对齐,这样,就会有一张虚拟页的内容与页目录的内容完全相同。
- 页目录表中存在一个页目录条目,该条目内含的物理地址就是页目录表本身的物理地址。
代码:
boot_pgdir[PDX(VPT)] = PADDR(boot_pgdir) | PTE_P | PTE_W;
这个图是我看到别人的博客很好借来的,如果侵删
博客地址:
[]: kiprey.github.io/2020/08/uCo…
结果:
make grade:
make qemu:
至于Challenge
就是buddy system和slub算法,等我有时间再写吧,主要是想推一下进度