初始化内核地址空间
Exercise 5
Fill in the missing code in mem_init() after the call to check_page().
Your code should now pass the check_kern_pgdir() and check_page_installed_pgdir() checks.
错误
kernel panic at kern/pmap.c:787: assertion failed: check_va2pa(pgdir, KERNBASE + i) == i
实际卡在以下语句下,
// check phys mem for (i = 0; i < npages * PGSIZE; i += PGSIZE) assert(check_va2pa(pgdir, KERNBASE + i) == i);
发现其以npages * PGSIZE为检查边界,修改KERNBASE以上内存的映射代码为
boot_map_region(kern_pgdir, KERNBASE, npages * PGSIZE, 0, PTE_W );
又出现了以下错误:
kernel panic at kern/pmap.c:804: assertion failed: pgdir[i] & PTE_P
.
调试发现,页目录表中的页目录项,960-991索引项都通过assert
,错误发生在992项处,即992项无效。(991-960+1)*2^10 = 2^15 = 32768. 刚好等于npages的值。
感觉是npages
与 0xFFFFFFFF-KERNBASE
不一致导致的。打印npages =32768
, (0xFFFFFFFF-KERNBASE)/PGSIZE
向上取整= 65536页。很明显用npages映射的内存远远没有后一种方法多,所以出现第二种错误很正常,但是第一种错误为什么会出现呢?
继续改为出现第一种错误的写法,通过调试发现,在 i=0时就出错了,证明KERNBASE根本就没有映射到物理地址0处。
根本没有道理啊,第二种能映射成功,证明是没有问题的呀。而且之前的两次boot_map_region
都没有问题, 应该是第三次的size= 268435456
过大导致boot_map_region()
出现了Bug。
最初写法,发现用va+size
会出现溢出的情况,比如当前情况,size=268435456=0x10000000
, va=0xF0000000
, 两者相加直接等于0了。 所以当我们用第二种npage*PGSIZE
时能成功初始化一部分映射。
并且,当我们直接给size
传入0xFFFFFFFF-KERNBASE
即等于0xFFF_FFFF
,其与va相加为0xFFFF_FFFF
, 就会出现死循环。因为pva从0xF000_0000
开始增长,最大只能为0xFFFF_F000
,永远不会有大于等于va+size
的时候。
static void
boot_map_region(pde_t *pgdir, uintptr_t va, size_t size, physaddr_t pa, int perm)
{
uintptr_t pva = va;
physaddr_t ppa = pa;
pte_t *pte;
for (; pva < va+size; pva+=PGSIZE, ppa+=PGSIZE) //循环映射
{
pte = pgdir_walk(pgdir, (void *)pva, 1); //查找页表项
if (!pte)
{
return;
}
*pte = PTE_ADDR(ppa) | (perm | PTE_P); //设置页表项
}
}
修改后:
static void
boot_map_region(pde_t *pgdir, uintptr_t va, size_t size, physaddr_t pa, int perm)
{
size_t pgs = size / PGSIZE;
if (size % PGSIZE != 0) {
pgs++;
} //计算总共有多少页
for (int i = 0; i < pgs; i++) {
pte_t *pte = pgdir_walk(pgdir, (void *)va, 1);//获取va对应的PTE的地址
if (pte == NULL) {
panic("boot_map_region(): out of memory\n");
}
*pte = pa | PTE_P | perm; //修改va对应的PTE的值
pa += PGSIZE; //更新pa和va,进行下一轮循环
va += PGSIZE;
}
}
最终代码
要求把pages结构体所在的页面和虚拟地址UPAGES相互映射。这里只要计算出pages结构体的大小,就可以进行映射了。说实话,之前注释有点没看懂。以为要实现虚存对pages指向的物理页的映射。
boot_map_region(kern_pgdir, UPAGES, ROUNDUP((sizeof(struct PageInfo)*npages), PGSIZE), PADDR(pages), PTE_U );
映射内核栈KSTACKTOP
boot_map_region(kern_pgdir, KSTACKTOP-KSTKSIZE, KSTKSIZE, PADDR(bootstack), PTE_W );
映射内核基址KSTACKTOP
uint32_t kern_size = ROUNDUP((0xFFFFFFFF-KERNBASE), PGSIZE); cprintf("size: %d pages:%d\n", kern_size, kern_size/PGSIZE); boot_map_region(kern_pgdir, (uintptr_t) KSTACKTOP, kern_size, (physaddr_t)0, PTE_W );
1.1.4. Questions
- 我们已将内核和用户环境放在同一地址空间中。 为什么用户程序无法读取或写入内核的内存? 哪些特定机制保护内核内存?
A:页表项中有读写保护位,以及PTE_U
区分内核和用户,MMU应该会实现这种保护。
- 此操作系统可以支持的最大物理内存量是多少? 为什么?
A:4GB,因为32位地址线,可以寻址4GB大小。
- 如果我们实际拥有最大的物理内存量,那么管理内存的空间开销是多少? 这个开销是如何分解的?
A:4GB/PASIZE = 2^32/2^12 = 2^20页, 每个页表项4B,
- 页表占用内存2^20x4B = 4M,
- page directory = 4K,
- Pages结构体2^20*(4+2) = 6M
地址空间布局替代方案
我们在JOS中使用的地址空间布局不是唯一的。操作系统可以将内核映射到低线性地址,同时将线性地址空间的上部留给用户进程。但是x86内核一般不采用这种方法,因为x86的向后兼容模式之一,称为虚拟8086模式,在处理器中“硬连线”使用线性地址空间的底部。因此如果内核映射到那里,就无法可以使用。
Challenge感觉都好难啊,占坑!!
总结
整个lab2都在填充mem_init()
这个函数,也就是建立一个二级页表,可分为以下几个步骤。
- 内存检查
读取基本内存大小、扩展内存大小,并根据PGSIZE求出有多少个page,同时打印可用内存大小。
- 创建页目录表
在kernel.ld
链接文件中,声明了end
在bss段的未使用位置的最前端,实现了boot_alloc()
在bss段未使用的最前端进行页目录、页表存储空间的申请。
.bss : {
PROVIDE(edata = .);
*(.bss)
PROVIDE(end = .);
BYTE(0)
}
第一次进入boot_alloc
函数会执行以下代码。
if (!nextfree) {
extern char end[];
nextfree = ROUNDUP((char *) end, PGSIZE);
}
- 创建物理页表记录的数据结构,并对其初始化
记录各个页面的引用计数,将物理内存的分页用链表串接起来,同时记录目前空闲可用的页。
- 建立虚拟内存与物理内存映射关系,即初始化各个页表项
首先要实现通过页目录查到对应的页表,判断页表是否存在,是否需要申请空间存储新的页表。以及页表项的初始化,页的插入,页的查询等等操作。
页表的地址空间也是使用boot_alloc()
函数分配,即都是紧跟在bss段后面。 分别映射UPAGES
、KSTACKTOP
、KERNBASE
三个区域。
geray
在`映射内核基址KSTACKTOP`这一章节中,是否需要将参数`KSTACKTOP`修改为`KERNBASE`?, 因为注释中时说从虚拟地址KERNBASE开始。 但貌似测试写的有点水, 这两种方案都可以通过。