Part 3: Kernel Address Space
JOS 把处理器的32位地址空间划分为两部分。User Environment管理低地址的部分,Kernel则对高地址的部分保持控制。这种划分比较的任意,大致是以inc/memlayout,h
中的ULIM
为划分。
因为kernel和user memory在彼此的地址空间中存在,我们需要使用权限位来让用户代码只能访问用户的地址空间。不然可能会导致写入了内核代码而产生问题。
用户态应当没有权限来访问在ULIM
上的内存,kernel则应当拥有读写权限。用户态代码和内核都对[UTOP, ULIM)
的内存只拥有读权限,因为这部分是为了向用户态暴露一部分内核只读数据结构。UTOP
下面的地址空间则是留给用户态使用的。
初始化内核地址空间
Exercise 5要求设置UTOP
以上的地址空间,补充完成mem_init()
中剩下内容。
//
// 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
// Your code goes here:
boot_map_region(kern_pgdir, UPAGES, PTSIZE, PADDR(pages), PTE_U);
//
// 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
// Your code goes here:
boot_map_region(kern_pgdir, KSTACKTOP - KSTKSIZE, KSTKSIZE, PADDR(bootstack), PTE_W);
//
// 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
// Your code goes here:
boot_map_region(kern_pgdir, KERNBASE, 0x10000000, 0, PTE_W);
然后make qemu
看看结果:
Physical memory: 131072K available, base = 640K, extended = 130432K
check_page_free_list() succeeded!
Fail to allocate a new page! Out of Free Physical Memory!
Fail to allocate a new page! Out of Free Physical Memory!
check_page_alloc() succeeded!
Fail to allocate a new page! Out of Free Physical Memory!
Fail to allocate a new page! Out of Free Physical Memory!
Fail to allocate a new page! Out of Free Physical Memory!
Fail to allocate a new page! Out of Free Physical Memory!
Fail to allocate a new page! Out of Free Physical Memory!
Fail to allocate a new page! Out of Free Physical Memory!
check_page() succeeded!
check_kern_pgdir() succeeded!
check_page_free_list() succeeded!
check_page_installed_pgdir() succeeded!
通过了所有测试。
在回答后面的问题之前,我们重新回过头梳理一下这个lab究竟做了什么事情,完成了怎样的映射。
Review:
先来回顾一下各个函数究竟在干什么,特别关注操作对象及其地址情况。
boot_alloc
:
给定n
,在.bss end (0xf011A0000)
之上划分一块n
大小,按页对齐的虚拟地址空间。
page_init
:
对虚拟地址[0xf011B000, 0xf015B000)
的pages
数组根据物理内存的使用情况进行初始化,产生一个PageInfo
的空闲物理内存页的链表。
page_alloc
:
给定alloc_flags
(决定分配的页是否要清零),从PageInfo
的空闲物理内存页的链表中找到代表空闲物理页的PageInfo
结构体,并将这个结构体对应的虚拟内存页根据alloc_flag
清零或不清零,返回指向这个结构体的指针。
page_free
:
将给定的PageInfo
结构体加入空闲链表中。这里即不对物理内存做操作,也不对虚拟内存做操作,也不调整页表项。
pgdir_walk
:
给定指向页目录的指针和虚拟地址,返回对应虚拟地址的页表项的指针。这里特别需要注意虚拟地址和物理地址的区分。拿到的页目录的指针是虚拟地址,得到的页目录项中包括了页表的物理地址。需要先转换成页表的虚拟地址,然后访问拿到对应的页表项,再取出页表项的虚拟地址,返回。
page_lookup
:
给定指向页目录的指针,一个虚拟地址,和一个指向页表项指针的指针。首先查找该虚拟地址对应的页表项,如果不存在指向该页表项的指针,或者该页表项不是Present
返回NULL,否则传入的双重指针指向该页表项的指针,返回指向对应该页表项对应物理地址的PageInfo
结构体的指针,并且使虚拟地址对应的TLB无效。
page_remove
:
给定指向页表项的指针,一个虚拟地址。先查找并拿到虚拟地址对应的PageInfo
结构体和指向该虚拟地址对应页表项的指针。减少对该物理页的引用并清空该页表项,相当于取消虚拟地址和其对应物理地址的映射。
page_insert
:
给定指向页目录的指针,一个代表物理页的指向PageInfo
结构体的指针,和一个虚拟地址。先找到该虚拟地址对应的页表项的指针,如果该指针存在且页表项Present
,那么我们先增加对给定物理页的引用,使用page_remove
移除原有的映射(顺序是针对corner case的考虑),拿到给定结构体代表的物理页的物理地址,写到给定虚拟地址对应的页表项里,并使该虚拟地址对应的TLB无效。
boot_map_region
:
给定指向页目录的指针,一个起始虚拟地址,一个起始物理地址,和地址空间大小以及权限。取出虚拟地址对应的页表项,把对应的物理地址和权限写进去。注意在注释中说不需要增加相应物理页的引用。(感谢GZZ同学解惑,我之前这里还不是特别清楚为什么不用引用计数增加,因为这里并不是真正地分配了page,而仅仅是通过写页表完成了映射)
这里看的时候还需要特别注意,KADDR
和page2kva
的区别,以及PADDR
和page2pa
的区别。前者是直接返回某个虚拟地址的物理地址或者是某个物理地址的虚拟地址,即加减KERNBASE
,而后者则是返回PageInfo
结构体对应的物理地址和虚拟地址。
还需要注意的一点是,分配空闲页是分配空闲页,映射虚拟地址和物理地址只是映射,二者函数功能分离。
我们再回去看mem_init()
,映射完成之后就是这么个情况:
然后我们来回答一下剩下的问题:
2. 要看页目录的填充情况,只需要看mem_init
函数中的映射行为就可以了。
每个entry对应的基虚拟地址也就是entry乘上0x400000
, 因此就不重复说了。
kern_pgdir[PDX(UVPT)] = PADDR(kern_pgdir) | PTE_U | PTE_P;
手动把UVPT
对应的页目录项映射到页目录对应的物理地址,UVPT
对应entry 957,内容是页目录的物理地址。
boot_map_region(kern_pgdir, UPAGES, PTSIZE, PADDR(pages), PTE_U);
把UPAGES
和pages
数组占据的物理地址空间建立映射,UPAGES
对应entry 956,指向UPAGES
对应的页表,页表指向pages
数组的物理地址空间。
boot_map_region(kern_pgdir, KSTACKTOP - KSTKSIZE, KSTKSIZE, PADDR(bootstack), PTE_W);
把KSTACKTOP-KSTSIZE
(即cpu0的kernelstack
)和bootstack
占据的物理地址空间建立映射,对应entry 959,指向包含多个kernstack
到对应bootstack
映射的页表。注意在这里我们只映射了cpu0的kernelstack
和bootstack
,而由于剩下的kernelstack
页目录项是一样的,所以页目录项非空,且相应的页表非空,但是页表中除了cpu0 kernelstack
之外其它的页表项是空的。
boot_map_region(kern_pgdir, KERNBASE, 0x10000000, 0, PTE_W);
把kernel
映射到物理地址0开始的256MB物理地址空间,kernel
地址从0xf0000000
到0xffffffff
, 因此对应的页目录项的enrty从960到1023,指向负责映射kernel
的页表,页表指向从0开始的256MB物理地址空间。
3. 因为使用了权限位,因为内核代码的权限位标志为supervisor level且Read-only access
4. 最多支持2G,因为RO PAGES 4096*1024 Byte 一个 这个地方之前没有注意到PageInfo
结构为8 Byte,一个物理页4096 Byte,因此最多访问4096 * 1024 / 8 * 4096 = 2GBKADDR
的映射是加上kernbase
,我以为是映射到全部的4G虚拟地址空间,因此为了不溢出,只能支持256MB。
5. 最多可以放4MB的 如果只能支持256MB的大小,那么PageInfo
数组,加上4KB的页目录,再加上4MB的页表,一共8MB+4KBPageInfo
数组最多512KB,加上4KB的页目录,但是可以把所有的虚拟地址映射到256MB的物理内存上去,因此页表最多使用4MB大小,这样总共是4612KB.
6. 在jmp *%eax
之后。 因为我们之前在entrypgdir.c
里面把[0x00000000, 0x00400000)
的虚拟地址映射到了[0x00000000, 0x00400000)
的物理地址。因为后面kern_pgdir
会被加载进来,低虚拟地址就不再使用了。
Challenge
(补坑)