虚拟地址到物理地址的映射

软件访问的虚拟地址会先被 MMU 映射到物理地址,然后通过物理地址来访问内存或其他外设。我们在编程过程中使用到的地址均为虚拟地址

  • 若虚拟地址位于 kseg0 段,则将虚拟地址的最高位置零得到物理地址,通过 cache 访存。
  • 若虚拟地址位于 kseg1 段,则将虚拟地址的高三位置零得到物理地址,不通过 cache 访存。
  • 若虚拟地址位于 kuseg 段,则需要通过 TLB 获取物理地址,通过 cache 访存。

MMU 中通过硬件 TLB 来完成地址映射。但是我们需要通过软件来完成 TLB 重填。

物理内存管理

我们使用链表管理空闲页表。C 语言中没有泛型,因此我们需要巧妙地使用宏来实现链表。链表宏的代码位于 include/queue.h 中。实现非常巧妙,建议仔细阅读理解。(其中对链表进行遍历的宏 LIST_FOREACH, TAILQ_FOREACH 在课下不会用到,但是在上机时很有可能会用到,建议理解含义)

pmap.h

这个头文件中定义了许多我们可能会用到的函数。是必读的头文件。这些用于转换的函数都应该记住。阅读这些头文件也会有助于我们理解程序进行地址转换的过程。

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
extern Pde *cur_pgdir; // 当前的页目录基地址

LIST_HEAD(Page_list, Page); // 定义一个存储 Page 的链表
typedef LIST_ENTRY(Page) Page_LIST_entry_t;

struct Page {
Page_LIST_entry_t pp_link; /* free list link */

// Ref is the count of pointers (usually in page table entries)
// to this page. This only holds for pages allocated using
// page_alloc. Pages allocated at boot time using pmap.c's "alloc"
// do not have valid reference count fields.

u_short pp_ref;
};

extern struct Page *pages;
extern struct Page_list page_free_list;

static inline u_long page2ppn(struct Page *pp) {
return pp - pages; // 由页获取物理页框号
}

static inline u_long page2pa(struct Page *pp) {
return page2ppn(pp) << PGSHIFT; // 由页获取物理地址
}

static inline struct Page *pa2page(u_long pa) {
if (PPN(pa) >= npage) {
panic("pa2page called with invalid pa: %x", pa);
}
return &pages[PPN(pa)]; // 由物理地址获取页
}

static inline u_long page2kva(struct Page *pp) {
return KADDR(page2pa(pp)); // 由页获取虚拟地址
}

static inline u_long va2pa(Pde *pgdir, u_long va) {
Pte *p;

pgdir = &pgdir[PDX(va)];
if (!(*pgdir & PTE_V)) {
return ~0;
}
p = (Pte *)KADDR(PTE_ADDR(*pgdir));
if (!(p[PTX(va)] & PTE_V)) {
return ~0;
}
return PTE_ADDR(p[PTX(va)]);
} // 由虚拟地址和页目录基地址获取对应的物理地址

void mips_detect_memory(void);
void mips_vm_init(void);
void mips_init(void);
void page_init(void);
void *alloc(u_int n, u_int align, int clear);

int page_alloc(struct Page **pp);
void page_free(struct Page *pp);
void page_decref(struct Page *pp);
int page_insert(Pde *pgdir, u_int asid, struct Page *pp, u_long va, u_int perm);
struct Page *page_lookup(Pde *pgdir, u_long va, Pte **ppte);
void page_remove(Pde *pgdir, u_int asid, u_long va);
void tlb_invalidate(u_int asid, u_long va);

extern struct Page *pages;

void physical_memory_manage_check(void);
void page_check(void);

pmap.c

本文件中的函数涉及的都是很底层的部分。

  • void *alloc:分配一定的物理内存。仅在建立虚拟内存系统时会被使用。
  • void mips_vm_init:只干了一件事:为二级页表结构分配空间。
  • void page_init:初始化页表管理结构,将空闲页表都插入到 page_free_list 中。
  • int page_alloc:从空闲页表中取出一页并分配。
  • int page_free:释放一页,
  • int pgdir_walk:通过给出的页目录基地址,找到虚拟地址 va 对应的页表项并将其地址赋给 ppte。若不存在页表项且 create1 则为其分配一个页表。
  • int page_insert:将虚拟地址 va 映射到 pp 对应的物理页面。
  • struct Page *page_lookup:寻找虚拟地址 va 映射到的页面。将二级页表项地址赋值给 ppte。返回对应的页控制块。
  • void page_decref:减少页面的引用。若减少后引用数为 0,则释放这一页。
  • void page_remove:取消 va 映射的页面。
  • void tlb_invalidate:使 TLB 中带有 asidva 地址的表项无效。

这些函数还是要自己都读一遍才能理解好。

TLB无效化与重填

在本次实验中可能用到的相关指令:

  • tlbr:以 Index 寄存器中的值为索引,读出 TLB 中对应的表项到 EntryHiEntryLo
  • tlbwi:以 Index 寄存器中的值为索引,将此时 EntryHiEntryLo 的值写到索引指定的 TLB 表项中。
  • tlbwr:将 EntryHiEntryLo 的数据随机写到一个 TLB 表项中。
  • tlbp:根据 EntryHi 中的 Key(包含 VPNASID),查找 TLB 中与之对应的表项,并将表项的索引存入 Index 寄存器(若未找到匹配项,则 Index 最高位被置 1)。

TLB 的无效化是通过 kern/tlb_asm.S 中的 tlb_out 实现的。首先通过 tlbq 指令拿出索引,然后将 CP0_ENTRYHICP0_ENTRYLO0 赋值为零,再通过 tlbwi 写回到指定的 TLB 表项中。

这个函数在 kern/pmap.c 中的 void tlb_invalidate() 被调用。

TLB 的无效化由 kern/tlb_asm.S 中的 do_tlb_refill 完成。这个函数首先取出引发错误的虚拟地址,然后取出进程对应的 asid,然后调用 _do_tlb_refill 函数(该函数位于 kern/tlbex.c 中)。最后将物理地址存入 EntryLo, 并执行 tlbwr 将此时的 EntryHiEntryLo 写入到 TLB 中。