实验环境
Hardware:
Memory: 16G
Processor: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz × 6
OS Type: 64 bit
Disk: 512GB
Software
OS: Ubuntu 18.04 LTS(x86_64)
GCC: gcc 7.5.0 #gcc -v
Make: GNU Make 4.1 #make --verison
GDB: GNU gdb 8.1.0 #gdb --version
这个lab主要包括两部分:
- 为kernel写一个物理内存分配器
- 写一个虚拟内存
准备工作:
运行以下命令以获得最新版本的lab2:
git pull
git checkout -b lab2 origin/lab2
git checkout -b
命令做了两件事情:
- 它在本地创建了叫lab2的branch,根据远端仓库中的origin/lab2分支
- 它将本地lab文件夹中的内容变成lab2分支的内容。
可以使用git checkout branch-name
在不同的分支之间切换,注意在不同的分支之间切换之前要先commit修改内容到当前分支之中,否则会丢失。
现在可以把之前在lab1分支中做的修改merge到当前的lab2分支中:
git merge lab1
有时候git可能会merge失败,主要有两种原因:
- 同一个文件中既有2个人独自编辑的部分也有共同编辑的部分,merge不知道是谁merge谁
- 最新版本的某个文件被删除了,而本地之前的版本还存在这个文件。
解决方法:
- 可以使用
git checkout --ours
保留本地版本 - 可以使用
git checkout --theirs
保留远端版本 - 可以
git rm
删除本地文件再合并
然后再git merge
进行合并。
最前面说了一些提醒事项,重要的有如下的内容:
memlayout.h
里包括了需要实现的虚拟地址空间布局。pmap.h
定义了可以用来跟踪空闲物理内存的PageInfo
结构。kclock.c
和kclock.h
用来控制PC时钟和CMOS RAM的。pmap.c
需要读这些硬件设备来判断有多少物理内存。
特别注意memlayout.h
和pmap.h
文件,这个lab要求使用并理解里面的内容。同时inc/mmu.h
也包括了lab中有用的好多定义。
Part 1: Physical Page Management
这里网站上提到的内容不太多,主要还是需要看代码。网站上说JOS通过页粒度(Page granularity)来管理物理内存,就是把内存分页,通过MMU映射到物理内存,并且用分页机制保护已分配内存。
要写的部分是物理页的分配器。它用一个PageInfo
的链表来跟踪空闲页。这里提到了这个链表不是存在空闲页里面的,和xv6不一样。
Exercise 1 要求补全kern/pmap.c
中以下函数:
boot_alloc()
mem_init()
page_init()
page_alloc()
page_free()
补完之后启动JOS看能否通过check_page_alloc()
函数的测试。
下面我们就来看代码:
首先在memlayout.h
里讲了jos的虚拟地址的分布。这部分后面使用到虚拟内存的时候再说。Lab1中告诉了我们物理内存的分布情况,大致如下:
0x00000000
到0x000A0000
的base memory,可用0x000A0000
到0x000FFFFF
的384KB空间由硬件保留,用作存储BIOS程序等功能,不可用0x00100000
以上的为extended memory,可用
先看一下调用逻辑有个大概印象。在kern/init.c
中首先调用了cons_init()
来初始化控制台设备,然后调用了mem_init()
完成物理内存的初始化。这是需要在lab2中完成的函数。
回到kern/pmap.c
查看需要补全的mem_init()
函数。它首先调用了i386_detect_memory()
。这个函数用于查看机器有多少物理内存。它初始化了两个全局变量:npages
, 机器有多少物理内存页;npages_basemem
, base memory包括多少物理页。
接着这个函数调用了boot_alloc()
函数为内核页目录kern_pgdir
分配了PGSIZE
大小的内存,那么我们先来补全boot_alloc()
.
boot_alloc()
直接来看boot_alloc()
提供的代码。代码解释以注释形式给出。
static void *
boot_alloc(uint32_t n)
{
static char *nextfree; //第一个未使用byte的虚拟地址
char *result; //函数返回值
if (!nextfree) {
extern char end[]; //end是定义在/kern/kernel.ld中定义的符号,位于bss段的末尾,即从内核虚拟地址的末尾开始分配虚拟地址
nextfree = ROUNDUP((char *) end, PGSIZE);
}
//这里要求分配足够n byte大小的内存,然后更新nextfree,并且要注意对齐。
//Your code here:
return NULL;
}
这里直接连续的往下分配即可。这个函数的补全如下:
result = nextfree;
nextfree = ROUNDUP((char*)result + n, PGSIZE);
return result;
继续回到mem_init()
往下看。下面它为kern_pgdir
分配了虚拟地址空间,然后到了需要我们补充的地方,看下面的要求:
- 要求分配一列大小为
npages
的Page Info
数组,保存在pages
变量中,用memset
来初始化这个数组为0.
根据要求补全如下:
pages = (struct PageInfo*) boot_alloc(sizeof(struct PageInfo) * npages);
memset(pages, 0 ,sizeof(struct PageInfo) * npages);
接着往下,我们已经为初始的内核数据结构分配了空间,然后要着手建立未分配物理页的链表,mem_init()
调用了page_init()
来完成这件事。
page_init()
原来给了一段代码,但是这段代码把所有的物理页都设置成了空闲,这显然是不对的。我们来看注释的提示:
- 把物理页0标记成已用, 这样保存了实模式中断描述符表(real-mode IDT)和BIOS结构,后面估计会用到。
- base memory中
[PGSIZE, npages_basemem * PGSIZE)
的部分是未分配的。 - 然后
[IOPHYSMEM, EXTPHYSMEM)
作为IO hole 不能被分配 - 最后扩展内存中有一部分已经被我们使用了,我们需要通过某些方式知道这一点。
我们可以修改给出的循环如下:
void
page_init(void)
{
size_t i;
size_t IO_Hole_start = (size_t)IOPHYSMEM / PGSIZE; //the start of IO hole
size_t IO_Hole_end = (size_t)EXTPHYSMEM / PGSIZE; //the end of IO hole, also the start of the extended memory.
size_t EXT_Used_end = PADDR(boot_alloc(0)) / PGSIZE; //the end of used extended memory
//ATTENTION! here the address returned by boot_alloc is virtual memory address which must be translated to physical address since here we are considering physical pages!
//Mark page 0 as used
pages[0].pp_ref = 1;
pages[0].pp_link = NULL;
for (i = 1; i < npages; i++) {
//Mark IO hole and Used Extended Memory as used.
if (i>= IO_Hole_start && i<EXT_Used_end){
pages[i].pp_ref =