Jos-lab2
这次lab主要的工作是在JOS 操作系统中实现内存分页管理的功能。除了第一个challenge外其他代码都在 pmap.c 文件中完成。
这次的 lab(challenge除外)可以分为两部分部分:
1、物理页面管理和页表管理。前者强调对机器拥有的物理内存的管理,包括建立对应的数据结构、处理分配和回收动作等。
需要完成以下函数:
- boot_alloc()
- mem_init()
- page_init()
- page_alloc()
- page_free()
2、利用Intel x86 系列处理器的页式地址管理功能,完成(虚拟)线性地址到物理地址的转换,包括建立页目录、页表等。
需要完成以下函数:
- pgdir_walk()
- boot_map_region()
- page_lookup()
- page_remove()
- page_insert()
- boot_map_region_large()
对于上述所涉及的函数(boot_map_region_large除外)都只需要照着文件中的注释写即可,
故不多做解释。
接着上一个lab的内容,并调用 i386_init()函数,而 i386_init()函数在将自己的 BSS 区域清零后,调用 cons_init()函数设置好屏幕显示设备为 cprintf 的运行做好准备后就调用 mem_init()函数进入lab2。
lab中所涉及到的内存的分页机制包含页面管理和页表管理两个部分。对于前一个部分,主要是讨论内核如何在初始物理内存中分配空间和建立合理的数据结构来对物理页面进行管理,而后一个部分主要是讨论如何建立两级页目录和页表,从而在开启 x86 的分页管理后仍然能够进行合理的逻辑地址到物理地址的转换。
页面管理
这个Page是这个lab中很重要的一个单项链表结构,用来管理物理内存。其中pp_link指向链表的下一个单元的地址,pp_ref则表示页表结构中映射到该段物理页的entry数量,通过该结构可以完成这个lab的很多事情。
struct Page {
struct Page *pp_link;
uint16_t pp_ref;
};
在 JOS 中,页面操作主要由 page_alloc()以及page_free()完成。同时,还要进行页面的初始化。由于系统的物理内存由 4KB 大小的很多物理页面组成,同时这些物理页面与页面管理单向链表中的管理结点有一一对应的关系,根据一个物理页的首地址可以计算出它所对应的管理结点的位置,可以用 pages[下标]来索引。在页面管理数据结构初试化的时候,系统会把所有管理结点加入页面管理链表,但是,由于有些物理内存已经被 x86 系统所使用或者预先放了一些系统数据。同时,有些内存已为内核本身所占用,这些内存不能把它们当自由页面分配出去。所以,在建立好页面管理链表后,需要将这些已经被使用的内存对应的页面管理结点从链表中移出。page_alloc()函数被调用后,将取页面管理双向链表中的第一个管理结点,并将该结点返回出去,需要注意的是,由于无法确定得到的物理内存页面是否是“干净”的,如果已经有数据,就会对之后的程序造成影响。所以得到页面管理结点后,要将它所对应的物理页面清零。
页表管理
lab中有提到逻辑地址( Virtual Address)、线性地址(Linear Address)和物理地址(Physical Address)。逻辑地址是指程序在编译连接后,变量名字等的符号地址, 在 JOS 系统中的内核部分, 该地址是以 KERNBASE(默认等于 0xF0000000,实际上可以根据具体的情况加以修改);线性地址是指经过 x86 保护模式的段地址变换后的地址,该变换的过程是 逻辑地址+段首地址;物理地址是指内存存储单元的编址,如 1GB
的内存,它的物理编址是从 0x00000000 到 0x40000000。
关于线性地址的转换,在ics课上已经讲过,就不说了。
关于第二部分的函数,主要就是用于处理对页目录、页表和数据页的关联。这里稍微解释一下:
pte_t *pgdir_walk(pde_t *pgdir, const void *va, int create)
根据输入的虚拟地址va,返回它在二级页表中对应的表项。如果页不存在,根据其中的create参数,决定是否要为这个虚拟地址分配物理页。
static void boot_map_region(pde_t *pgdir, uintptr_t va, size_t size, physaddr_t pa, int perm)
将从va开始的size个byte映射到pa。
struct Page *page_lookup(pde_t *pgdir, void *va, pte_t **pte_store)
根据va,返回对应的页表的页,如果有pte_store,则将对应的pte的位置保存pte_store上。
void page_remove(pde_t *pgdir, void *va)
删除给定va对应的页表,同时也要清除对应的TLB。
int page_insert(pde_t *pgdir, struct Page *pp, void *va, int perm)
这个函数将虚拟地址va映射到pp对应的页表上,如果没有分配就直接映射va,如果页表已分配先判断是否映射va相同的虚拟地址,如果是就只删除对应的TLB,如果不是就直接remove。
部分函数代码
因为代码过多,只贴出其中几个。其余基本代码可以在提交的代码中查看。
关于boot_map_region_large()
uint32_t cr4;
cr4 = rcr4();
cr4 |= CR4_PSE;
lcr4(cr4);
boot_map_region_large(kern_pgdir, KERNBASE, 64, 0, PTE_P | PTE_W);
static void
boot_map_region_large(pde_t *pgdir, uintptr_t va, size_t size, physaddr_t pa, int perm)
{
uint32_t i=0;
while(i<size)
{
kern_pgdir[PDX(va)] = pa | perm | PTE_PS;
va=va+PTSIZE;
pa=pa+PTSIZE;
i++;
}
}
需要用这个boot_map_region_large()替代boot_map_region()。先开启CR4中的PSE位以此使用PDE的PS位。通过分配PDE,替代了二级页表的分配。通过计算可以得出只需要64的PDE表项。
Challenge1(1)
主要是要实现一个showmapping的命令,改命令可以打出指定范围内每个页所对应的权限位
主要实现如下,只需要遍历一下范围地址里在二级页表里的entry并输出权限位即可。
int
showmappings(int argc, char **argv, struct Trapframe *tf)
{
if (argc!=3)
{
cprintf("Usage: showmappings 0xbegin_addr 0xend_addr\n");
return 0;
}
uint32_t beg_addr = atoi(argv[1]);
uint32_t end_addr = atoi(argv[2]);
while(beg_addr <= end_addr)
{
pte_t *pte = pgdir_walk(kern_pgdir, (void *) beg_addr, 1);
if (!pte)
{
panic("out of the memory");
}
else if (*pte & PTE_P)
{
cprintf("page at %x has the permission that PTE_P: %x, PTE_W: %x, PTE_U: %x\n", beg_addr, *pte&PTE_P, *pte&PTE_W, *pte&PTE_U);
}
else
{
cprintf("no that page: %x\n", beg_addr);
}
beg_addr=beg_addr+PGSIZE;
}
return 0;
}
Challenge1(2)
实现对一个地址页表权限位的设置,清零与更改,也不难。
得到一个地址所对应的二级页表后,根据所输入的指令更改页表里的权限位,实现如下:
int
setmapping(int argc, char **argv, struct Trapframe *tf)
{
if (argc!=4)
{
cprintf("Usage: setmapping 0xaddr [0|1|2 : set or clear or change ] [P|W|U]\n");
return 0;
}
uint32_t addr = atoi(argv[1]);
pte_t *pte = pgdir_walk(kern_pgdir, (void *)addr, 1);
cprintf("Before setmapping :PTE_P: %x, PTE_W: %x, PTE_U: %x\n", *pte&PTE_P, *pte&PTE_W, *pte&PTE_U);
uint32_t perm = 0;
if (argv[3][0] == 'P')
{
perm = PTE_P;
}
else if (argv[3][0] == 'W')
{
perm = PTE_W;
}
else
{
perm = PTE_U;
}
if (argv[2][0] == '0')
{
*pte = *pte|perm;
}
else if(argv[2][0]=='1')
{
*pte = *pte & ~perm;
}
else if(argv[2][0]=='2')
{
*pte = *pte^perm;
}
cprintf("After setmapping :PTE_P: %x, PTE_W: %x, PTE_U: %x\n", *pte&PTE_P, *pte&PTE_W, *pte&PTE_U);
return 0;
}
Challenge1(3)
把地址范围内内存里的内容打印出来,并且支持虚拟地址与物理地址。如果是物理地址,转换为虚拟地址即可。
int
mon_dump(int argc, char **argv, struct Trapframe *tf)
{
if (argc !=4)
{
cprintf("Usage: dump [P|V] 0xbegin_addr 0xend_addr\n");
return 0;
}
uint32_t beg_addr = atoi(argv[2]);
uint32_t end_addr = atoi(argv[3]);
if(argv[1][0]=='P')
{
if(PGNUM(beg_addr)>=npages||PGNUM(end_addr)>=npages)
{
panic("Wrong address");
}
beg_addr=(uint32_t)KADDR(((physaddr_t)beg_addr));
end_addr=(uint32_t)KADDR(((physaddr_t)end_addr));
}
void** addr = (void**) beg_addr;
int i=0;
for (; beg_addr < end_addr;beg_addr++)
{
cprintf(" contents of %x is %x\n", beg_addr, addr[i]);
i++;
}
return 0;
}
Challenge2
实现一个称作alloc_page_with_color的函数,返回一个所需要的color的页。这个函数需要遍历page_free_list直到得到所需要的color的页,并返回。
struct Page *alloc_page_with_color(int alloc_flags, int color)
{
if(!page_free_list)
{
return NULL;
}
struct Page* page = page_free_list;
struct Page* result;
while(page)
{
if((((int)page2pa(page))&4) ==color)
{
page->pp_link = 0;
result=page;
page= page->pp_link;
if(alloc_flags & ALLOC_ZERO)
{
memset(page2kva(result), 0, PGSIZE);
}
return result;
}
page=page->pp_link;
}
return NULL;
}