# 一、 实验目的
了解 MIPS 4Kc 的访存流程与内存映射布局
掌握与实现物理内存的管理方法(链表法)
掌握与实现虚拟内存的管理方法(两级页表)
掌握 TLB 清除与重填的流程
# 二、 访存流程
# 虚拟地址到物理地址的映射
# Exercise 2.1
补全 mips_detect_memory
函数
void mips_detect_memory(u_int _memsize) | |
{ | |
/* Step 1: Initialize memsize. */ | |
memsize = _memsize; | |
/* Step 2: Calculate the corresponding 'npage' value. */ | |
/* Exercise 2.1: Your code here. */ | |
npage = memsize / PAGE_SIZE; | |
// finished | |
printk("Memory size: %lu KiB, number of pages: %lu\n", memsize / 1024, npage); | |
} |
每页的大小为 4KB,这里用到了宏 PAGE_SIZE
,其定义位于 ./include/mmh.h
中:
#define PAGE_SIZE 4096 |
# alloc 函数
static void *alloc(u_int n, u_int align, int clear); |
作用是分配 n
字节的空间并返回初始的虚拟地址,同时将地址按 align
字节对齐。若 clear
为真,则将对应内存空间的值清零,否则不清零。函数中的全局变量 freemem
记录了空闲空间的最低地址位,查阅 mmu.h
可知 0x80400000
是 kseg0
中内核栈空间的栈顶, freemem
是从此处开始。
这部分指导书描述的已经很详细。需要掌握以下几个宏的用法:
ROUND(freemem, align)
:返回[ freemem / align ] * align
(将freemem
按align
向上对齐)ROUNDDOWN(freemem, align)
:返回[ freemem / align ] * align
(将freemem
按align
向下对齐)PADDR(va)
:返回虚拟地址va
所对应的物理地址KADDR(x)
:返回物理地址x
所对应的虚拟地址
# 三、物理内存管理
# 链表宏
链表宏的定义位于 include/queue.h
,其实现了 双向链表 功能。注意和下面这种双向链表区分,我们结点的 le_next
指向后继结点,而 le_prev
指向前驱结点的 le_next
# Exercise 2.2
After
仿照 Before
实现即可,需要注意判断 listelm
的后继节点是否存在
#define LIST_INSERT_BEFORE(listelm, elm, field) \ | |
do \ | |
{ \ | |
(elm)->field.le_prev = (listelm)->field.le_prev; // 将 elm 的前驱指针赋值为 listelm 的前驱指针 \ | |
LIST_NEXT ((elm), field) = (listelm); // 令 elm 的后继指针指向 listelm \ | |
*(listelm)->field.le_prev = (elm); // 令 (listelm 原本的前驱结点) 的后继指针指向 elm \ | |
(listelm)->field.le_prev = &LIST_NEXT ((elm), field); // 令 listelm 的前驱指针指向 elm 的后继指针 \ | |
} while (0) |
#define LIST_INSERT_AFTER(listelm, elm, field) \ | |
/* Exercise 2.2: Your code here. */ \ | |
do \ | |
{ \ | |
LIST_NEXT((elm), field) = (listelm)->field.le_next; // 将 elm 的后继指针赋值为 listelm 的后继指针 \ | |
(elm)->field.le_prev = &((listelm)->field.le_next); // 令 elm 的前驱指针指向 listelm 的后继指针 \ | |
if ((listelm)->field.le_next != NULL) \ | |
{ // 令 (listelm 原本的后继结点) 的前驱指针 指向 elm 的后继指针 \ | |
((listelm)->field.le_next)->field.le_prev = &((elm)->field.le_next); \ | |
} \ | |
(listelm)->field.le_next = elm; // 令 listelm 的后继指针指向 elm \ | |
} while (0) |
# 页控制块
中维护了 npage
个页控制块,也就是 Page
结构体。每一个页控制块对应一页的物理内存, 用这个结构体来按页管理物理内存的分配。 npage
个 Page
和 npage
个物理页面一一顺序对应。具体来说,用一个数组存放这些 Page
结构体,首个 Page
的地址为 P
,则 P[i]
对应从 0
开始计数的第 i
个物理页面。
需要掌握 ./include/pamp.h
中一些函数的用法
page2ppn(struct Page *pp)
: 计算地址为pp
的物理页在全局pages
数组中的页号page2pa(struct Page *pp)
: 根据Page
结构体指针得到对应的物理地址Page *pa2page(u_long pa)
: 根据物理地址得到对应的Page
结构体指针page2kva(struct Page *pp)
: 根据Page
结构体指针得到对应的虚拟地址va2pa(Pde *pgdir, u_long va)
: 给定页目录pgdir
,将 虚拟地址(VA)
转换为 物理地址(PA)
# Exercise 2.3
补全 page_init
函数。其功能是初始化所有物理页 Page
指针,将使用过的页的引用次数置 1,未使用的页加入 page_free_list
void page_init(void) | |
{ | |
/* Step 1: Initialize page_free_list. */ | |
/* Hint: Use macro `LIST_INIT` defined in include/queue.h. */ | |
/* Exercise 2.3: Your code here. (1/4) */ | |
LIST_INIT(&page_free_list); // 注意对 page_free_list 取地址 | |
// finished | |
/* Step 2: Align `freemem` up to multiple of PAGE_SIZE. */ | |
/* Exercise 2.3: Your code here. (2/4) */ | |
freemem = ROUND(freemem, PAGE_SIZE); | |
// finished | |
/* Step 3: Mark all memory below `freemem` as used (set `pp_ref` to 1) */ | |
/* Exercise 2.3: Your code here. (3/4) */ | |
struct Page *p; | |
for (p = pages; page2kva(p) < freemem; p++) | |
{ | |
p->pp_ref = 1; | |
} | |
// finished | |
/* Step 4: Mark the other memory as free. */ | |
/* Exercise 2.3: Your code here. (4/4) */ | |
for (; page2ppn(p) < npage; p++) | |
{ | |
p->pp_ref = 0; | |
LIST_INSERT_HEAD(&page_free_list, p, pp_link); | |
} | |
// finished | |
} |
# Exercise 2.4
补全 page_alloc
函数。其功能是向空闲链表 page_free_list
申请页面
int page_alloc(struct Page **new) | |
{ | |
/* Step 1: Get a page from free memory. If fails, return the error code.*/ | |
struct Page *pp; | |
/* Exercise 2.4: Your code here. (1/2) */ | |
if (LIST_EMPTY(&page_free_list)) | |
{ | |
return -E_NO_MEM; | |
} | |
pp = LIST_FIRST(&page_free_list); | |
// finished | |
LIST_REMOVE(pp, pp_link); | |
/* Step 2: Initialize this page with zero. | |
* Hint: use `memset`. */ | |
/* Exercise 2.4: Your code here. (2/2) */ | |
memset(page2kva(pp), 0, PAGE_SIZE); | |
// finished | |
*new = pp; | |
return 0; | |
} |
# Exercise 2.5
补全 page_free
函数。它的作用是将 pp 指向的页控制块重新插入到 page_free_list
中。此外需要先确保 pp
指向的页控制块对应的物理页面引用次数为 0
void page_free(struct Page *pp) | |
{ | |
assert(pp->pp_ref == 0); | |
/* Just insert it into 'page_free_list'. */ | |
/* Exercise 2.5: Your code here. */ | |
LIST_INSERT_HEAD(&page_free_list, pp, pp_link); | |
return; | |
} |
# 四、虚拟内存管理
前面已经提到过,MOS 中用 PADDR
与 KADDR
这两个宏可以对位于 kseg0
的虚拟地址和对应的物理地址进行转换。
但是,对于位于 kuseg
的虚拟地址,MOS 中采用两级页表结构对其进行地址转换。
第一级表称为页目录 (Page Directory),第二级表称为页表 (Page Table)。为避免歧义,下面用 一级页表指代 Page Directory,二级页表指代 Page Table。
由于一个页表项可以恰好由一个 32 位整型来表示,因此可以使用 Pde
来表示一级页表项类型,用 Pte
来表示二级页表项类型,这两者的本质都是 u_long
类型,它们的类型别名定义位于 include/mmu.h
。
例如,设 pgdir
是一个 Pde *
类型的指针,表示一个一级页表的基地址,那么使用 pgdir\+ i
即可得到偏移量为 i
的一级页表项(页目录项)地址
需要掌握以下几个宏的用法,其定义位于 ./include/mmu.h
中:
PDX(va)
: 获取虚拟地址va
的 31-22 位 页目录索引(一级页表项偏移量)PTX(va)
: 获取虚拟地址va
的 21-12 位 页表索引(二级页表项偏移量)PTE_ADDR(pte)
: 从页表项pte
中提取 高 20 位 物理页号PTE_FLAGS(pte)
: 从页表项pte
中提取 低 12 位 权限位PPN(pa)
: 从物理地址pa
中提取 物理页号VPN(va)
: 从虚拟地址pa
中提取 虚拟页号
# 二级页表检索函数
static int pgdir_walk(Pde *pgdir, u_long va, int create, Pte **ppte); |
该函数的作用是:给定一级页表的基址,给定目标虚拟地址,将对应的 指向二级页表项的 指针赋值给 *ppte
。(没有则可创建)
注意,这里可能会在页目录项无效且 create
为真时,使用 page_alloc
创建一个页表,此时应维护申请得到的物理页的 pp_ref
字段。
# Exercise 2.6
static int pgdir_walk(Pde *pgdir, u_long va, int create, Pte **ppte) | |
{ | |
Pde *pgdir_entryp; | |
struct Page *pp; | |
/* Step 1: 计算页目录项(一级页表项)地址 */ | |
/* Exercise 2.6: Your code here. (1/3) */ | |
pgdir_entryp = pgdir + PDX(va); | |
// finished | |
/* Step 2: If the corresponding page table is not existent (valid) then: | |
* * If parameter `create` is set, create one. Set the permission bits 'PTE_C_CACHEABLE | | |
* PTE_V' for this new page in the page directory. If failed to allocate a new page (out | |
* of memory), return the error. | |
* * Otherwise, assign NULL to '*ppte' and return 0. | |
*/ | |
/* Exercise 2.6: Your code here. (2/3) */ | |
if ((*pgdir_entryp & PTE_V) == 0) // 也可以直接写 if (PTE_V)。PTE_V 的定义位于,/include/mmh.h 中。 | |
{ | |
if (create) | |
{ | |
if (page_alloc(&pp) != 0) // 给物理页分配空间。这个代码块也可以直接用 try (page_alloc (&pp)) 替代 | |
{ | |
return -E_NO_MEM; | |
} | |
*pgdir_entryp = page2pa(pp); // 获取物理页指针对应的物理地址 | |
*pgdir_entryp = *pgdir_entryp | PTE_C_CACHEABLE | PTE_V; // 权限赋值 | |
pp->pp_ref++; // 物理页的引用 +1 | |
} | |
else | |
{ | |
*ppte = NULL; | |
return 0; | |
} | |
} | |
// fininshed | |
/* Step 3: Assign the kernel virtual address of the page table entry to '*ppte'. */ | |
/* Exercise 2.6: Your code here. (3/3) */ | |
// 下面这条语句很复杂,用于获取二级页表项的地址。其整体逻辑如下: | |
// 从页目录项 (*pgdir_entryp) 中提取二级页表的物理页号 (PTE_ADDR) | |
// 将物理页号转换为内核虚拟地址 (KADDR),即二级页表的虚拟基地址 | |
// 加上二级页表项偏移量,找到 va 对应的二级页表项地址 | |
*ppte = (Pte *)KADDR(PTE_ADDR(*pgdir_entryp)) + PTX(va); | |
return 0; | |
} |
# 取消地址映射的函数
void page_remove(Pde *pgdir, u_int asid, u_long va) |
作用是删除一级页表基地址 pgdir
对应的两级页表结构中虚拟地址 va
对物理地址的映射。如果存在这样的映射,那么对应物理页面的引用次数会减少一次
# 增加地址映射函数
int page_insert(Pde *pgdir, u_int asid, struct Page *pp, u_long va, u_int perm); |
作用是将一级页表基地址 pgdir
对应的两级页表结构中虚拟地址 va
映射到页控制块 pp
对应的物理页面,并将页表项权限为设置为 perm
。其中参数 asid
是进程的地址空间标识符,用于区分不同进程的 TLB 条目(防止不同进程的相同虚拟地址冲突),在本次实验中无需理会。
需要了解以下函数的用法:
tlb_invalidate(asid, va)
: 用于 显式失效TLB
中特定虚拟地址va
的缓存项,保证下次访问相应虚拟地址va
时一定触发TLB
重填,进而保证访存的正确性。
# Exercise 2.7
int page_insert(Pde *pgdir, u_int asid, struct Page *pp, u_long va, u_int perm) | |
{ | |
Pte *pte; | |
// 检查 va 是否已经存在了映射(只检查不创建) | |
pgdir_walk(pgdir, va, 0, &pte); | |
// 如果已经存在了映射,检查映射是否有效 | |
if (pte && (*pte & PTE_V)) | |
{ | |
// 如果映射有效,检查其映射的物理页和将插入的物理页是否一致 | |
if (pa2page(*pte) != pp) | |
{ | |
// 如果不一致,则删除原映射,后续再插入新页 | |
page_remove(pgdir, asid, va); | |
} | |
else | |
{ | |
// 如果一致,则只需更新权限映射位 | |
// 但先要将 TLB 中缓存的页表项删掉,然后更新内存中的页表项。 | |
// 这样下次加载 va 所在页时,TLB 会重新从页表中加载这一页表项。 | |
// 插入完成后函数立即返回。 | |
tlb_invalidate(asid, va); | |
*pte = page2pa(pp) | perm | PTE_C_CACHEABLE | PTE_V; | |
return 0; | |
} | |
} | |
/* Step 2: Flush TLB with 'tlb_invalidate'. */ | |
/* Exercise 2.7: Your code here. (1/3) */ | |
// 将 TLB 中缓存的页表项删掉 | |
tlb_invalidate(asid, va); | |
/* Step 3: Re-get or create the page table entry. */ | |
/* If failed to create, return the error. */ | |
/* Exercise 2.7: Your code here. (2/3) */ | |
// 检索二级页表项,没有则创建 | |
try(pgdir_walk(pgdir, va, 1, &pte)); | |
/* Step 4: Insert the page to the page table entry with 'perm | PTE_C_CACHEABLE | PTE_V' | |
* and increase its 'pp_ref'. */ | |
/* Exercise 2.7: Your code here. (3/3) */ | |
// 获得物理地址并设置权限位 | |
*pte = page2pa(pp) | perm | PTE_C_CACHEABLE | PTE_V; | |
pp->pp_ref++; | |
return 0; | |
} |
# 寻找映射的物理地址函数
struct Page * page_lookup(Pde *pgdir, u_long va, Pte **ppte) |
作用是返回一级页表基地址 pgdir
对应的两级页表结构中虚拟地址 va
映射的物理页面的页控制块,同时将 ppte
指向的空间设为对应的二级页表项地址,如果未找到则设为 NULL
# 五、访问内存与 TLB 重填
# TLB 组成与 CP0 寄存器
与内存管理相关的 CP0 寄存器:
寄存器序号 | 寄存器名 | 用途 |
---|---|---|
8 | BadVaddr | 保存引发地址异常的虚拟地址 |
10、2 | EntryHi、EntryLo | Hi 记录了 TLB 的高 32 位,Lo 记录了 TLB 的低 32 位,所有读写 TLB 的操作都要通过这两个寄存器 |
0 | Index | 决定索引号为某个值的 TLB 表项被读或者写 |
1 | Random | 提供一个随机的索引号用于 TLB 的读写 |
每个 TLB
表项都有两个组成部分,包括一组 Key
和两组 Data
。 TLB
采用奇偶页的设计,即使用 VPN
中的高 19
位与 ASID
作为
Key
,一次查找到两个 Data
(一对相邻页面的两个页表项),并用 VPN
中的最低 1
位在两个 Data
中选择命中的结果。因此在对 TLB
进行维护(无效化、重填)时,除了维护目标页面,同时还需要维护目标页面的邻居页面
EntryHi
、 EntryLo
都是 CP0 中的寄存器,他们只是分别对应到 TLB 的 Key 与两组 Data,并不是 TLB 本身。
其中 EntryLo0
、 EntryLo1
拥有完全相同的位结构, EntryLo0
存储 Key
对应的偶页,而 EntryLo1
存储 Key
对应的奇页。
软件必须经过 CP0
与 TLB
交互,因此软件操作 TLB
的流程总是分为两步:
- 填写 CP0 寄存器
- 使用 TLB 相关指令
# TLB 相关指令
指令 | 作用 |
---|---|
tlbr | 读 TLB 。以 Index 寄存器中的值为索引,读出 TLB 中对应的表项到 EntryHi 与 EntryLo 。 |
tlbwi | 写 TLB 。以 Index 寄存器中的值为索引,将此时 EntryHi 与 EntryLo 的值写到索引指定的 TLB 表项中。 |
tlbwr | 随机写 TLB 。将 EntryHi 与 EntryLo 的数据随机写到一个 TLB 表项中。 |
tlbp | 查 TLB 。用根据 EntryHi 中的 Key (包含 VPN 与 ASID ),查找 TLB 中与之对应的表项。如果命中,并将表项的索引存入 Index 寄存器。若未找到匹配项,则 Index 最高位被置 1 |
以上指令直接使用即可,不需要操作数
# TLB 维护流程
访问需要经过转换的虚拟内存地址时,首先要使用 Key
(虚拟页号和当前进程的 ASID
) 在 TLB
中查询该地址对应的物理页号
- 如果在
TLB
中存在对应的TLB
表项时,则可取得物理地址; - 如果不能查询到,则产生
TLB Miss
异常,系统跳转到异常处理程序,在内核的两级页表结构中找到对应的物理地址,对TLB
进行重填。
如果 TLB
中已暂存了页表中某一虚拟地址对应的页表项内容,之后操作系统更新了该页表项,但没有更新 TLB
,则访问该虚拟地址时实际可能会访问到错误的物理页面。所以我们需要维护 TLB
的表项,使得当 TLB
能够查询到虚拟地址相应的页号时,取得的物理页号和权限信息与实际在内核页表中对应的数据一致。
具体来说,维护 TLB
的流程如下:
- 更新页表中虚拟地址对应的页表项的同时,将
TLB
中对应的旧表项无效化 - 在下一次访问该虚拟地址时,硬件会触发
TLB
重填异常,此时操作系统对TLB
进行重填
# Exercise 2.8
LEAF(tlb_out) | |
.set noreorder | |
// 保存 EntryHi 的原值 | |
mfc0 t0, CP0_ENTRYHI | |
// 将 Key 写入 EntryHi | |
mtc0 a0, CP0_ENTRYHI | |
nop | |
// 使用 tlbp 查找对应旧表项,并将索引存入 Index | |
/* Exercise 2.8: Your code here. (1/2) */ | |
tlbp | |
nop | |
/* Step 2: Fetch the probe result from CP0.Index */ | |
mfc0 t1, CP0_INDEX | |
.set reorder | |
bltz t1, NO_SUCH_ENTRY | |
.set noreorder | |
// 将三个 CP0 寄存器清零 | |
mtc0 zero, CP0_ENTRYHI | |
mtc0 zero, CP0_ENTRYLO0 | |
mtc0 zero, CP0_ENTRYLO1 | |
nop | |
// 使用 tlbwi 将旧表项清零,使其无效化 | |
/* Exercise 2.8: Your code here. (2/2) */ | |
tlbwi | |
.set reorder | |
NO_SUCH_ENTRY: | |
// 恢复 EntryHi 的值 | |
mtc0 t0, CP0_ENTRYHI | |
j ra | |
END(tlb_out) |
# TLB 重填流程
从
BadVAddr
中取出引发TLB
缺失的虚拟地址。从
EntryHi
的0 – 7
位取出当前进程的ASID
。先在栈上为返回地址、待填入
TLB
的页表项以及函数参数传递预留空间,并存入返回地址。以存储奇偶页表项的地址、触发异常的虚拟地址和 ASID 为参数,调用_do_tlb_refill
函数。该函数是 TLB 重填过程的核心,其功能是根据虚拟地址和ASID
查找页表,将对应的奇偶页表项写回其第一个参数所指定的地址。将页表项存入
EntryLo0
、EntryLo1
,并执行tlbwr
将此时的EntryHi
与EntryLo0
、EntryLo1
写入到TLB
中(在发生TLB
缺失时,EntryHi
已经由硬件写入了虚拟页号等信息,无需修改)
# Exercise 2.9
void _do_tlb_refill(u_long *pentrylo, u_int va, u_int asid) | |
{ | |
tlb_invalidate(asid, va); | |
Pte *ppte; | |
/* 提示: | |
尝试在循环中调用 'page_lookup' 以查找虚拟地址 va | |
在当前进程页表中对应的页表项 '*ppte' | |
如果 'page_lookup' 返回 'NULL',表明 '*ppte' 找不到,使用 'passive_alloc' | |
为 va 所在的虚拟页面分配物理页面, | |
直至 'page_lookup' 返回不为 'NULL' 则退出循环。 | |
你可以在调用函数时,使用全局变量 cur_pgdir 作为其中一个实参。其中存储了当前进程一级页表基地址位于 kseg0 的虚拟地址 | |
*/ | |
/* Exercise 2.9: Your code here. */ | |
while (page_lookup(cur_pgdir, va, &ppte) == NULL) | |
{ | |
passive_alloc(va, cur_pgdir, asid); | |
} | |
ppte = (Pte *)((u_long)ppte & ~0x7); | |
pentrylo[0] = ppte[0] >> 6; | |
pentrylo[1] = ppte[1] >> 6; | |
} |
这里为什么使用 while
语句而非 if
呢?主要是出于对操作系统内存管理可靠性和并发安全性的严格考量
# Exercise 2.10
do_tlb_refill_call: | |
addi sp, sp, -24 /* Allocate stack for arguments(3), return value(2), and return address(1) */ | |
sw ra, 20(sp) /* [sp + 20] - [sp + 23] store the return address */ | |
addi a0, sp, 12 /* [sp + 12] - [sp + 19] store the return value */ | |
jal _do_tlb_refill /* (Pte *, u_int, u_int) [sp + 0] - [sp + 11] reserved for 3 args */ | |
lw a0, 12(sp) /* Return value 0 - Even page table entry */ | |
lw a1, 16(sp) /* Return value 1 - Odd page table entry */ | |
lw ra, 20(sp) /* Return address */ | |
addi sp, sp, 24 /* Deallocate stack */ | |
mtc0 a0, CP0_ENTRYLO0 /* Even page table entry */ | |
mtc0 a1, CP0_ENTRYLO1 /* Odd page table entry */ | |
nop | |
/* Hint: use 'tlbwr' to write CP0.EntryHi/Lo into a random tlb entry. */ | |
/* Exercise 2.10: Your code here. */ | |
tlbwr | |
jr ra | |
END(do_tlb_refill) |
# 六、思考题
# Thinking 2.1
在编写的 C 程序中,指针变量中存储的地址被视为虚拟地址,还是物理地址?MIPS 汇编程序中 lw 和 sw 指令使用的地址被视为虚
拟地址,还是物理地址?
均为虚拟地址
# Thinking 2.2
从可重用性的角度,阐述用宏来实现链表的好处。
宏将一段代码封装成一条语句,具有可重用性。其相比函数更具有类型无关性,而且没有额外的栈的开销。
查看实验环境中的 /usr/include/sys/queue.h,了解其中单向链表与循环链表的实现,比较它们与本实验中使用的双向链表,分析三者在插入与删除操作上的性能差异。
链表 头部插入 尾部插入 指定结点插入 头部删除 尾部删除 指定结点删除 单向链表 O(1) O(n) O(n) O(1) O(n) O(n) 双向链表 O(1) O(n) O(1) O(1) O(n) O(1) 循环链表 O(1) O(1) O(n) O(1) O(1) O(n)
# Thinking 2.3
请阅读 include/queue.h 以及 include/pmap.h, 将 Page_list 的结构梳理清楚,选择正确的展开结构。
struct Page_list{ struct { struct { struct Page *le_next; struct Page **le_prev; } pp_link;u_short pp_ref;
}* lh_first;}
# Thinking 2.4
- 请阅读上面有关 TLB 的描述,从虚拟内存和多进程操作系统的实现角度,阐述 ASID 的必要性。
在多进程系统中,每个进程拥有独立的地址空间。同一虚拟地址在不同地址空间中通常映射到不同物理地址,通过 ASID 可以判断是在哪个地址空间。
- 请阅读 MIPS 4Kc 文档《MIPS32® 4K™ Processor Core Family Software User’sManual》的 Section 3.3.1 与 Section 3.4,结合 ASID 段的位数,说明 4Kc 中可容纳不同的地址空间的最大数量。
64 个。
Since the ASID is only 6 bits long, OS software does have to lend a hand if there are ever more than 64 address spaces in concurrent use; but it probably won’t happen too often.
# Thinking 2.5
tlb_invalidate 和 tlb_out 的调用关系?
tlb_invalidate
调用tlb_out
请用一句话概括 tlb_invalidate 的作用。
用于 显式失效
TLB
中特定虚拟地址va
的缓存项,保证下次访问相应虚拟地址va
时一定触发TLB
重填,进而保证访存的正确性逐行解释 tlb_out 中的汇编代码。
见上文 Exercise 2.8
# Thinking 2.6
请结合 Lab2 开始的 CPU 访存流程与下图中的 Lab2 用户函数部分,尝试将函数调用与 CPU 访存流程对应起来,思考函数调用与 CPU 访存流程的关系
函数调用与 CPU 访存流程紧密相关:当调用函数时,CPU 通过虚拟地址访问指令和数据,若 TLB 未缓存该地址映射则触发异常,由操作系统查询页表完成地址转换;函数内的栈操作、全局变量访问同样依赖这一机制,而返回地址的跳转再次引发指令获取流程
# Thinking 2.6
简单了解并叙述 X86 体系结构中的内存管理机制,比较 X86 和 MIPS 在内存管理上的区别。
特性 x86 MIPS 地址转换机制 分段(可选) + 分页(主流) 纯分页 页表管理 硬件自动遍历多级页表 软件处理 TLB 缺失(通过异常) TLB 管理 硬件自动更新,支持 PCID 软件显式控制( tlbwi
/tlbwr
)进程隔离标识 PCID(12 位,现代 x86) ASID(8 位) 缺页处理 硬件触发 #PF
,内核处理类似,但需软件查询页表 典型应用场景 通用计算(PC / 服务器) 嵌入式 / 实时系统
# 七、参考资料
南风北辰 - 博客园
重结晶 - 博客园
NothingToSay - 知乎