Lab 2: Memory management
做这个实验之前首先需要知道什么是分页。分段在这个实验里面没用到过。
前面是一大堆教你怎么获取lab2资源的,我不知道怎么弄,后来乱搞了一下,就把lab1的覆盖掉了,变成了lab2。这个我相信就我不懂。
Part 1: Physical Page Management
第一个是物理页面管理。1
2
3
4
5boot_alloc() //这个是系统加载前做个物理内存分配,也就初始化用了一下
mem_init() // 顾名思义,内存初始化
page_init() //页面初始化
page_alloc() //真正的分配物理页面
page_free() // 页面释放
首先,我们先观察一下init.c
文件,这个是内核初始化调用的,上个实验已经清楚了。
init.c
1 | void |
看 mem_init()
我们先看看kern/pmap.h
和inc/memlayout.h
里面有什么,没看懂就算了,我也没看懂,大致知道有些啥就行了。补充一个inc/mmu.h
不用知道具体实现,但是一些东西后面用的超多。
mmu.h
1 |
|
memlayout.h
1 |
|
kern/pmap.h1
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/* See COPYRIGHT for copyright information. */
//这个几个扩展变量范围到了具体定义再说
extern char bootstacktop[], bootstack[];
extern struct PageInfo *pages;
extern size_t npages;
extern pde_t *kern_pgdir;
/* This macro takes a kernel virtual address -- an address that points above
* KERNBASE, where the machine's maximum 256MB of physical memory is mapped --
* and returns the corresponding physical address. It panics if you pass it a
* non-kernel virtual address. 将虚拟地址转换成物理地址
*/
static inline physaddr_t
_paddr(const char *file, int line, void *kva)
{//具体分析不过来告辞
if ((uint32_t)kva < KERNBASE)
_panic(file, line, "PADDR called with invalid kva %08lx", kva);
return (physaddr_t)kva - KERNBASE;
}
/* This macro takes a physical address and returns the corresponding kernel
* virtual address. It panics if you pass an invalid physical address. */
//这个是物理地址转换成虚拟地址
static inline void*
_kaddr(const char *file, int line, physaddr_t pa)
{
if (PGNUM(pa) >= npages)
_panic(file, line, "KADDR called with invalid pa %08lx", pa);
return (void *)(pa + KERNBASE);
}
enum {
// For page_alloc, zero the returned physical page.
ALLOC_ZERO = 1<<0,
};
// 后面就是几个函数的声明,后面会看到的
void mem_init(void);
void page_init(void);
struct PageInfo *page_alloc(int alloc_flags);
void page_free(struct PageInfo *pp);
int page_insert(pde_t *pgdir, struct PageInfo *pp, void *va, int perm);
void page_remove(pde_t *pgdir, void *va);
struct PageInfo *page_lookup(pde_t *pgdir, void *va, pte_t **pte_store);
void page_decref(struct PageInfo *pp);
void tlb_invalidate(pde_t *pgdir, void *va);
static inline physaddr_t
page2pa(struct PageInfo *pp)
{ //将 PagaInfo 转换成真正的物理地址
return (pp - pages) << PGSHIFT;
}
static inline struct PageInfo*
pa2page(physaddr_t pa)
{ // 或得物理地址的数据结构
if (PGNUM(pa) >= npages)
panic("pa2page called with invalid pa");
return &pages[PGNUM(pa)];
}
static inline void*
page2kva(struct PageInfo *pp)
{ //将页的数据结构转换成虚拟地址
return KADDR(page2pa(pp));
}
pte_t *pgdir_walk(pde_t *pgdir, const void *va, int create);
接下来我们就开始看内存怎么初始化的了。这个时候就要打开kern/pmap.c
,看里面的mem_int()
;
我们一段一段的来。
先看看定义了啥1
2
3
4
5
6
7
8 // These variables are set by i386_detect_memory()
size_t npages; // Amount of physical memory (in pages) 物理内存的页数
static size_t npages_basemem; // Amount of base memory (in pages) basemem的页数
// These variables are set in mem_init() 这几个变量就是原本pmap.h扩展的那几个
pde_t *kern_pgdir; // Kernel's initial page directory 内核初始化页目录
struct PageInfo *pages; // Physical page state array 物理内存页表数组
static struct PageInfo *page_free_list; // Free list of physical pages 空闲页表描述结构体指针
然后我们直接跟着 mem_init()
的过程走。1
2
3
4
5
6 uint32_t cr0; //定义了两个变量,干啥的还不清楚接着走
size_t n;
// Find out how much memory the machine has (npages & npages_basemem).
i386_detect_memory(); //这个是查看有多少个页 还有个页基础内存 这个函数并没有要我们实现的意思就不管他了,看看,也就是帮我们查看有多少内存,不过不知道为啥这个查出来只有 128 M 少了一半。
// Remove this line when you're ready to test this function.
// panic("mem_init: This function is not finished\n"); 这个注释就行了...
后面运行了这个 boot_alloc
作用很明显,就是创建一个页目录。1
2
3
4//////////////////////////////////////////////////////////////////////
// create initial page directory.
kern_pgdir = (pde_t *) boot_alloc(PGSIZE);
memset(kern_pgdir, 0, PGSIZE);
boot_alloc()
1 | // This simple physical memory allocator is used only while JOS is setting |
看代码实现还是挺容易理解的。kern_pgdir = (pde_t *) boot_alloc(PGSIZE)
这句就相当于直接在后面开了一个PGSIZE
大小的作为初始化页目录,然后把他初始化为0了。PGSIZE =4096
的定义是在上一次的实验。也就是4096个字节,所以kern_pgdir
占4096B
一个页表项是4B
,所以总共是1024
个页表。(这个时候我在想们是不是 每个页表也是 1024
个页,一个页 4KB
这样内存就是 102410244KB 就是4G,不知道是不是这样,纯属猜测。)
后面有这么一段1
2
3
4
5
6
7
8//////////////////////////////////////////////////////////////////////
// Recursively insert PD in itself as a page table, to form
// a virtual page table at virtual address UVPT.
// (For now, you don't have understand the greater purpose of the
// following line.)
// Permissions: kernel R, user R
kern_pgdir[PDX(UVPT)] = PADDR(kern_pgdir) | PTE_U | PTE_P;//后面这两个参要看mmu.h
自己本身就是页表,所以把自己插入进去。大家可以试试输出这个几个值看看,再对照前面那个内存。
紧接着 就是分配页了1
2
3
4
5
6
7
8
9
10//////////////////////////////////////////////////////////////////////
// Allocate an array of npages 'struct PageInfo's and store it in 'pages'.
// The kernel uses this array to keep track of physical pages: for
// each physical page, there is a corresponding struct PageInfo in this
// array. 'npages' is the number of physical pages in memory. Use memset
// to initialize all fields of each struct PageInfo to 0.
// Your code goes here:
// 把每个页的结构存下来,放到pages里面,npages 是 页表的个数,知道这些 也就简单了。
pages=(struct PageInfo *) boot_alloc(npages *sizeof(struct PageInfo));
memset(pages,0,npages*sizeof(struct PageInfo));//初始化
大家自行输出 这个空间的大小。如果没错的话,n=32768 ,PageInfo=8
256KB
。这是最后一次使用boot_alloc
,他的作用也就干了两件事,一件事是分配页目录,第二个是为每个页分配数据结构。
接着就运行了page_init()
这个时候你需要知道空闲列表,前面加载内核的时候有一部分内存是不能用的。1
2
3
4
5
6
7
8
9
10
11//////////////////////////////////////////////////////////////////////
// Now that we've allocated the initial kernel data structures, we set
// up the list of free physical pages. Once we've done so, all further
// memory management will go through the page_* functions. In
// particular, we can now map memory using boot_map_region
// or page_insert 我们已经初始化了数据结构现在,需要知道空闲列表,以后使用内存就通过page_*函数,尤其是的是我们可以用 boot_map_region 和page_insert 进行映射。
page_init();
check_page_free_list(1);
check_page_alloc();
check_page();
接下来我们就要实现 page_init()
page_init()
1 | // -------------------------------------------------------------- |
后面就要实现两个函数一个是内存分配page_alloc
,一个是内存释放page_free
。
page_alloc
1 | // |
page_free()
1 | // |
到此 物理内存分配实验全部结束了,总的来说其实就干了三件事:
- 建了了一个页目录,对所有页建了一个数据结构
- 把所有空闲的空间建成了一个空闲链表。
- 提供了一个物理内存,释放一个物理内存
这个是从我第一个资源获取那里面一个大佬那盗过来的。
Part 2: Virtual Memory
首先这个实验让你 先试试水,让你了解下物理地址和虚拟地址的差距。在虚拟内存里面都是连续的空间,转换成了物理地址就是一页一页的了。本来还有分段操作,但是呢这个里面没有用上,给禁用了。
是否记得 那年夏天我们所做过的 Lab 1 part3
用了一个简单的页表,就映射了 4MB,而现在我们要映射256MB。Question 1
肯定是虚拟地址啊。
然后讲了KADDR,PADDR
,前面代码那个啥文件里面有,看一下就可以知道了。
后面又扯了一大堆,看一看了解一下就行了。
然后又继续我的看源码大业了。
这次函数并没有在 mem_init()
里面使用,但是呢写了一些测试的东西。我们就照着实验上来一个个实现函数。1
2
3
4
5pgdir_walk()
boot_map_region()
page_lookup()
page_remove()
page_insert()
这个函数看懂了会受益很大的。
pgdir_walk()
1 | // Given 'pgdir', a pointer to a page directory, pgdir_walk returns |
boot_map_region()
1 | // |
page_lookup()
1 | // |
page_remove()
1 | // |
page_insert()
1 | // |
Permissions and Fault Isolation
现在 就是让你映射内核区域了。
1 | ////////////////////////////////////////////////////////////////////// |
这个也是盗的:
Question:
2. 到目前为止页目录表中已经包含多少有效页目录项?他们都映射到哪里?
3BD号页目录项,指向的是kern_pgdir
3BC号页目录项,指向的是pages数组
3BF号页目录项,指向的是bootstack
3C0~3FF号页目录项,指向的是kernel
3. 如果我们把kernel和user environment放在一个相同的地址空间中。为什么用户程序不同读取,写入内核的内存空间?用什么机制保护内核的地址范围。
用户程序不能去随意修改内核中的代码,数据,否则可能会破坏内核,造成程序崩溃。
正常的操作系统通常采用两个部件来完成对内核地址的保护,一个是通过段机制来实现的,但是JOS中的分段功能并没有实现。二就是通过分页机制来实现,通过把页表项中的 Supervisor/User位置0,那么用户态的代码就不能访问内存中的这个页。
4. 这个操作系统的可以支持的最大数量的物理内存是多大?
由于这个操作系统利用一个大小为4MB的空间UPAGES来存放所有的页的PageInfo结构体信息,每个结构体的大小为8B,所以一共可以存放512K个PageInfo结构体,所以一共可以出现512K个物理页,每个物理页大小为4KB,自然总的物理内存占2GB。
5. 如果现在的物理内存页达到最大个数,那么管理这些内存所需要的额外空间开销有多少?
这里不太明白,参考别的答案是,首先需要存放所有的PageInfo,需要4MB,需要存放页目录表,kern_pgdir,4KB,还需要存放当前的页表,大小为2MB。所以总的开销就是6MB + 4KB。
6. 回顾entry.S文件中,当分页机制开启时,寄存器EIP的值仍旧是一个小的值。在哪个位置代码才开始运行在高于KERNBASE的虚拟地址空间中的?当程序位于开启分页之后到运行在KERNBASE之上这之间的时候,EIP的值是小的值,怎么保证可以把这个值转换为真实物理地址的?
在entry.S文件中有一个指令 jmp *%eax,这个指令要完成跳转,就会重新设置EIP的值,把它设置为寄存器eax中的值,而这个值是大于KERNBASE的,所以就完成了EIP从小的值到大于KERNBASE的值的转换。
在entry_pgdir这个页表中,也把虚拟地址空间[0, 4MB)映射到物理地址空间[0, 4MB)上,所以当访问位于[0, 4MB)之间的虚拟地址时,可以把它们转换为物理地址。
Address Space Layout Alternatives
进程的虚拟地址空间的布局不是只有我们讨论的这种唯一的情况,我们也可以把内核映射到低地址处。但是JOS之所以要这么做,是为了保证x86的向后兼容性。
只要我们能够仔细设计,虽然很难,但是我们也能设计出来一种内核的布局方式,使得进程的地址空间就是从0到4GB,无需为内核预留一部分空间,但是仍然能够保证,用户进程不会破坏操作系统的指令,数据。