【xv6学习之Lab 2】Memory Management

               

Part 1: Physical Page Management



内存分布如下:



注意到kernel结束之后就是free memory了,而在free memory的最开始存放的是pgdir,这块内存同样由boot_alloc申请



实验开始之前,我们先搞懂几个常识:

1、首先是 i386 的页面大小问题。虽然在很多操作系统书讲到页式地址管理的时候总会说页面大小是可以根据系统的安排而调整的,一般是 1KB 的整数倍,如 1KB,2KB,4KB 或者 8KB 等。但是,在 32 位的 i386 上,页面的大小是固定的 4KB!当然,在 Extended Paging 模式下(在 Pentium 以后的处理器中进行了支持),为了寻址更大的空间,页面的大小甚至可以为 4MB,虽然这种模式在本实验以及后续实验中应该不会碰到。

2、在页面管理上,系统必须管理所有的物理内存空间。对于已经存在在内核中的一些实模式数据(如 Real Mode IDT)和系统区域(如 BIOS 数据,显存区域等),系统不能把它们当作空闲内存分配出去给其他进程使用;而对于内核本身所在的区域,当然也不能把它当做空闲区分配出去给其他进程使用,而且同时,还需要建立起合适的映射关系,达到将逻辑地址转换到物理地址的目标(始终记住:内核程序的逻辑地址是从 KERNBASE=0xF0000000 开始的!)。在确定了页面大小和分页的范围后,物理内存里应该分多少个页面就很清楚了:应该拿物理内存的大小去整除 4KB,即 页面的数目(npages) = 物理内存的大小>>12


注意通读相关代码及comment


实验首先是要填写boot_alloc():

           kern/pmap.c


nextfree为静态字符指针,未初始化,存在.bss段,故初始值为0。

首次执行会进入if,并得到一个不小于 'end' 的地址,也即第一个页的首地址。

这里,很巧妙的利用了局部静态变量nextfree没有显式的赋值初始化的时候,会默认初始化为0并且只初始化一次。

这两个特点都利用的很好。如果nextfree是第一次使用,就进入if判断语句,如果之前进入过if判断语句了,再次调用boot_alloc() 的时候就不需要再进入if语句了。

要注意的是:页表4K ( PGSIZE ) 对齐。

kern/pmap.c



接着填mem_init():

kern/pmap.c



然后是page_init():

要填写这段代码,可以参见page_init()函数里的注释,得到如下物理内存:


这部分地址映射到虚拟地址只需要加上 KERNBASE(0xF0000000) 即可。注意当前代码执行过程中,地址已经使用的的是虚拟地址。

我们知道 Boot Sector 的代码从 0x7C00 开始,而 ELF header 的代码从 0x10000开始,这两部分代码其实都在当前 base memory 的 free space 中,如今把这两部分都忽略掉将其看作是空闲区域,其原因就是那两部分代码的使命已经达成,代码可以被覆盖以备他用(记得有注释提到过)。

kern/entrypgdir.c


kern/pmap.c


代码实现如下:



开始对这样的物理地址分布还心存疑虑,后来看到 /kern/pmap.h 里的page2pa()函数与pa2page()函数后,我就很坚定了。


这两个函数是 pages[i] 与 i页面物理首地址相互转换的函数。由page2pa()函数我们可以清楚看出分页机制即是将连续的物理内存(从0x00000000开始往后)分成多个页面大小的块,所以在得知 pages[] 变量的下标后可以很容易转换得到对应页面块的物理地址。


下来是page_alloc():

page_alloc()函数的实现,就是把当前free list中的空闲页释放一个,然后更新page_free_list,让ta指向下一个空闲页即可。

kern/pmap.c


需要注意的地方是要让当前被分配的页面的 pp_link 变为 NULL,因为上面的注释。


其次是page_free()函数:


做到此处时才发现一个错误。刚开始我的代码是 pp.pp_link,这里由于pp是指针,所以在使用其内在变量时要使用 -> 符号,而不能使用 . 引用符。



Part 2: Virtual Memory


都这么说了,好好看看吧。详细的逻辑地址,线性地址,物理地址的区别戳:内存管理


Virtual, Linear, and Physical Addresses


A C pointer is the "offset" component of the virtual address. In boot/boot.S, we installed a Global Descriptor Table (GDT) that effectively disabled segment translation by setting all segment base addresses to 0 and limits to 0xffffffff. Hence the "selector" has no effect and the linear address always equals the offset of the virtual address. 

boot/boot.S




这一部分不管我怎么操作都不能调出qemu的monitor,google了一下,使用以下命令可以调出:

qemu-system-i386 -hda obj/kern/kernel.img -monitor stdio -gdb tcp::26000 -D qemu.log
其实就是对 make qemu 的简单修改。

详细的 qemu monitor 用法可查看 lab guide


可以看出逻辑地址的映射情况与权限。



与之前的物理内存对比起来看可以发现两者的对应关系。

一共有两个PDE,其实就分别是线性地址(注意此时等于逻辑地址)的 0xf0000000~0xf0400000 和 0x00000000~0x00400000,但对应的物理地址都是0x00000000~0x00400000,一共有2^10个页面。



查看各个寄存器信息。

例如CS =0008 00000000 ffffffff 00cf9a00 DPL=0 CS32 [-R-]

0008 : The visible part of the code selector. We're using segment 0x8. This also tells us we're referring to the global descriptor table (0x8&4=0), and our CPL (current privilege level) is 0x8&3=0.(参考selector格式)


00000000 : The base of this segment. Linear address = logical address + 0x00000000.(注意 selector 的一个作用就是得到当前的 base address)

ffffffff : The limit of this segment. Linear addresses above 0xffffffff will result in segment violation exceptions.

00cf9a00The raw flags of this segment, which QEMU helpfully decodes for us in the next few fields.

DPL=0 : The privilege level of this segment. Only code running with privilege level 0 can load this segment.

CS32 : This is a 32-bit code segment. Other values include DS for data segments (not to be confused with the DS register), and LDT for local descriptor tables.

[-R-] : This segment is read-only.


此外,用qemu和gdb分别使用xp指令和x指令去查看地址内容指令。



在虚拟地址功能还未打开时:



发现虚拟地址的 0xf0100000 处的指令与实际指令不同。

在打开了虚拟地址功能后:




这次就对了~


From code executing on the CPU, once we're in protected mode (which we entered first thing in boot/boot.S), there's no way to directly use a linear or physical address. All memory references are interpreted as virtual addresses and translated by the MMU, which means all pointers in C are virtual addresses.



特别注意此处 uintptr_t 和 physaddr_t  是 32-bit 整型,不能直接解引用,除非将其转换为指针类型,但请注意将物理地址转换为指针型再去寻址没用意义,因为 MMU 会将其看作是虚拟地址,从而会出错。* 解引用针对的都虚拟地址。

因此,变量 x 的类型应该是 uintptr_t 。


物理地址——>逻辑地址:KADDR(pa)

逻辑地址——>物理地址:PADDR(va)



首先是 pgdir_walk():

这个函数就是实现虚拟地址到实际物理地址的翻译过程,根据给出的虚拟地址,返回其二级页表中对应的页表项的虚拟地址。

create 标志是1。如果当前va地址所属的页不存在,那么申请开辟这页,
create 如果标志是0,仅仅是查询va地址所属的页是否存在。存在就返回对应的page table的入口地址,不存在就返回NULL。

这个函数虽然不长,但是里面有各种地址,包括物理地址,虚拟地址(此处与线性地址相等),struct PageInfo* ,要充分知道几种宏用法(在kern/pmap.h以及inc/mmu.h里,例如KADDR(pa), PADDR(va), PTE_ADDR(pte), page2pa(struct PageInfo*), pa2page(pa), page2kva(struct PageInfo *), PTX(la)等等),还有多种结构,包括 linear address 的结构,PTE(PDE)的结构,关系也是错综复杂,一定要头脑清楚,此外还需记得 page_alloc()函数并不会为新分配的page的pp_ref加一,需要调用 page_alloc() 的函数自己完成。

PDE(PTE)里面的地址是物理地址!!! memset()函数使用的也是虚拟地址。

kern/pmap.c


关于为什么是 PTE_P | PTE_U | PTE_W 还有疑惑。


boot_map_region()函数把虚拟地址[va,va+size)的区域映射到物理地址pa开始的内存中去:

kern/pmap.c



page_lookup()函数检测va虚拟地址对应的物理页是否存在,不存在返回NULL。

kern/pmap.c


需要注意的是:pgdir_walk()函数并不保证当前指向的 PTE 是 Present 的,所以我们要自行判断。


page_remove()清除va与物理页的映射,把这个虚拟页的关联物理页的 PTE 置为NULL。关于 tlb_invalidate() 函数的作用,可戳 http://blog.csdn.net/cinmyheart/article/details/39994769

kern/pmap.c



page_insert() 把pp描述的物理页与虚拟地址va关联起来。

如果va所在的虚拟内存页不存在,那么pgdir_walk()的create为1,创建这个虚拟页
如果va所在的虚拟内存页存在,那么取消当前va的虚拟内存页也和之前物理页的关联,并且为va建立新的物理页联系——pp所描述的物理页

kern/pmap.c



哎,有bug了,耐心找吧~

哈哈,找到了!!

总结这部分代码,容易错的地方有这几点:

1、指针计算: 由于指针一般占据4个字节,所以再用基地址加上偏移量之前一定要确定当前基地址的指针类型,否则加法将会是单纯加上OFFSET,而非我们所需要的 4 * OFFSET。这个务必注意!

此处要知道不管是 pte_t,pdt_t,uintptr_t,physaddr_t其实都是uint32_t类型。

2、page_insert()函数一定要注意一个特殊的情况,即把相同的 va 再次映射到 pp 上时,若此时 pp 恰好只有一个映射(也即 va),此时在清除原始的映射时,pp由于pp_ref会被减到 0而被page_free(),pp 被 free 到 page_free_list,注意将此操作的影响,这里需要手动将该链表的 pp 节点删除,并将其 pp_ref 加一。注意 page_insert()函数只是把已经page_alloc()过的物理页映射到某个虚拟地址,此时的物理页是分配过的,不会出现在page_free_list中,如果出现只会是上面的特殊情况。

3、page_alloc()函数并不会为新分配的物理页的pp_ref加一,故调用 page_alloc() 的函数注意手动为其加一。


Part 3: Kernel Address Space

JOS系统将虚拟地址分为用户和核两个部分,用户部分在下,核在上,它们以 ULIM 为界。核约有 256M 的虚拟空间。

The user environment will have no permission to any of the memory above ULIM, while the kernel will be able to read and write this memory.For the address range [UTOP,ULIM), both the kernel and the user environment have the same permission: they can read but not write this address range. This range of address is used to expose certain kernel data structures read-only to the user environment. Lastly, the address space below UTOP is for the user environment to use; the user environment will set permissions for accessing this memory.




这一部分就是要利用之前写的子函数完成虚拟地址的映射,使用 boot_map_region() 函数。

kern/pmap.c


这部分映射pages,下面注释的那部分代码不知道需不需要。此处需要注意的是 PADDR(pages),我们知道 pages 其实是 struct PageInfo * 类型的指针,但其本质就是虚拟地址,只有在需要计算 pages[i] 物理地址时,才会用到 page2pa() 函数,此时我们是要把以 pages 虚拟地址开始的一页映射到物理页,所以用的是 PADDR(),开始理解错了,一直用的 page2pa。


kerm/pmap.c


这部分映射 kernel stack。注意 guard page 那部分不需要任何操作。bootstack 是虚拟地址。


kern/pmap.c


这部分映射 KERNBASE 。


以上3部分还有一个需要小心的是权限这部分。


3个主要使用的权限:

P: 0(默认)该页未被使用,1表示该页被使用,即PTE_P。

R/W: 0(默认)只读,1表示读写,即PTE_W。

U/S: 0(默认)系统用,1表示用户,即PTE_U。 

所以 Permissions: kernel RW, user NONE 对应 PTE_W | PTE_P(开始我写的PTE_P,显示下面的错误,吓死宝宝了。。。)



修改完这个错误后


make grade后





2、

kern_pgdir[3BD] 对应 kern_pgdir。

kern_pgdir[3BC] 对应 pages。

kern_pgdir[3BF] 对应 bootsatck。

kern_pgdir[3C0]~[3FF] 对应 从0开始的物理地址。


3、我们通过设置 PTE_U 让用户获取权限,不设置时默认为用户不可用。

4、2G, becuase the maximum size of UPAGES is 4MB, sizeof(struct PageInfo))=8Byte, so we can have at most4MB/8B=512K pages, the size of one page is 4KB, so we can have at most 4MB/8B*4KB)=2GB physical memory.

5、假设我们有2G的物理内存,那么4MB的pages,4KB的kern_pgdir,加上2MB的页表,一共是6MB+4KB。

6、在跳转命令jmp *%eax之后我们有高位的EIP。由于程序将 [0, 4MB) 和 [KERNBASE, KERNBASE + 4MB) 均映射到了物理地址的 [0, 4MB) 因此在开启页之后即使为 low EIP,也能继续运行。因为我们需要跳转命令这是在low eip 执行,为了保证正确,因此需要这一步。


Challenge暂时不做,后面有时间了再看~

           

猜你喜欢

转载自blog.csdn.net/qq_44919369/article/details/89353392