主要姓名:冯成 学号:19021221183 学院:电子工程学院
【嵌牛导读】
本章实验是上一讲的续集,只要是管理物理内存。
首先必须要注意一点的是在lab2和lab1中并没有建立起完整的虚拟机制,但是gdb是采用虚拟地址的,所以会发现部分调试信息时错误的。以lab2为例:可以注意到在lab2中的tools/kernel.ld中有这一行代码. = 0xC0100000;
。这表示kernel的文件的虚拟地址起点在0xC0100000
。而lab2中在kernel_entry中开启了分页机制,开启分页机制后CPU寻址采用虚拟地址,而此时lab2中没有完善虚拟机制,所以不能简单的直接使用gdb进行调试。如下示例。
page_init(void) {
struct e820map *memmap = (struct e820map *)(0x8000 + KERNBASE);
uint64_t maxpa = 0;
cprintf("maxpa = %d, &maxpa =%p \n", maxpa, (void *)&maxpa);
int a=12;
cprintf("a = %d, &a =%p \n", a, (void *)&a);
cprintf("e820map:\n");
int i;
for (i = 0; i < memmap->nr_map; i ++) {
...
}
}
第一次循环中i变量初始化为0,但是当我们在gdb中使用print i时会得到下面结果。(下面是在vscode中的gdb调试结果,可能数值会不一样)。
-exec print i
$2 = -1072667567
为了说明这个问题,在原始代码中增加了两行测试代码,即
int a=12;
cprintf("a = %d, &a =%p \n", a, (void *)&a);
我们在gdb中打印a的地址会得到如下结果,
-exec print &a
$3 = (int *) 0xc0118ec8
-exec print a
$4 = -1072667557
但是程序的输出为
a = 12, &a =0xc0118f44
很明显gdb的结果是错误的,两个地址之间有f44-ec8=7c的偏差。我们可以给gdb中的地址加上这个偏差得到一个真实的地址。
以循环中i的值为例,首先我们在gdb中使用print &i获取到i的地址,如
-exec print &i
$5 = (int *) 0xc0118f20
该地址加上偏移量得到0xc0118f9c。于是真实的i的地址就在0xc0118f20位置。我们验证如下
-exec x/4xb 0xc0118f9c
0xc0118f9c: 0x00 0x00 0x00 0x00
接下来的几次循环中分别为
0x01 0x00 0x00 0x00
0x02 0x00 0x00 0x00
0x03 0x00 0x00 0x00
目前发现问题
1, 为什么不能输出正确的maxpa地址,程序输出结果为
maxpa = 0, &maxpa =0x0
2, 为什么不能调试entry.S中的代码。
但是可以通过
objdump -S -d kernel
来得到运行信息。
6
7 c0100000 <kern_entry>:
8 c0100000: b8 00 a0 11 00 mov $0x11a000,%eax
9 c0100005: 0f 22 d8 mov %eax,%cr3
10 c0100008: 0f 20 c0 mov %cr0,%eax
11 c010000b: 0d 2f 00 05 80 or $0x8005002f,%eax
12 c0100010: 83 e0 f3 and $0xfffffff3,%eax
13 c0100013: 0f 22 c0 mov %eax,%cr0
14 c0100016: 8d 05 1e 00 10 c0 lea 0xc010001e,%eax
15 c010001c: ff e0 jmp *%eax
16
17 c010001e <next>:
18 c010001e: 31 c0 xor %eax,%eax
19 c0100020: a3 00 a0 11 c0 mov %eax,0xc011a000
20 c0100025: bd 00 00 00 00 mov $0x0,%ebp
21 c010002a: bc 00 90 11 c0 mov $0xc0119000,%esp
22 c010002f: e8 02 00 00 00 call c0100036 <kern_init>
ubuntu@VM-0-9-ubuntu:~/os_kernel_lab/labcodes/lab2/bin$ readelf -a kernel
ELF Header:
Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
Class: ELF32
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: EXEC (Executable file)
Machine: Intel 80386
Version: 0x1
Entry point address: 0xc0100000
Start of program headers: 52 (bytes into file)
Start of section headers: 130828 (bytes into file)
Flags: 0x0
Size of this header: 52 (bytes)
Size of program headers: 32 (bytes)
Number of program headers: 3
Size of section headers: 40 (bytes)
Number of section headers: 12
Section header string table index: 11
Section Headers:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
[ 0] NULL 00000000 000000 000000 00 0 0 0
[ 1] .text PROGBITS c0100000 001000 006198 00 AX 0 0 1
[ 2] .rodata PROGBITS c01061a0 0071a0 001350 00 A 0 0 32
[ 3] .stab PROGBITS c01074f0 0084f0 00c8dd 0c A 4 0 4
[ 4] .stabstr STRTAB c0113dcd 014dcd 002b32 00 A 0 0 1
[ 5] .data PROGBITS c0117000 018000 002a36 00 WA 0 0 4096
[ 6] .data.pgdir PROGBITS c011a000 01b000 002000 00 WA 0 0 4096
[ 7] .bss NOBITS c011c000 01d000 000f28 00 WA 0 0 32
[ 8] .comment PROGBITS 00000000 01d000 00002b 01 MS 0 0 1
[ 9] .symtab SYMTAB 00000000 01d02c 001d40 10 10 124 4
[10] .strtab STRTAB 00000000 01ed6c 001148 00 0 0 1
[11] .shstrtab STRTAB 00000000 01feb4 000058 00 0 0 1
lab2 导读:
首先相对于lab1,lab2实现了对物理内存得管理,开启了分页机制,也实现了页分配机制。
在entry.S文件中,主要的多了页机制的初始化,如:
# kernel builtin pgdir
# an initial page directory (Page Directory Table, PDT)
# These page directory table and page table can be reused!
.section .data.pgdir
.align PGSIZE
__boot_pgdir:
.globl __boot_pgdir
# map va 0 ~ 4M to pa 0 ~ 4M (temporary)
# .space a, b 表示将a大小的内存设置为b,如果省略“,b”则设置为0.
.long REALLOC(__boot_pt1) + (PTE_P | PTE_U | PTE_W)
# 从4M到KERNBASE设置为0
.space (KERNBASE >> PGSHIFT >> 10 << 2) - (. - __boot_pgdir) # pad to PDE of KERNBASE
# map va KERNBASE + (0 ~ 4M) to pa 0 ~ 4M
.long REALLOC(__boot_pt1) + (PTE_P | PTE_U | PTE_W)
.space PGSIZE - (. - __boot_pgdir) # pad to PGSIZE
# 实际上就是从KERNBASE+4M到4G设置为0
# 实际上__boot_pgdir就是页目录,__boot_pt1是二级页表。
# 虚拟内存为0 ~ 4M和 KERNBASE + (0 ~ 4M)都使用了同一个二级页目录,即__boot_pt1
# 表明这两个虚拟地址对应同一个物理地址。
.set i, 0
__boot_pt1:
.rept 1024
.long i * PGSIZE + (PTE_P | PTE_W)
.set i, i + 1
.endr
为什么要这样做?
要理解这里就必须明白为什么在kern_entry的第一行是movl $REALLOC(__boot_pgdir), %eax,因为此时并没有开启分页机制,CPU使用的是物理地址,而在编译链接时整个程序被链接在虚拟地址为KERNBASE的偏移地方。所以此时的__boot_pgdir地址(虚拟)是大于KERNBASE,但是实际上__boot_pgdir物理地址可能在0到1GB之间,所以必须使用REALLOC宏来得到真实的__boot_pgdir物理地址。而next中已经是开启了分页机制了,所以此时对__boot_pgdir的访问不需要使用REALLOC宏了。
lab1中我们知道ucore在物理内存0x00100000以上,我们假设,在kern_entry中,eip一直是0x001...。假设执行 movl %eax, %cr0前(开启分页机制前),eip是0x001xxx,执行后为eip是0x001xx1(CPU流水线)。此时的eip应该是下一行,但是因为开起了分页机制,所以我们必须把0x001xx1映射到真实的物理地址,也就是把vm 0-4m映射为pm 0-4m。当通过跳转执行到next后,此时的eip是KERNBASE + 0x001.....。此后所有的访问也都是基于KERNBASE 的,所以第一个页表项就是无用的,就需要清除(next的第一二行代码)。
kern_entry:
# load pa of boot pgdir
movl $REALLOC(__boot_pgdir), %eax
movl %eax, %cr3
# enable paging
movl %cr0, %eax
orl $(CR0_PE | CR0_PG | CR0_AM | CR0_WP | CR0_NE | CR0_TS | CR0_EM | CR0_MP), %eax
andl $~(CR0_TS | CR0_EM), %eax
movl %eax, %cr0
# update eip
# now, eip = 0x1.....
leal next, %eax
# set eip = KERNBASE + 0x1.....
jmp *%eax
next:
# unmap va 0 ~ 4M, it's temporary mapping
xorl %eax, %eax
movl %eax, __boot_pgdir
# set ebp, esp
movl $0x0, %ebp
# the kernel stack region is from bootstack -- bootstacktop,
# the kernel stack size is KSTACKSIZE (8KB)defined in memlayout.h
movl $bootstacktop, %esp
# now kernel stack is ready , call the first C function
call kern_init
首先在kern_init中多了pmm_init函数
该函数主要做了以下操作,根据bios探索的内存,找到一个最大值(小于等于KMEMSIZE),然后把从当前空闲位置(即ucore代码数据段后)到最大值这个范围内的物理地址使用Page结构体描述。然后将这个范围内的有效地址PGSIZE对齐并进行初始化(设置属性),放在空闲链表里面。
相较于lab1,lab中需要完善的代码就是对物理内存得管理。首先可以通过阅读default_pmm.c了解简单的FFMA算法,虽然注释上说需要完善,default_init
, default_init_memmap
, default_alloc_pages
, default_free_pages
.但是实际上这些都已经完善了,需要修改的地方只有一点,就是将list_add中的list_add_after改为list_add_before。因为default_pmm.c 中的default_check函数中有以下代码,为了方便理解,我们删掉了代码中不影响的代码,但是整体表达意思没变:
struct Page *p0 = alloc_pages(5), *p1, *p2;
free_pages(p0 + 2, 3);
p2 = p0 + 1;
free_page(p0);
assert((p0 = alloc_page()) == p2 - 1);
上边代码表明,刚释放一个页,然后再申请一个页,一定是刚才释放的那一页。这就需要分配器从列表的首部获取空闲Page,每次将释放的空闲页表也放到首部,所以我们释放空闲链表需要使用list_add_before而不是list_add_after。
流程
检查check_pgdir
assert(get_page(boot_pgdir, 0x0, NULL) == NULL);
// 获取la地址对应的Page结构体信息
struct Page *get_page(pde_t *pgdir, uintptr_t la, pte_t **ptep_store)
{
pte_t *ptep = get_pte(pgdir, la, 0);
if (ptep_store != NULL)
{
*ptep_store = ptep;
}
if (ptep != NULL && *ptep & PTE_P)
{
return pte2page(*ptep);
}
return NULL;
}
pte_t * get_pte(pde_t *pgdir, uintptr_t la, bool create)
{
// (1) find page directory entry
// (2) check if entry is not present
// (3) check if creating is needed, then alloc page for page table
// CAUTION: this page is used for page table, not for common data page
// (4) set page reference
// (5) get linear address of page
// (6) clear page content using memset
// (7) set page directory entry's permission
// (8) return page table entry
// 这里的代码,先按自己理解的写,只要逻辑写对,然后运行,看看哪里报错,然后查看错误代码。
// 比如boot_pgdir[hight12]到底存储促你地址还是物理地址
pde_t *pdep = NULL;
uint32_t hight12 = PDX(la); // 保留高12位
uint32_t twopageentry; // 二级页表入口
// NOTE:根据entry.S中代码,一级的目录项都有 (PTE_P | PTE_U | PTE_W)
if (create && !(boot_pgdir[hight12] & PTE_P)) // 不存在并且需要创建新页·
{
// 分配一个页存储一个二级页目录,也就是1024个pte项。
// 32位系统下,1024个pte项,刚好是4k。
struct Page *temp = alloc_page();
if(temp==NULL)
{
return NULL;
}
uint32_t pa = page2pa(temp); // 获取到这个Page的、真实物理地址
set_page_ref(temp, 1);
memset(KADDR(pa), 0, PGSIZE); // 将这个Page的内存区域清空
// 实际上boot_pgdir[hight12]这里也可以存储物理地址,只要约定好就可以了,但是
// check_pgdir 中有一行代码:ptep = &((pte_t *)KADDR(PDE_ADDR(boot_pgdir[0])))[1];
// 这表明boot_pgdir[hight12]存储的是虚拟地址
// boot_pgdir[hight12] = ((uint32_t)KADDR(pa) | (PTE_P | PTE_U | PTE_W));
boot_pgdir[hight12] = pa | (PTE_P | PTE_U | PTE_W);
}
twopageentry = KADDR(PDE_ADDR(boot_pgdir[hight12])); // PED_ADDR 是将低12位清零的
if (boot_pgdir[hight12] & PTE_P)
{
// PTX 保留高22位
pdep = &(((pte_t *)twopageentry)[PTX(la)]);
}
return pdep;
}
struct Page *
alloc_pages(size_t n)
{
struct Page *page = NULL;
bool intr_flag;
// 开关中断
local_intr_save(intr_flag);
{
page = pmm_manager->alloc_pages(n);
}
local_intr_restore(intr_flag);
return page;
}
static struct Page * default_alloc_pages(size_t n) {
assert(n > 0);
if (n > nr_free) {
return NULL;
}
struct Page *page = NULL;
list_entry_t *le = &free_list;
while ((le = list_next(le)) != &free_list) {
struct Page *p = le2page(le, page_link);
if (p->property >= n) { // 找到第一个大于n的空闲块
page = p;
break;
}
}
if (page != NULL) // 将空闲块中剩余的页放在空闲链表里面
{
list_del(&(page->page_link));
if (page->property > n)
{
struct Page *p = page + n;
SetPageProperty(p);
p->property = page->property - n;
list_add(&free_list, &(p->page_link));
}
nr_free -= n;
ClearPageProperty(page);
}
return page;
}
// page_remove_pte - free an Page sturct which is related linear address la
// - and clean(invalidate) pte which is related linear address la
// note: PT is changed, so the TLB need to be invalidate
static inline void
page_remove_pte(pde_t *pgdir, uintptr_t la, pte_t *ptep)
{
//(1) check if this page table entry is present
//(2) find corresponding page to pte
//(3) decrease page reference
//(4) and free this page when page reference reachs 0
//(5) clear second page table entry
//(6) flush tlb
if (ptep != NULL && *ptep & PTE_P)
{
struct Page *page = pte2page(*ptep);
if (page)
{
page_ref_dec(page);
if (page->ref == 0)
{
free_page(page);
}
}
*ptep = 0;
tlb_invalidate(pgdir, la);
}
}
首先必须要注意一点的是在lab2和lab1中并没有建立起完整的虚拟机制,但是gdb是采用虚拟地址的,所以会发现部分调试信息时错误的。以lab2为例:可以注意到在lab2中的tools/kernel.ld中有这一行代码. = 0xC0100000;
。这表示kernel的文件的虚拟地址起点在0xC0100000
。而lab2中在kernel_entry中开启了分页机制,开启分页机制后CPU寻址采用虚拟地址,而此时lab2中没有完善虚拟机制,所以不能简单的直接使用gdb进行调试。如下示例。
page_init(void) {
struct e820map *memmap = (struct e820map *)(0x8000 + KERNBASE);
uint64_t maxpa = 0;
cprintf("maxpa = %d, &maxpa =%p \n", maxpa, (void *)&maxpa);
int a=12;
cprintf("a = %d, &a =%p \n", a, (void *)&a);
cprintf("e820map:\n");
int i;
for (i = 0; i < memmap->nr_map; i ++) {
...
}
}
第一次循环中i变量初始化为0,但是当我们在gdb中使用print i时会得到下面结果。(下面是在vscode中的gdb调试结果,可能数值会不一样)。
-exec print i
$2 = -1072667567
为了说明这个问题,在原始代码中增加了两行测试代码,即
int a=12;
cprintf("a = %d, &a =%p \n", a, (void *)&a);
我们在gdb中打印a的地址会得到如下结果,
-exec print &a
$3 = (int *) 0xc0118ec8
-exec print a
$4 = -1072667557
但是程序的输出为
a = 12, &a =0xc0118f44
很明显gdb的结果是错误的,两个地址之间有f44-ec8=7c的偏差。我们可以给gdb中的地址加上这个偏差得到一个真实的地址。
以循环中i的值为例,首先我们在gdb中使用print &i获取到i的地址,如
-exec print &i
$5 = (int *) 0xc0118f20
该地址加上偏移量得到0xc0118f9c。于是真实的i的地址就在0xc0118f20位置。我们验证如下
-exec x/4xb 0xc0118f9c
0xc0118f9c: 0x00 0x00 0x00 0x00
接下来的几次循环中分别为
0x01 0x00 0x00 0x00
0x02 0x00 0x00 0x00
0x03 0x00 0x00 0x00
目前发现问题
1, 为什么不能输出正确的maxpa地址,程序输出结果为
maxpa = 0, &maxpa =0x0
2, 为什么不能调试entry.S中的代码。
但是可以通过
objdump -S -d kernel
来得到运行信息。
6
7 c0100000 <kern_entry>:
8 c0100000: b8 00 a0 11 00 mov $0x11a000,%eax
9 c0100005: 0f 22 d8 mov %eax,%cr3
10 c0100008: 0f 20 c0 mov %cr0,%eax
11 c010000b: 0d 2f 00 05 80 or $0x8005002f,%eax
12 c0100010: 83 e0 f3 and $0xfffffff3,%eax
13 c0100013: 0f 22 c0 mov %eax,%cr0
14 c0100016: 8d 05 1e 00 10 c0 lea 0xc010001e,%eax
15 c010001c: ff e0 jmp *%eax
16
17 c010001e <next>:
18 c010001e: 31 c0 xor %eax,%eax
19 c0100020: a3 00 a0 11 c0 mov %eax,0xc011a000
20 c0100025: bd 00 00 00 00 mov $0x0,%ebp
21 c010002a: bc 00 90 11 c0 mov $0xc0119000,%esp
22 c010002f: e8 02 00 00 00 call c0100036 <kern_init>
ubuntu@VM-0-9-ubuntu:~/os_kernel_lab/labcodes/lab2/bin$ readelf -a kernel
ELF Header:
Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
Class: ELF32
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: EXEC (Executable file)
Machine: Intel 80386
Version: 0x1
Entry point address: 0xc0100000
Start of program headers: 52 (bytes into file)
Start of section headers: 130828 (bytes into file)
Flags: 0x0
Size of this header: 52 (bytes)
Size of program headers: 32 (bytes)
Number of program headers: 3
Size of section headers: 40 (bytes)
Number of section headers: 12
Section header string table index: 11
Section Headers:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
[ 0] NULL 00000000 000000 000000 00 0 0 0
[ 1] .text PROGBITS c0100000 001000 006198 00 AX 0 0 1
[ 2] .rodata PROGBITS c01061a0 0071a0 001350 00 A 0 0 32
[ 3] .stab PROGBITS c01074f0 0084f0 00c8dd 0c A 4 0 4
[ 4] .stabstr STRTAB c0113dcd 014dcd 002b32 00 A 0 0 1
[ 5] .data PROGBITS c0117000 018000 002a36 00 WA 0 0 4096
[ 6] .data.pgdir PROGBITS c011a000 01b000 002000 00 WA 0 0 4096
[ 7] .bss NOBITS c011c000 01d000 000f28 00 WA 0 0 32
[ 8] .comment PROGBITS 00000000 01d000 00002b 01 MS 0 0 1
[ 9] .symtab SYMTAB 00000000 01d02c 001d40 10 10 124 4
[10] .strtab STRTAB 00000000 01ed6c 001148 00 0 0 1
[11] .shstrtab STRTAB 00000000 01feb4 000058 00 0 0 1
lab2 导读:
首先相对于lab1,lab2实现了对物理内存得管理,开启了分页机制,也实现了页分配机制。
在entry.S文件中,主要的多了页机制的初始化,如:
# kernel builtin pgdir
# an initial page directory (Page Directory Table, PDT)
# These page directory table and page table can be reused!
.section .data.pgdir
.align PGSIZE
__boot_pgdir:
.globl __boot_pgdir
# map va 0 ~ 4M to pa 0 ~ 4M (temporary)
# .space a, b 表示将a大小的内存设置为b,如果省略“,b”则设置为0.
.long REALLOC(__boot_pt1) + (PTE_P | PTE_U | PTE_W)
# 从4M到KERNBASE设置为0
.space (KERNBASE >> PGSHIFT >> 10 << 2) - (. - __boot_pgdir) # pad to PDE of KERNBASE
# map va KERNBASE + (0 ~ 4M) to pa 0 ~ 4M
.long REALLOC(__boot_pt1) + (PTE_P | PTE_U | PTE_W)
.space PGSIZE - (. - __boot_pgdir) # pad to PGSIZE
# 实际上就是从KERNBASE+4M到4G设置为0
# 实际上__boot_pgdir就是页目录,__boot_pt1是二级页表。
# 虚拟内存为0 ~ 4M和 KERNBASE + (0 ~ 4M)都使用了同一个二级页目录,即__boot_pt1
# 表明这两个虚拟地址对应同一个物理地址。
.set i, 0
__boot_pt1:
.rept 1024
.long i * PGSIZE + (PTE_P | PTE_W)
.set i, i + 1
.endr
为什么要这样做?
要理解这里就必须明白为什么在kern_entry的第一行是movl $REALLOC(__boot_pgdir), %eax,因为此时并没有开启分页机制,CPU使用的是物理地址,而在编译链接时整个程序被链接在虚拟地址为KERNBASE的偏移地方。所以此时的__boot_pgdir地址(虚拟)是大于KERNBASE,但是实际上__boot_pgdir物理地址可能在0到1GB之间,所以必须使用REALLOC宏来得到真实的__boot_pgdir物理地址。而next中已经是开启了分页机制了,所以此时对__boot_pgdir的访问不需要使用REALLOC宏了。
lab1中我们知道ucore在物理内存0x00100000以上,我们假设,在kern_entry中,eip一直是0x001...。假设执行 movl %eax, %cr0前(开启分页机制前),eip是0x001xxx,执行后为eip是0x001xx1(CPU流水线)。此时的eip应该是下一行,但是因为开起了分页机制,所以我们必须把0x001xx1映射到真实的物理地址,也就是把vm 0-4m映射为pm 0-4m。当通过跳转执行到next后,此时的eip是KERNBASE + 0x001.....。此后所有的访问也都是基于KERNBASE 的,所以第一个页表项就是无用的,就需要清除(next的第一二行代码)。
kern_entry:
# load pa of boot pgdir
movl $REALLOC(__boot_pgdir), %eax
movl %eax, %cr3
# enable paging
movl %cr0, %eax
orl $(CR0_PE | CR0_PG | CR0_AM | CR0_WP | CR0_NE | CR0_TS | CR0_EM | CR0_MP), %eax
andl $~(CR0_TS | CR0_EM), %eax
movl %eax, %cr0
# update eip
# now, eip = 0x1.....
leal next, %eax
# set eip = KERNBASE + 0x1.....
jmp *%eax
next:
# unmap va 0 ~ 4M, it's temporary mapping
xorl %eax, %eax
movl %eax, __boot_pgdir
# set ebp, esp
movl $0x0, %ebp
# the kernel stack region is from bootstack -- bootstacktop,
# the kernel stack size is KSTACKSIZE (8KB)defined in memlayout.h
movl $bootstacktop, %esp
# now kernel stack is ready , call the first C function
call kern_init
首先在kern_init中多了pmm_init函数
该函数主要做了以下操作,根据bios探索的内存,找到一个最大值(小于等于KMEMSIZE),然后把从当前空闲位置(即ucore代码数据段后)到最大值这个范围内的物理地址使用Page结构体描述。然后将这个范围内的有效地址PGSIZE对齐并进行初始化(设置属性),放在空闲链表里面。
相较于lab1,lab中需要完善的代码就是对物理内存得管理。首先可以通过阅读default_pmm.c了解简单的FFMA算法,虽然注释上说需要完善,default_init
, default_init_memmap
, default_alloc_pages
, default_free_pages
.但是实际上这些都已经完善了,需要修改的地方只有一点,就是将list_add中的list_add_after改为list_add_before。因为default_pmm.c 中的default_check函数中有以下代码,为了方便理解,我们删掉了代码中不影响的代码,但是整体表达意思没变:
struct Page *p0 = alloc_pages(5), *p1, *p2;
free_pages(p0 + 2, 3);
p2 = p0 + 1;
free_page(p0);
assert((p0 = alloc_page()) == p2 - 1);
上边代码表明,刚释放一个页,然后再申请一个页,一定是刚才释放的那一页。这就需要分配器从列表的首部获取空闲Page,每次将释放的空闲页表也放到首部,所以我们释放空闲链表需要使用list_add_before而不是list_add_after。
流程
检查check_pgdir
assert(get_page(boot_pgdir, 0x0, NULL) == NULL);
// 获取la地址对应的Page结构体信息
struct Page *get_page(pde_t *pgdir, uintptr_t la, pte_t **ptep_store)
{
pte_t *ptep = get_pte(pgdir, la, 0);
if (ptep_store != NULL)
{
*ptep_store = ptep;
}
if (ptep != NULL && *ptep & PTE_P)
{
return pte2page(*ptep);
}
return NULL;
}
pte_t * get_pte(pde_t *pgdir, uintptr_t la, bool create)
{
// (1) find page directory entry
// (2) check if entry is not present
// (3) check if creating is needed, then alloc page for page table
// CAUTION: this page is used for page table, not for common data page
// (4) set page reference
// (5) get linear address of page
// (6) clear page content using memset
// (7) set page directory entry's permission
// (8) return page table entry
// 这里的代码,先按自己理解的写,只要逻辑写对,然后运行,看看哪里报错,然后查看错误代码。
// 比如boot_pgdir[hight12]到底存储促你地址还是物理地址
pde_t *pdep = NULL;
uint32_t hight12 = PDX(la); // 保留高12位
uint32_t twopageentry; // 二级页表入口
// NOTE:根据entry.S中代码,一级的目录项都有 (PTE_P | PTE_U | PTE_W)
if (create && !(boot_pgdir[hight12] & PTE_P)) // 不存在并且需要创建新页·
{
// 分配一个页存储一个二级页目录,也就是1024个pte项。
// 32位系统下,1024个pte项,刚好是4k。
struct Page *temp = alloc_page();
if(temp==NULL)
{
return NULL;
}
uint32_t pa = page2pa(temp); // 获取到这个Page的、真实物理地址
set_page_ref(temp, 1);
memset(KADDR(pa), 0, PGSIZE); // 将这个Page的内存区域清空
// 实际上boot_pgdir[hight12]这里也可以存储物理地址,只要约定好就可以了,但是
// check_pgdir 中有一行代码:ptep = &((pte_t *)KADDR(PDE_ADDR(boot_pgdir[0])))[1];
// 这表明boot_pgdir[hight12]存储的是虚拟地址
// boot_pgdir[hight12] = ((uint32_t)KADDR(pa) | (PTE_P | PTE_U | PTE_W));
boot_pgdir[hight12] = pa | (PTE_P | PTE_U | PTE_W);
}
twopageentry = KADDR(PDE_ADDR(boot_pgdir[hight12])); // PED_ADDR 是将低12位清零的
if (boot_pgdir[hight12] & PTE_P)
{
// PTX 保留高22位
pdep = &(((pte_t *)twopageentry)[PTX(la)]);
}
return pdep;
}
struct Page *
alloc_pages(size_t n)
{
struct Page *page = NULL;
bool intr_flag;
// 开关中断
local_intr_save(intr_flag);
{
page = pmm_manager->alloc_pages(n);
}
local_intr_restore(intr_flag);
return page;
}
static struct Page * default_alloc_pages(size_t n) {
assert(n > 0);
if (n > nr_free) {
return NULL;
}
struct Page *page = NULL;
list_entry_t *le = &free_list;
while ((le = list_next(le)) != &free_list) {
struct Page *p = le2page(le, page_link);
if (p->property >= n) { // 找到第一个大于n的空闲块
page = p;
break;
}
}
if (page != NULL) // 将空闲块中剩余的页放在空闲链表里面
{
list_del(&(page->page_link));
if (page->property > n)
{
struct Page *p = page + n;
SetPageProperty(p);
p->property = page->property - n;
list_add(&free_list, &(p->page_link));
}
nr_free -= n;
ClearPageProperty(page);
}
return page;
}
// page_remove_pte - free an Page sturct which is related linear address la
// - and clean(invalidate) pte which is related linear address la
// note: PT is changed, so the TLB need to be invalidate
static inline void
page_remove_pte(pde_t *pgdir, uintptr_t la, pte_t *ptep)
{
//(1) check if this page table entry is present
//(2) find corresponding page to pte
//(3) decrease page reference
//(4) and free this page when page reference reachs 0
//(5) clear second page table entry
//(6) flush tlb
if (ptep != NULL && *ptep & PTE_P)
{
struct Page *page = pte2page(*ptep);
if (page)
{
page_ref_dec(page);
if (page->ref == 0)
{
free_page(page);
}
}
*ptep = 0;
tlb_invalidate(pgdir, la);
}
}