MIT6.828 Lab2 part3 Kernel Address Space

环境

ubuntu 20.04 64位系统

lab2地址:点击我查看lab2

正文

lab2说实话还是挺难的,这也首先归因我对于页式内存了解有点不到位。在实验中走了一些弯路。不过还是很努力地把lab3的内容都做完了。废话不多说,开始干!

下面这段还是翻译下lab2 part3的一些原文。
JOS 将32bit的线性地址空间划分为两个部分。用户使用低地址的部分,内核使用高地址的部分。这两者划分的界限是inc/memlayout.h中的ULIM这个宏,为内核保留了大约256MB左右的虚拟地址空间。(之所以这里说的是大约,是因为还有一部分是内核的栈,还有一个Memory-mapped I/O,这一块估计是给IO用的)。这也就是解释了在lab1中提到的,为什么内核占据了虚拟地址的高地址部分,因为我们要留给user process足够的空间。
在inc/memlayou.h当中有内存的示意图,代码里面的那个有点难懂,我自己做一个在补充到这里。

memory

这里有更好看的图

几个关键的位置我已经用红色标出来了,UTOP下面的没画,位置不够了。不够对于本次lab已经足够了。
对于大于ULIM的地址,用户程序不能对这块内存执行任何操作,读写都不行,内核对这一块内存可读可写。对于内存从UTOP-ULIM,这一块内存对于kernel和user是read-only的,不能修改。低于UTOP的地址是给user process用的。绿色的是我们本次实验需要关注的,page table不需要,只是感觉挺重要的。细说一下page table。

Exercise 5

补充在mem_init()在chech_page()之后缺失的代码

接下来你要设置的是大于UTOP的地址,内存的结构已经在inc/memlayout.h当中了,你要用lab2当中的所实现的代码来完成这些工作,这里说明我们要操作的都是内核能访问但是用户不能访问的地址(除了第一题)。
第一道题

// Map 'pages' read-only by the user at linear address UPAGES
// Permissions:
// - the new image at UPAGES -- kernel R, user R
// (ie. perm = PTE_U | PTE_P)
// - pages itself -- kernel RW, user NONE

将pages的地址映射到UPAGES起始的虚拟地址,这一块对于用户和kernel来说都是read-only的。pages是一个PageInfo结构体数组存。在part2的当中有一个函数叫做boot_map_region它能够将一块虚拟地址映射到一块物理地址。 看一下inc/memlayout.h中的地质结构,可以看到pages所占的大小是PTSIZE(一个page table可以索引的地址范围,1024*4kb = 4MB)。这块内存的权限是PTE_U | PTE_P(P:是否在内存当中,U:If U is set,then the page accessed by all)。答案很明了了:

boot_map_region(kern_pgdir,UPAGES,PTSIZE,PADDR(pages),PTE_U | PTE_P);

将UPAGES起始的PTSIZE大小的内存映射到物理地址为:PADDR(pages)的区域,权限位PTE_U | PTE_P(只读)

PS:
lab2页面说[UTOP-ULIM]这一块内存都是只读的,我就想既然都是read only那么kernel怎么去修改pages这个结构,怎么去修改页表呢?后来想想相通了(也不知道理解的对不对)。首先我们要注意的是pages这些数据结构,实际上是在内核的空间内的(毕竟他们都是代码里面的变量,一开始就被加载到内存去了)。内核可以对自己的变量操作,这是很显而易见的。然后我们将UPAGES映射到pgaes的物理地址去。并且设置为read only。对于kernel和user来说,当他们使用UPGAES来访问的时候,都是read only的。但是内核可以在自己的代码里面自由地操作pages。(因为我们待会会对整个kernel映射到0-256MB这个地方,也就是说有UPAGES可以访问pages,内核自身的pages这个指针也可以访问Pages。)懂了吗老铁?下面是用qemu-monitor 打印的结果(后期在补充一个qemu monitor 的使用):

结果

pages的虚拟地址

可以看到内存中的数据是相同的。这也说明我们对pages映射成功了!

同样的,页表也是类似。首先先明白一个很重要的事实,就是page directory也是也是page table。想一下什么是page table? 一个page table中的一个entry都对应着一个页的物理地址。page directory一个entry对应的一个page tbale, page table也是一个页。那我们很自然也可以将page directory看作是一个page table。下面来看关键的几行代码:

kern_pgdir = (pde_t*) boot_alloc(PGSIZE);
memset(kern_pgdir,0,PGSIZE);
kern_pgdir[PDX(UVPT)] = PADDR(kern_pgdir) | PTE_U | PTE_P;

首先先申请了一个PGSIZE大下的内存用于存放page directory。然后将这块内存内容都设置为0。然后获得虚拟地址UVPT对应的pgae directory index,把对应的page directory entry设置为PADDR(kern_pgdir) | PTE_U | PTE_P;。也就是说PDX(UVPT)指向的是page directory自身。关键记住把page directory当作一个page table一个Page table可以管理4MB的内存,当我们访问UVPT到UVPT+PTSZIE这里面的内存的时候,都是访问由Page directory它管理的内存(下图是一个例子)。但是page directory所管理的内存是所有的page table.所以UVPT+PTSIZE可以看作是page table的内存,懂了没有 铁汁!访问UVPT到UVPT+PTSZIE就是访问Page table!!这一段理解了好久才想明白,虚拟内存还有一个很重要的特点就是,数据在虚拟内存上表现是连续的,但是在实际的物理内存当中可能是非连续的也可能是连续的,取决于页被映射到了哪里。

直接访问UVPT

如果我们直接访问UVPT:0xrf40_0000这个地址。我们来过一下这个过程:

  1. 首先计算得到PDE index=957,然后发现里面的地址就是kern_dir。
  2. 然后page directory被作为Page table。PTE index =0,就取得第一个页的的地址 page addr。
  3. 最后加上offset,形成了真正的地址。
    其他的地址过程相类似,各位可以试着试一下。相比能理解把page directoy当做page table的意思。

上述的分析是我个人的理解,我没有通过debug去认真的试验过!!

第二道题

// Use the physical memory that 'bootstack' refers to as the kernel
// stack. The kernel stack grows down from virtual address KSTACKTOP.
// We consider the entire range from [KSTACKTOP-PTSIZE, KSTACKTOP)
// to be the kernel stack, but break this into two pieces:
// * [KSTACKTOP-KSTKSIZE, KSTACKTOP) -- backed by physical memory
// * [KSTACKTOP-PTSIZE, KSTACKTOP-KSTKSIZE) -- not backed; so if
// the kernel overflows its stack, it will fault rather than
// overwrite memory. Known as a "guard page".
// Permissions: kernel RW, user NONE

这一道题完成的是将内核栈映射到物理地址。看一下上面内存的结构图,内核栈的总大小是[KSTACKTOP-PTSIZE, KSTACKTOP)。但是上面题目说,这一块栈的内存要分为两块大小. [KSTACKTOP-KSTKSIZE, KSTACKTOP) 这一块地方是需要被映射的,[KSTACKTOP-PTSIZE, KSTACKTOP-KSTKSIZE) 这一块地方不需要被映射。也就是说当我们的栈超过了[KSTACKTOP-KSTKSIZE, KSTACKTOP)会报栈溢出了,但是溢出的数据会到[KSTACKTOP-PTSIZE, KSTACKTOP-KSTKSIZE) 这里。并不会消失。上面一道题明白后,这道题就简单了。注意我们在内核中栈的地址是在bootstack,它此时也是虚拟地址,所以首先要将它转为物理地址。然后在映射,下面是答案:

boot_map_region(kern_pgdir,KSTACKTOP-KSTKSIZE,KSTKSIZE,PADDR(bootstack),PTE_W | PTE_P);

从KSTACKTOP-KSTKSIZE映射一个KSTKSIZE大小的内存到PADDR(bootstack)这里去,权限是PTE_W | PTE_P(可读写,并且页都在内存当中),PTE_U没有设置,所以只有内核可以访问它。

第三题

// Map all of physical memory at KERNBASE.
// Ie. the VA range [KERNBASE, 2^32) should map to
// the PA range [0, 2^32 - KERNBASE)
// We might not have 2^32 - KERNBASE bytes of physical memory, but
// we just set up the mapping anyway.
// Permissions: kernel RW, user NONE

将所有的内核地址映射到[0, 2^32 - KERNBASE),这个真的太简单了,没什么好说的,直接上答案。内核占据的大小是256MB,知道这点就好啦

boot_map_region(kern_pgdir,KERNBASE,0xffffffff-KERNBASE,0,PTE_W | PTE_P);

question

  1. 到目前为止,page directory当中那些已经被填充了?他们又分别指向哪里呢?(logically ,虽然page directory当中应该存放的是物理地址,但是题目要求我们是写虚拟地址)


    page directory

    当我们访问logical里面的地址的时候,在page directory当中会映射到phyiscal当中去。

  2. 为什么在同一个地址空间当中,用户程序不能访问内核的内存的呢?
    这个问题比较简单了,因为我们对内核的内存页设置pte_w,但没有设置PTE_U。所以有些内存只能内核自己访问,而用户无法访问。
  3. 该操作系统可以管理的内存最大值是多少?
    每一个页都以可以对应的PageInfo,这个结构的有8字节。8字节计算方法:指针+short=6字节,但是gcc会在后面追加两个2字节的空数据,这样就在32bit上的电脑完成了字节对齐。看一下内存结构图,pages这个数据结构分配的大小是一个PTSIZE。4MB/8 bit = 512K个,也就是说我们的系统可以管理512K个物理页。512K*4KB=2GB。所以这个系统总共可以管理2GB的内存。
  4. 管理内存的开销是多少,如果我们使用了所有的虚拟内存(在32bit的机器下是4GB),那么需要多少内存?
    回想一下,管理内存所使用的主要就是page table+page directory+PageInfo。逐个计算他们的大小,page directory很简单,page directory:4KB。 每个page directory entry又对应一个page table, page table =4KB。 page table有1024个,所以page table:1024 * 4KB=4MB。 一个page对应一个PageInfo,所以4GB对应1M个page。1M * 8bit=8mb。
    总共需要的内存: 4kb+ 8mb+4mb=12mb+4kb
  5. 重新回顾下kern/entry.S 和 kern/entrypgdir.c。当我们开启Paging的时候,此时EIP还是小于1MB的内存空间内。什么时候我们过渡到KERNBASE上面的内存去呢?为什么能够在打开页表的时候仍然运行在low EIP?我们什么时候运行在大于在KERNBASE?为什么这个过渡十分重要?
  • 何时过渡到KERNBASE上面的内存去?
    mov $relocated, %eax
    jmp *%eax

主要是这两句,在开启paging后,$relocated这个标号对应的地址已经是大于KERNBASE了,然后jmp就跳转到大于KERNBASE的内容去了。也就是relocated标号对应的指令。

  • 为什么在开启paging后,仍然能在low EIP的地方运行指令?
    一开始没理解这里的意思,后面明白了,在上面jmp还没跳转前。此时的指令还在内存的低端,但是因为标号的地址是和链接的地址有关的,$relocated已经是大于KERNBASE的地址了。如下图所示:


    image

可以看到$relocated对应地址是0xf010_002f。不过呢此时,mov和jmp都还在0x10020这样的低地址处。回想下,我们已经通过 改变cr0寄存器开启了paging。为什么此时还可以运行?讲道理原来地址已经不是简单的物理地址了。答案就是这两条语句:

    movl    $(RELOC(entry_pgdir)), %eax
    movl    %eax, %cr3

我们先使用entry_pgdir这个地址作为page directory的地址。具体细节看entrypgdir.c中的注释。之所以能继续运行,是因为我们将虚拟地址的[0, 4MB) 映射到了物理地址[0, 4MB)。注意这里没有初始化所有page table,只初始化了第一个page table。所以jmp和mov指令前面的地址虽然是虚拟地址,但是被映射到了相同的物理地址上,所以原来的代码还是可以运行的。还有我们还将[KERNBASE, KERNBASE+4MB) 也映射到了物理地址的[0, 4MB)。当我们正式进入内核的代码,比如说在运行mem_init()的时候,实际上的物理地址还是在物理地址[0, 4MB)这一块内,当我们把page_init()等这些函数实现以后,就可以实现真正的页式内存了。

  • 什么时候运行在大于KERNBASE的内存呢?
大于KERNBASE

答案也很明显了,jmp指令跳转后就运行在大于KERBASE的地址了,虽然实际上映射的地址还是在物理地址的[0, 4MB)。看上面的截图,左边的地址已经在0xf01002f了。

  • 为什么这个过渡很重要?
    因为我们此时还没有设置能够管理页式内存的函数,所以我们只能先稍微将一小部分虚拟内存映射。在这一小部分内存里面实现管理内存的函数。

PS

  1. 内核的链接地址是在kern/kernel.ld中写的。

你可能感兴趣的:(MIT6.828 Lab2 part3 Kernel Address Space)