6.828笔记 Lab 2: Memory Management
Lab2分为三个部分,第一个部分是Physical Page Management,第二个部分是Virtual Memory,第三部分是Kernel Address Space。
Physical Page Management
物理页管理,需要写一个physical memory allocator,即物理内存分配器,用来管理内存。在内存管理里面,是以4096字节的page(页)为单位的。
需要按顺序完成kern/pmap.c
中的下列函数:
- boot_alloc()
- mem_init() (只需要完成在check_page_free_list(1)之前的)
- page_init()
- page_alloc()
- page_free()
在kern/init.c
里面,有i386_init()
函数,调用了kern/pmap.c
中的mem_init()
函数。mem_init()
又调用了i386_detect_memory()
函数,用来探测有多少实际内存的。就先打断点看内存数量。得到输出:
1 | Physical memory: 131072K available, base = 640K, extended = 130432K |
不需要关注是怎么算出来的,只需要看下面几行(单位是kilobytes):
1 | npages = totalmem / (PGSIZE / 1024); |
物理地址131072K等于128M,131072K=640K+130432K。也就是模拟器整个物理空间大小为128M,其中base
部分(图中的Low Memory部分)为640K,其他的都是extended
部分。PGSIZE
为4096,即一页有4096个字节。共131072KB内存,每4KB为一页,则npages就是整个物理空间可以划分出的页码数量,为32768页。npages_basemem
为base
部分可以划分出的页码数量,为160页。
初始页目录
然后就是下面两行:
1 | kern_pgdir = (pde_t *) boot_alloc(PGSIZE); |
pde_t
其实就是一个uint32_t
,也就是unsigned int
类型的指针,指向内存空间内的一个地址。memset
是把从kern_pgdir
开始的PGSIZE
个字节设置为0,相当于初始化。关键是补充boot_alloc()
函数。boot_alloc()
函数其实我一开始没看懂到底是啥意思,把网上的抄了下,后面才打断点,调试,慢慢看懂。
1 | static void * |
分配了初始页目录,大小为一页。有一个static
变量*nextfree
,用来指向下一个可用的地址。ROUNDUP((char *) end, PGSIZE)
是把end
的地址往上对齐到PGSIZE
个字节(向上取整)。如果n>0
,把nextfree + n
个字节的地址往上对齐,然后赋值给nextfree
。最后返回此次调用分配的地址的首地址result
。如果超出了物理地址,则报错。但是报错的那里像下面这样写有问题,所以就先删了。
1 | if (nextfree-KERNBASE > npages*PGSIZE) |
第一次调用返回的结果给kern_pgdir
:
1 | kern_pgdir:f0117000 |
可以看出,返回的是一个虚拟地址。上一次lab说把内核从0x100000
开始放,然后地址映射,在这里f0117000
会映射到00117000
,转换成十进制除以4096,得到279。即内核从0x100000
开始放,到0x117000
结束。kern_pgdir
的地址为0x117000
。
PageInfo分配
下面是让分配npages
个struct PageInfo
,保存在pages
中,pages
是一个PageInfo *
结构。先看PageInfo
结构体:
1 | struct PageInfo { |
其实就是链表,pp_link
指向下一个PageInfo
结构,pp_ref
为0表示此PageInfo
对应的一页物理地址未使用,为1表示在使用。
那么代码就比较简单了:
1 | pages = (struct PageInfo*) boot_alloc(npages*sizeof(struct PageInfo)); |
直接调用boot_alloc
分配npages*sizeof(struct PageInfo)
个字节的空间,转换成struct PageInfo*
并把初始地址赋给pages
,然后把这些地方全部赋值为0。
打印下面几个内容:
1 | cprintf("pages:%#x\n", pages); |
pages
的f0118000
和刚才的f0117000
相差0x1000
,也就是分给初始页目录的4KB。每一个PageInfo
的大小为8字节,内存大小共131072K,每4K对应于一个PageInfo
,所以共131072/4=32768
个PageInfo
,占用32768*8/4096=64
页。
此时的内存空间:
1 |
|
page_init
1 | void |
他给出的代码里面page_free_list
好像是反着链接的,我写的时候改了一下,正着链接。pages
相当于一个数组,放着32768个PageInfo
,page_free_list
把其中pp_ref = 0
的结构体串起来,形成了一个空闲页链表。page_alloc()
和page_free()
就很简单了,照着提示,对空闲页链表操作就可以了。最后注释一行panic
,运行,出现
1 | check_page_free_list() succeeded! |
说明前面的成功完成了。
Virtual Memory
1 |
|
C语言中使用的地址是虚拟地址,经过段式分页得到线性地址,再通过分页得到物理地址,用于访存。重点在从线性地址变换为物理地址的过程。
操作:
- 使用
make qemu
进入后,输入ctrl+a
,再按c
,进入QEMU monitor界面 - 在QEMU monitor界面中,输入
info pg
或info mem
,官网有介绍
下面要用到的函数、宏定义主要在kern/pmap.h
、inc/memlayour.h
和inc/mmu.h
中,建议仔细看这几个文件
分页机制
先记录一下分页机制。下图是xv6 book Chapter 2的一张图。
分页是对于虚拟地址的分页,虚拟地址一共有从0
到0xffffffff
共4GB的空间,而实际上物理上只有128MB的空间。下面要实现的函数中,输入进来的地址就是上图中的虚拟地址,被分为了三个部分,Dir
、Table
和Offset
。文档中说,当前使用的Page Direction
的地址存放在CR3寄存器,所以可以直接访问到Page Direction
。Dir
10位,当作索引,从Page Direction
的1024项中得到相关的一项,如果该项存在(PTE_P=1
),则此项的高20位是对应的Page Table
的物理地址,在C代码中使用时要转换为虚拟地址(加上KERNBASE
),得到Page Table
的地址。类似地,Table
10位,得到对应的项,检测是否存在,若存在,则高20位为对应的物理地址,加上Offset
得到实际物理地址。
注意:
- 每个
Page Direction
和Page Table
都是1024项,每一项都是32位,即占用内存4byte*1k=4KB。所以在代码中,kern_pgdir
是4KB的。如果某一项Page Table
不存在,要分配空间时,使用的是page_alloc()
函数,即分配了一页4KB。 Page Table
中的每一项的高20位为地址,低12位为权限,所以对应的地址是4KB对齐的,意思就是只能记录0,4KB,8KB这样的地址,也就是Page Table
中的每一项对应实际物理地址中的4KB。每一个Page Table
有1024个这样的项,则对应4MB。Page Direction
有1024个这样的Page Table
,则共可以映射4GB的空间,和之前的是对应的。- 刚才初始化的
kern_pgdir
是一个Page Direction
,占4KB。之前的代码初始化了kern_pgdir
,即全部设置为0。然后通过kern_pgdir[PDX(UVPT)] = PADDR(kern_pgdir) | PTE_U | PTE_P;
设置了kern_pgdir
的映射,后面的设置需要自己在代码中完成。至于之后每个进程是否需要拥有自己的Page Direction
和Page Table
,需要在后面的Lab中了解。
pgdir_walk
根据上面的解释和代码中的提示,现在就比较好理解了。
1 | pte_t * |
boot_map_region
boot_map_region()
是把从va
开始的size
个虚拟地址映射到pa
开始的size
个物理地址。如果在Page Direction
对应的项不存在,则会分配空闲的一页,把这一页的地址记录在Page Direction
中,在分配的这一页中的对应的4KB对齐的位置,写入对应的物理地址和权限,但是并没有对该物理地址进行读写操作。
1 | static void |
其他的就比较简单了,在page_insert()
中有一点要注意,先pp->pp_ref++;
,参考的那个博客里面也都讲了。
Kernel Address Space
在这一部分设置内核的映射。在inc/memlayout.h
中有虚拟的4GB的分配。从UTOP
往下是用户可用的;从UTOP
到ULIM
,用户和内核都是只读的;从ULIM
往上到KERNBASE
有一段,放的是栈之类的;从KERNBASE
到4GB都是内核的,内核可以读写,用户无法访问。KERNBASE
为0xf0000000
,到0xffffffff
有256MB。
根据博客的提示,在mem_init()
对应位置加上:
1 | boot_map_region(kern_pgdir, UPAGES, npages*sizeof(struct PageInfo), PADDR(pages), PTE_U); |
分别是把npages
个PageInfo
映射到UPAGES
的虚拟地址,用户可读;把KSTACKTOP-KSTKSIZE
开始的KSTKSIZE
个字节映射到bootstack
的位置;把从KERNBASE
,即0xf0000000
开始的256MB映射到物理0的位置,此时,访问虚拟地址0xf0001000
就会转到物理地址0x00001000
。
最后make grade
,满分。
总结
下面这幅图总结了这次Lab的大部分内容,其中,kern_pgdir
的位置和之前的不一样,应该是因为要完成Lab,在之前的基础上对内核里面添加了代码,所以内核部分占用地多了,所以kern_pgdir
地址靠后了。
1 | (qemu) info pg |
一个一个来分析。
- 0x3bc=956,里面的
PTE
是从000-03f,0x3f=63。和上图分析的一样,只使用了前64项 - 0x3bd=957,上面说这是
Page Direction
。仔细观察,会发现这里面的PTE
和info pg
显示出的PDE
相同,即在这个地方放的是kern_pgdir
,里面的页表项即整个Page Dicection
映射的项 - 0x3bf=959,0x3f8-0x3ff即1016到1023,和之前的分析一致
- 0x3c0=960,从0到0x3ff,即1023都有项,只不过有些是dirty和accessed了的
PDE
从0x3c1到0x3ff,即961到1023,都是有项的
观察0x3bd项的Physical page,这个的意思就是Page Direction
中分配了的Page Table
对应的物理地址。有一个是0011a,其他的是从1到0x42,即66,和之前的分析一致。至于第一个PTE
是3而不是1,经过测试发现,按顺序的话,本来应该是PageInfo[1]
,即0x1000,但是有check程序,估计释放的时候把顺序改变了,所以前三个PageInfo
的顺序反了。
1 | (qemu) info mem |
和上面的结果类似,只是更简略了,意思是一样的,就不展开讲了。
最后,再来一个测试。比较明显地,PageInfo
被映射了两次,分别是在956项和960项。kern_pgdir[956]
的第0项对应PageInfo
的前4KB。PageInfo
的物理地址为0x11b000,在kern_pgdir[960]
项中对应11b项,即283项。在mem_init()
后面加上:
1 | cprintf("956[0](pages's pa):\n"); |
得到结果:
1 | 956[0](pages's pa): |
可见,前20位都是0011b,都做出了正确的映射。前一个的权限是005,也就是U-P。第二的权限是063,DA—WP,用户无法访问。
Lab2做得很艰难,直接看人家的博客看不懂,然后就开始看课程给的资料,看一次看不懂,各种资料换着看,加上抄抄代码,调试输出,最后终于搞懂了。最后的Challenge做了一个,就是显示虚拟地址对应的物理地址,大致怎么做还是很清楚的,但是一些代码上的具体的细节还是不太行,最后在网上抄了一个,能看懂。像修改权限位那些,就先没做,如果后面的Lab需要了就再弄。