笔记05 - Lab2: Memory Management

Part 1: Physical Page Management

程序的几乎所有代码都集中在 pmap.c 文件中。此部分为物理页面管理,强调对机器拥有的物理内存的管理,包括建立对应的数据结构、处理分配和回收动作等。需要完成boot_alloc()mem_init() (only up to the call to check_page_free_list(1))、page_init()page_alloc()page_free()等函数。

背景知识

JOS 的启动过程实际上是先把 bootsector 的内容读到 0x7c00 处 (注意,bootsector 的代码在编译的时候已经故意地把逻辑地址的首地址定在了 0x7c00 上),bootsector 中的代码开始执行后,会从磁盘上紧接着自己的第 2 个扇区开始,一直读 8 个扇区的内容(一共是 8×512=4KB, ELF 头的大小)到 0x10000(64KB)的地方,然后,通过对 ELF 头的解析,得到 kernel 模块编译出来后所占的大小,并将 kernel 读到物理内存 0x100000(1MB)开始的地方。然后加载 entry_pgdir 的物理地址到 cr3 中开启分页,并调用 i386_init() 函数,而 i386_init() 函数在将自己的 BSS 区域清零后,调用 cons_init() 函数设置好屏幕显示设备为 cprintf 的运行做好准备后就调用 mem_init() 函数。 mem_init(); 函数调用 i386_detect_memory(); 函数读 CMOS 取得物理内存的实际大小。i386_init() 函数最后会调用 monitor(NULL); 并进入循环,处理用户通过终端输入的命令。
在调用 i386_init() 函数以前,内存分布如下所示:

以下是物理内存管理设计思想

调用i386_detect_memory()函数获取到物理内存的基本内存页数和总内存页数(包括扩展内存)。

调用boot_alloc()函数为页目录分配 4KB 的空间。外部字符数组变量 edata 和 end,其中 edata 表示的是 bss 节起始位置(虚拟地址),而 end 则是表示内核可执行程序结束位置(虚拟地址)。boot_alloc()函数从 end 表示的位置(注意是线性地址)开始进行 4K 位置对齐,然后分配足够页空间。nextfree 表示空闲的线性地址,npages 表示总内存页数。根据(uint32_t)nextfree - KERNBASE > npages * PGSIZE判断是否越界。每次分配后对 nextfree 进行 4K 对齐。

使用boot_alloc()函数分配 pages 结构体数组空间(struct PageInfo 类型),共 npages 项,将其清零,后续将从该数组空间中动态分配出页空间(比如分配页存储页表等)。pages 数组与实际物理空间存在映射关系,不需要通过pages 数组存储物理页位置。struct PageInfo 包括 pp_linkpp_refpp_ref标识页空间是否可用,pp_link用于回溯前一个空闲的PageInfo,形成双向列表(具体见page_init())。pages 始终指向页空间首位置,使用下标向后访问所有可用/不可用内存;page_free_list 始终指向可用页空间的末尾位置,往回指的指针 pp_link 要越过非空物理页,这样 page_free_list 才能通过 pp_link 往回获取空闲空间。由此可知, PageInfo 结构体数组空间是连续的,对于每次分配出的PageInfo指针,都可以计算出其与数组头之间的偏移,从而计算出当前是分配到第几页物理页;另一方面,从page_free_list角度看,连续的 PageInfo 结构体数组空间又是由指针回溯的不连续空间,从而可以自由合并和分出PageInfo空间。

关于内存可用与否:
第一页数据是被使用的,该页保存了实模式IDT和BIOS数据结构。
第二页至基本内存结束位置(640K)是可用的。
[IOPHYSMEM, EXTPHYSMEM)即[0x0A0000,0x100000)即[640K,1MB)是被使用的。io_hole = 96 pages。
扩展内存开始位置至当前 pages 数组存储的末端为不可使用的部分(准确来说应该是使用 boot_alloc(0) 获取得到的线性地址,此线性地址才是代表 boot_alloc() 后 4K 对齐的位置),boot_alloc(0) 的线性地址开始至可用内存末端即为可用的。
注意:扩展内存开始位置为 1M,内核代码存放于 0x100000 处,即 1M 处,KERNBASE 是内核代码存储起点的线性地址。对于存于物理地址 1M 后的数据,其线性地址减去 KERNBASE 的值相当于该处与内核代码存储起点线性地址位置的差,而不是与 0x0 的差, 需要加上 1M 的空间,即 256 page。

page_alloc()函数将从 pages 数组空间中由后往前分配,通过使用 page_free_list 指针和 pp_link 成员。当传入参数标识非 0 时,分配的空间将被清零。虽然一开始 pages 数组空间被清零,但此刻分配的空间可能是之前被使用后回收的,不一定为空。
page_free()函数回收页空间,将page_free_list指向回收的空间。

Part 2: Virtual Memory

通过 Part1 的几个函数,我们得以在可用内存空间上调用boot_alloc()函数分配足够的页对齐的空间大小,并且通过映射物理页使用情况的 pages 数组来分配二级页表页和索引页目录项到该二级页表页,以及分配物理页和索引二级页表项到该物理页。

背景知识

1、x86的虚拟地址包括段选择子和偏移部分,经过段地址转换以后,获得线性地址,再经过页地址转换以后,获得物理地址。
2、一个C指针就是代表虚拟地址的偏移地址部分。
3、boot/boot.S对GDT表进行设置,其中所有段基址设为0,段限长设为0xffffffff,因此段选择子部分失去了对段地址转换的影响,线性地址等于偏移地址(那也就是说一个C指针就是代表线性地址?)。
在内核还没动态创建页目录和页表之前,为了能在0xf0100000地址上运行内核代码,jos通过手写、静态初始化页目录和页表映射了前4M内存空间。而lab2实验则是动态映射物理地址空间的低256MB(0x00000000 - 0x0fffffff)到线性地址(0xf0000000 - 0xffffffff)。
4、qemu使用info pg命令可以查看当前页表的详细信息,包括映射的内存范围、权限以及标识。info mem命令概述了哪个范围的虚拟地址被映射,以及其权限。
5、页目录所在的物理页面的首地址在启动 x86 的页式地址管理前,需要放到 CR3 中,这样 x86 在进行页式地址转换时会自动地从 CR3 中取得页目录地址,从而找到当前的页目录。
6、在现代操作系统中,我们经常可以听到“用户进程的虚地址空间为 4GB”的说法,怎么来实现呢?原理其实很简单:为每个用户进程创建一个页目录,并将这个页目录保存到用户进程的上下文中,当该用户进程被切换过来执行的时候,就将该用户进程的页目录地址写入 CR3,并重新启动一次页式内存管理。当然如果采用这种方式,势必占用更多的内存用于页式地址变换。对于老版本的 Linux 系统以及我们现在接触的 JOS 系统, 为了避免太大的内存开销(另一个原因是因为还没有虚拟内存的支持),在整个系统中只使用一个内核页目录。这就意味着,整个系统的线性地址空间只有 4GB,而且是所有(操作系统内核、各个用户程序)代码所公用的!这种情况下,就必须对线性地址进行合理的规划了。
7、对于页式地址管理,由于页目录以及页表都存放在物理内存的页面中,要进行地址变换先要到内存中访问页目录和页表。由于 CPU 和内存速度的不匹配,为了最小化用于地址转换的总线周期, x86 系统中设计了用于地址翻译的缓存来解决这一问题,这一缓存称为 TLB( Translation Look-aside buffer,即旁路转换缓冲,或称为页表缓冲,或转换后备缓冲区),在该缓存中存放了最近访问的页目录和页表条目,由于程序执行的局部性原理,下一次的地址转换往往跟上一次的地址转换采用的是同一个页目录表项和页表项),同时,由于 TLB 跟处理器的距离更近,这样就极大地提高了地址翻译的效率和速度。但是,这样做可能存在一个潜在的问题:页目录(表)数据项的不一致性。以前系统里对应一个线性地址只有唯一的存放在内存中的页目录和页表,用于完成翻译的工作,但是现在由于 TLB 的存在,系统可能在高速缓存中也存放了一份页表项数据,用于更快地对地址进行翻译。因为 TLB 中的数据对于程序员来说是不可见的,程序对于页表项或者页目录项的修改并不能马上反映到 TLB 中,这样就可能导致错误的地址翻译,因为为了提高翻译的速度,处理器总是尽量地采用 TLB 中的页表数据进行地址的翻译。所以,为了避免这种数据的不一致性所导致的地址翻译的错误情形的出现,系统程序员就必须在对页表进行修改后使 TLB 中旧的页表数据失效。使其失效的办法有两个,一个是重载 CR3,使整个 TLB 中的数据都失效,也可以采用 invlpg 指令。

以下是线性地址到物理页映射的设计思想:
pgdir_walk()函数根据线性地址返回二级页表项入口。首先获取页目录项位置,使用指针指向其索引位置。如果页目录项不存在二级页表映射,且create标识为0,则返回NULL。否则使用page_alloc函数分配页表空间,分配失败则返回NULL,否则将分配的PageInfo结构体引用加1,并将其对应的物理页地址和权限(二级页表权限设为 kern R/W, user R/W)存在页目录项中。二级页表物理地址是4K对齐的,将其低12位清零然后转化为内核地址,使用指针指向它。注意到页目录和页表存的是物理地址,而指向页目录和页表的指针需要使用逻辑地址。最后根据指向二级页表的指针和通过线性地址求得的二级页表项索引,返回指向二级页表项的指针。pgdir_walk()只负责创建二级页表,然后返回指向二级页表项的指针,不对二级页表做处理,也不做其对物理页映射。我们知道,pages结构体数组是用于映射所有内存空间的,page_free_list指针指向的结构体所映射的内存空间均可用。page_alloc函数将从 pages 数组空间中分配可用空间,结构体映射的物理页地址将作为线性地址的页表,将其存储到页目录里。

page_lookup()函数根据线性地址返回二级页表项所指的物理页对应的PageInfo数据结构。pte_t **pte_store存储的是二级页表项的地址(传递过来的是某个指针的地址,对于那个指针来说,函数执行后它就指向对应的二级页表项了),设计这个指针的含义是:当通过page_remove()移除线性地址对应的物理页时,可以清空二级页表项的内容。page_lookup()函数调用pgdir_walk()获得指向二级页表项的指针,不允许创建二级页表。如果页目录项不存在二级页表映射且不允许创建二级页表,或者没有空间分配二级页表,pgdir_walk()会返回NULL,则page_lookup()返回NULL。如果获得的二级页表项不存在物理页映射,则page_lookup()返回NULL。否则将二级页表项的地址存于pte_t **pte_storepgdir_walk()返回的二级页表项指针内容是所指的物理页物理地址,将其低12位清零,并返回对应的PageInfo数据结构。

page_remove()函数移除线性地址对应的物理页,清空二级页表项,使tlb无效化。如果不同的线性地址映射到同一个二级页表项,它们就映射到同一个物理页。另一种情况是不同的线性地址映射到不同的二级页表项,且它们通过page_insert映射到同一个物理页,这代表的是不同的二级页表项存取同一个物理页地址。通过page_remove即可解除情况1到二级页表项的映射,在这里,一旦线性地址对应到二级页表项,即将其清空。page_remove()函数通过调用page_lookup()函数获得线性地址对应的物理页的PageInfo数据结构,然后调用page_decref()函数将PageInfo对象引用减1,如果引用为0则释放该内存页。

page_insert()函数将映射物理页的PageInfo数据结构的地址及访问权限存于线性地址所对应的二级页表项内。page_insert()函数首先调用pgdir_walk()函数获得指向线性地址的二级页表项的指针,允许创建二级页表。当对应的二级页表不存在且没有足够内存空间创建二级页表时,pgdir_walk()函数返回NULL,则page_insert()函数返回-1。否则pgdir_walk()函数返回指向二级页表项的指针,当该指针的P存在位为1时,代表当前二级页表项存在物理页映射,这时候需要判断该物理页与要插入的物理页是否是同一页,判断方法是:将指针内容(已经是物理地址)低12位清零,转化为PageInfo指针p,并与函数参数PageInfo *pp进行比较。如果是同一页(p与pp指向同个结构体),应该允许修改权限(包括降低权限,所以先清空低12位)后直接返回0,否则调用page_remove()函数移除线性地址对应的物理页,清空二级页表项。接着待插入的PageInfo指针pp的引用加1,将pp对应的物理页物理地址及权限存于二级页表项处,使tlb无效化。注意如果已映射的物理页和待插入的物理页是同一页的话,不能先移除物理页p再将pp的引用加1,因为一旦移除物理页p,p可能因为引用为0而被释放,进而page_free_list指向这块内存页,将其标记为可用,而此时pp的物理页是不可用的。

boot_map_region()函数将线性地址空间[va, va+size)映射到物理地址空间[pa, pa+size)。pa、va是页对齐的,size是页大小的倍数。boot_map_region()函数循环调用pgdir_walk()函数返回指向二级页表项的指针,然后将对应的物理地址和权限存储到该指针。jos只有一个内核页目录,最多映射4G空间。jos将[KERNBASE,4G)的空间映射到物理内存上,其中KERNBASE=0xf0000000=3840M,4G=4096M,两者相差256M。因此,物理内存最多映射256M,即[0,256M)。4G等于0x100000000,其被映射的最后一页的起点地址是0xfffff000,这也就是boot_map_region()中能被映射的最后一个线性地址。

Part 3: Kernel Address Space

jos一共有三个线性地址到物理地址的映射是需要的,此部分负责进行映射:
[UPAGES, sizeof(PAGES) ] => [pages, sizeof(PAGES)],这里 PAGES 代表页面管理结构所占用的空间;
[KSTACKTOP – KSTKSIZE, 8] => [bootstack, 8]
其中 bootstack 为内核编译时预先留下的 8 个页面(用做内核堆栈);
[KERNBASE, 4G) => [0, pages in the memory),这个地址映射范围比较广,含盖了所有物理内存。这里是256M。

以下是对question的解答:
2. 哪些页目录项已经被填充?它们映射到哪些地址并指向哪些内容?
使用技巧:使用qemu调试后,在执行qemu的控制台里,先后按下ctrl、a、h查看帮助,先后按下ctrl、a、c可切换qemu监控器和终端控制台,切换到qemu监控器后,执行info pg可查看当前页表结构。

以其中的[ef000-ef3ff] PDE[3bc] -------UWP为例,它表示该页目录项映射的线性空间范围是[ef000000, ef3fffff],两者之差为3fffff,即4M大小。3bc表示索引是956。UWP表示该项是kernel RW, user RW且president的。

上图表示的页目录项如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
Entry       Base Virtual Address    Points to (logically):
1023(3ff) 0xffc0000 Page table for top 4MB of phys memory, kernel RW, user NONE
1022(3fe) 0xff80000 Page table for second to top 4MB of phys mem, kernel RW, user NONE
.
960(3c0) 0xf0000000 Page table for bottom 4MB of phys memory, kernel RW, user NONE
959(3bf) 0xefc00000 (0xefff8000 - 0xf0000000 Kernel stack, kernel RW, user NONE)
(0xefc00000 - 0xefff8000 Invalid memory, or maybe other cpu's stack)
957(3bd) 0xef400000 Current page table kernel R-, user R- (pgdir)
956(3bc) 0xef000000 User pages, kernel R, user R
.
2 0x00800000
1 0x00400000
0 0x00000000 <Nothing, mapping got cleared>

3. 内核环境和用户环境被映射到同一个地址空间,为什么用户程序不会读写内核内存?有什么保护机制吗?
ULIM和UTOP将虚拟内存分为各个段。(ULIM, 4GB)只有内核环境能读写,(UTOP, ULIM]内核和用户环境都可以读取,[0x0, UTOP]是用户环境空间。这些内存空间被权限位所保护,如PTE_W(可写)和PTE_U(用户),这些是在页表/页目录项中设置的标识。
具体用于保护的机制是当前特权级别(CPL),CS的低2位。CPL=0则代表特权O/S,CPL=3 代表用户可访问。这也被用来检测当前的模式,以及我们是否可以写入虚拟内存地址。

4.jos能支持的最大物理内存是多少?为什么?
这个操作系统能支持的最大物理内存是256 MB。这是因为我们希望能够将所有的线性地址映射到物理地址。事实证明这样做使它更容易从物理地址反向映射到虚拟地址。

5.如果我们真的有最大数量的物理内存,需要多少空间开销来管理内存?
管理内存有页表和页目录,4G空间需要1个页目录和1024个页表,总共需要的空间开销是4KB * 1024 + 4KB。

6.重新阅读kern/entry.S和kern/entrypgdir.c,我们开启分页后,EIP仍然是小于1MB的值,我们是在哪个point开始在基于KERNBASE的EIP上执行指令?从我们开启分页的时刻,到我们开始在基于KERNBASE的EIP上执行指令的时刻,这段时间为什么能够使用low EIP?
在entry.S中,执行jmp *%eax指令之后eip将会加上KERNBASE。在开启分页到执行jmp *%eax指令这段期间,之所以能够使用low EIP,是因为线性地址[0, 4MB)和[KERNBASE, KERNBASE+4MB)同时映射到[0,4MB)物理地址,如果不映射线性地址[0, 4MB),则low eip的线性地址不在可访问线性地址范围内,将出现非法访问。

以下是对Challenge的解答:

Changeling1

我们使用了很多二级页表来映射KERNBASE地址,一个更有效的方法是使用页目录项的PTE_PS (“Page Size”)位,最初的80386不支持该位,但近年来的x86处理器可支持。阅读【Intel® 64 and IA-32 Architectures Software Developer’s Manual】第3.6 节【PAGING (VIRTUAL MEMORY) OVERVIEW】。**
如果将CR4中的PSE位打开,那么就可以开启4MB物理页。那么对应使用该物理页的虚拟地址寻址方式变为:

这个时候相应的页目录表项也要修改,要修改其PS位表明该表项对应的地址是4MB,不用再进行二级页表寻址了:

设置了使用4MB页表以后,那些二级页表查找和插入的过程就不能通用了,即pgdir_walk()page_insert()函数等。但这不影响check_page()函数的检查,因为check_page()函数虽然调用了pgdir_walk()page_insert()等函数,但是其检查逻辑是先模拟插入page再检查,二级页表的创建和页目录存储二级页表地址这些操作仍会被模拟(虽然此时页目录项的内容应该是4MB页地址,但其实是存了二级页表的地址)。相反,check_kern_pgdir()函数则不会成功执行,因为该函数是直接检查线性地址到物理地址的实际映射情况,再没有模拟插入page、创建二级页表的情况,所以其在涉及到二级页表的查询将会出错。所以修改为4MB页以后,应该注释掉check_kern_pgdir()函数的调用。如果qemu模拟器没有出现Triple fault,则代表寻址是正确的。实现代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
void
mem_init(void)
{
...
// Turn on CR4_PSE for 4MB page
lcr4 (rcr4 () | CR4_PSE);
...
//boot_map_region(kern_pgdir,UPAGES,PTSIZE,PADDR(pages),PTE_U | PTE_P);
boot_map_region_4m(kern_pgdir,UPAGES,PTSIZE,PADDR(pages),PTE_U | PTE_P);
...
//boot_map_region(kern_pgdir, KSTACKTOP - KSTKSIZE, KSTKSIZE, PADDR(bootstack), PTE_W | PTE_P);
boot_map_region_4m(kern_pgdir, KSTACKTOP - KSTKSIZE, KSTKSIZE, PADDR(bootstack), PTE_W | PTE_P);
...
//boot_map_region(kern_pgdir, KERNBASE, 0xffffffff - KERNBASE + 1, 0, PTE_W | PTE_P);
boot_map_region_4m(kern_pgdir, KERNBASE, 0xffffffff - KERNBASE + 1, 0, PTE_W | PTE_P);
...
// Check that the initial page directory has been set up correctly.
//check_kern_pgdir();
...
}

/*
jos只有一个内核页目录,最多映射4G空间。
jos将[KERNBASE,4G)的空间映射到所有物理内存上,这段内存是256M,
其中KERNBASE=0xf0000000=3840M,4G=4096M,两者相差256M。
因此,物理内存最多映射256M。4G等于0x100000000,其被映射的最后一页的起点地址是0xfffff000,
这也就是boot_map_region中能被映射的最后一个线性地址。

todo...可能存在的问题是:映射的是256M的内存,但是实际上只有64M可用的内存空间,
这些内存空间有对应的页面管理结构,其中一部分被作为映射256M内存空间时的二级页表。
*/
static void
boot_map_region(pde_t *pgdir, uintptr_t va, size_t size, physaddr_t pa, int perm)
{
// Fill this function in
/*
使用下述方法时,在映射[KERNBASE,4G)地址时
只能使用 boot_map_region(kern_pgdir, KERNBASE, 0xffffffff - KERNBASE, 0, PTE_W | PTE_P);,
不能使用 boot_map_region(kern_pgdir, KERNBASE, 0xffffffff - KERNBASE + 1, 0, PTE_W | PTE_P);
0xffffffff - KERNBASE + 1 = 0x10000000,KERNBASE+ 0x10000000会越界。
*/
/*
uintptr_t current_va;
physaddr_t current_pa = pa;
// The highest possible address of a virtual page
uint32_t last_page_addr = 0xfffff000;
for (current_va = va; current_va < va+size; current_va += PGSIZE)
{
pte_t *pte = pgdir_walk(pgdir,(void *)current_va,true);
if(pte == NULL) return;
*pte = (current_pa | perm | PTE_P);

//此判断是有必要的,当将[KERNBASE,4G)映射到[0,256M)时,线性地址累积到0xfffff000的时候,for循环的判断条件仍然是成立的。
if (current_va == last_page_addr)
break;

current_pa += PGSIZE;
}
*/
/*
使用下述方法时,在映射[KERNBASE,4G)地址时
本应该使用 boot_map_region(kern_pgdir, KERNBASE, 0xffffffff - KERNBASE + 1, 0, PTE_W | PTE_P);,
不能使用 boot_map_region(kern_pgdir, KERNBASE, 0xffffffff - KERNBASE, 0, PTE_W | PTE_P);,
因为这会导致循环次数减1,246M物理内存最后的1页没有被映射。
但由于实际的npages为16639,检测的总空间为64M,
check_kern_pgdir()函数会针对npages大小检测KERNBASE以上的地址映射情况,
超过64M以上的物理内存不会被检测到,所以并没有导致出错。
但严谨来说应该使用第一种情况。
*/
int i;
for (i = 0; i < size/PGSIZE; ++i, va += PGSIZE, pa += PGSIZE) {
pte_t *pte = pgdir_walk(pgdir, (void *) va, 1); //create
if (pte == NULL) panic("boot_map_region panic, out of memory");
*pte = pa | perm | PTE_P;
}
}

static void
boot_map_region_4m(pde_t *pgdir, uintptr_t va, size_t size, physaddr_t pa, int perm)
{
pde_t *pde;
int i;
for (i = 0; i < size/PTSIZE; ++i, va += PTSIZE, pa += PTSIZE) {
pde = pgdir + PDX(va);
*pde = pa | perm | PTE_P | PTE_PS;
}
}

Changeling2

扩展jos的命令,1、当给定线性地址的范围时,以一种易读的格式展示内存页映射情况。如’showmappings 0x3000 0x5000’命令可以展示线性地址0x3000、0x4000、0x5000的物理页映射及其权限位。2、能显式清除、设置或改变当前地址空间映射的权限。3、给定一段线性地址/物理地址范围,显示其内容,必须确保当给定范围跨页面边界的时候显示代码的正确性。**
1、提供两个函数,一个是将地址字符串转换为整数,一个是将读入参数,按页大小输出二级页表项内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
uint32_t jz_xtoi(char* pstr){
uint32_t res = 0;
pstr += 2;//0x...
while(*pstr){
if(*pstr >= 'a' && *pstr <= 'z') *pstr = *pstr - 'a' + 10 + '0';
else if(*pstr >= 'A' && *pstr <= 'Z') *pstr = *pstr - 'A' + 10 + '0';
res = res*16 + *pstr - '0';
++pstr;
}
return res;
}

int
mon_showmappings(int argc, char **argv, struct Trapframe *tf)
{
if(argc != 3) {
cprintf("Make sure the correct style: showmappings 0xbegin_addr 0xend_addr\n");
return 0;
}
//将地址字符串转换int,然后判断低12位是不是0
uint32_t begin = jz_xtoi(argv[1]), end = jz_xtoi(argv[2]);
if((begin & 0xfff) != 0 || (end & 0xfff) != 0 ){
cprintf("Make sure the addr's low 12 bits is zero\n");
return 0;
}
cprintf("Attention! You may test addr above UPAGES(0xef000000)\n");
cprintf("begin:%p, end:%p\n",begin,end);
pde_t *kpgdir = KADDR(rcr3()), *pde;
pte_t *pte, *p;
uint32_t va;
for (va = begin; va <= end; va += PGSIZE)
{
//or you can use pgdir_walk
pde = &kpgdir[PDX(va)];
if (*pde & PTE_P){
pte = (pte_t*) KADDR(PTE_ADDR(*pde));
if (*pte & PTE_P){
p = &pte[PTX(va)];
cprintf("va: %p, pa: %p, PTE_P: %x, PTE_W: %x, PTE_U: %x\n",
va, *p, *p&PTE_P, *p&PTE_W, *p&PTE_U);
} else {
cprintf("page mapping not exist: %x\n", va);
}
} else {
cprintf("page mapping not exist: %x\n", va);
}
}
return 0;
}

结果如下:

2、使用page_walk()函数实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
int 
mon_setpermissions(int argc, char **argv, struct Trapframe *tf)
{
if(argc != 4) {
cprintf("Make sure the correct style: setpermissions 0xaddr [0|1 :clear or set] [P|W|U]\n");
return 0;
}
//将地址字符串转换int,然后判断低12位是不是0
uint32_t va = jz_xtoi(argv[1]);
if((va & 0xfff) != 0 ){
cprintf("Make sure the addr's low 12 bits is zero\n");
return 0;
}

pte_t *pte = pgdir_walk((pde_t *)KADDR(rcr3()),(void *)va,false);
if (pte && (*pte & PTE_P)){
cprintf("before setpermissions %p\n",va);
cprintf("va: %p, pa: %p, PTE_P: %x, PTE_W: %x, PTE_U: %x\n",
va, *pte, *pte&PTE_P, *pte&PTE_W, *pte&PTE_U);
uint32_t perm = 0;
if (argv[3][0] == 'P') perm = PTE_P;
if (argv[3][0] == 'W') perm = PTE_W;
if (argv[3][0] == 'U') perm = PTE_U;
if (argv[2][0] == '0') //clear
*pte = *pte & ~perm;
else //set
*pte = *pte | perm;
cprintf("after setpermissions %p\n",va);
cprintf("va: %p, pa: %p, PTE_P: %x, PTE_W: %x, PTE_U: %x\n",
va, *pte, *pte&PTE_P, *pte&PTE_W, *pte&PTE_U);
} else {
cprintf("page mapping not exist: %x\n", va);
}
return 0;
}

结果如下:

3、代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int 
mon_dumpcontents(int argc, char **argv, struct Trapframe *tf)
{
if(argc != 4) {
cprintf("Make sure the correct style: dumpcontents [p|v :physical or virtual] 0x3000 10\n");
return 0;
}
void** begin = NULL;
long length = strtol(argv[3],0,0);
uint32_t i;
if (argv[1][0] == 'p') {
begin = (void**)(jz_xtoi(argv[2]) + KERNBASE);
} else if (argv[1][0] == 'v') {
begin = (void**)(jz_xtoi(argv[2]));
}
if(begin > begin + length){
cprintf("out of memory.\n");
return 0;
}
for (i = 0; i < length; ++i){
cprintf("va at %x is %x\n", begin+i, begin[i]);
}
return 0;
}

关于指针的指针可以参考C语言指针部分的博客。上述设计思想是:指针所加内容是指针存储的地址,所加范围是由指针指向的对象类型决定。因此,二级指针begin+i所加内容是二级指针存储的一级指针地址,所加范围是由二级指针所指向的一级指针决定,即每次加4个字节。begin[i]*(begin+i),即一级指针的内容。因此,这里是将内存内容当作一级指针内容看待!

Changeling3

操作系统一般会映射内核到低线性地址上,留下高地址部分给用户程序。x86内核由于存在向后兼容的虚拟8086模式,处理器会“硬连线式”使用线性空间的低地址部分,所以即使内核映射在那里也无法使用所有映射地址。但是,可以通过设计内核,不再为内核保存任何固定的线性地址部分,允许用户级进程整个无限制使用4GB的虚拟地址空间,同时还从这些进程中充分保护内核,和保护相互不同的进程。请写出满足上述要求的内核设计大纲(相关技术可称为follow the bouncing kernel),明确指出处理器在内核和用户模式之间转换会发生什么,描述内核会如何访问物理内存和IO设备,以及如何通过系统调用访问用户环境的虚拟地址空间。最后从灵活性、性能、内核的复杂性等阐述该方案的优缺点**

Changeling4

jos系统是从页粒度上分配和释放内存,但没有通用的malloc/free工具。如果我们要支持某些类型的I/O设备,它们需要物理上大于4KB的连续缓冲区,或者如果我们为了最大的处理器效率,需要在用户级环境(不仅仅是内核)上分配和映射4MB的superpages,那么将会产生问题。请概括出一个内核内存分配系统,支持管理2的幂大小的页,分配单元大小从4KB至一个合理大小值。确保存在将大分配单元按需求切分为小单元和将小单元合并为大单元的方案。**

显示 Gitment 评论