lab2实验报告 思考题 Thinking 2.1 题面 1 请根据上述说明,回答问题:在编写的 C 程序中,指针变量中存储的地址被视为虚拟地址,还是物理地址?MIPS 汇编程序中 lw 和 sw 指令使用的地址被视为虚拟地址,还是物理地址?
回答 答:C程序中指针变量中存储的地址被视为虚拟地址。MIPS汇编程序中lw
和sw
指令使用的地址被视为虚拟地址。
Thinking 2.2 题面 1 2 3 请思考下述两个问题: • 从可重用性的角度,阐述用宏来实现链表的好处。 • 查看实验环境中的 /usr/i nclude/sys/ queue.h,了解其中单向链表与循环链表的实现,比较它们与本实验中使用的双向链表,分析三者在插入与删除操作上的性能差异。
回答
采用宏定义对链表相关操作进行封装,使得相关操作可复用,减少了每次使用重新再书写的工作量,同时还减小了复杂度,使代码可读性更好,便于维护。
先查看/usr/include/sys/queue.h
中单向链表和循环链表的实现: 三者在插入与删除操作上有一定的性能差别: 1). 对于单向链表 ,由于所有节点都只维护了一个指向下一个节点的指针,因此在删除时需要遍历链表找到删除节点的前一个节点,同样的,在给定节点前插入也需要遍历链表得到前一个节点。但是在给定节点后插入则不需要经过遍历,直接插入即可。 2). 对于循环链表 ,其中头结点head维护指向循环链表的头尾指针(cqh_first
和cqh_last
),而头尾节点分别通过cqe_prev
和cqe_next
指针指向head节点再通过相应的指针,完成对于对方的访问(头对尾,尾对头)。因此删除、在一个节点前插入和在一个节点后插入都相对单向链表容易,跟我们实现的双向链表有异曲同工之妙,但是与双向链表不同的是删除和插入操作需要特判头尾节点的情况,也是head起到连接头尾节点的作用导致的 。 3). 对于双向链表 ,除了维护指向第一个节点指针的head节点,其余节点都维护一个指向下一个节点的指针和指向上一个节点的指向下一个节点的指针的指针 ,这样一来删除和在节点后插入时只需要特判是否是最后一个节点即可,而在节点前插入时则需要判断是否是第一个节点(head外的),删除和插入的效率与循环链表 基本一致。
Thinking 2.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 25 26 27 28 29 30 31 32 33 请阅读 include/queue.h 以及 include/pmap.h, 将 Page_list 的结构梳理清楚,选择正确的展开结构。 A:struct Page_list { struct { struct { struct Page * le_next; struct Page * le_prev; } pp_link; u_short p_ref; } * lh_first; } B:struct Page_list { struct { struct { struct Page * le_next; struct Page ** le_prev; } pp_link; u_short p_ref; } lh_first; } C:struct Page_list { struct { struct { struct Page * le_next; struct Page ** le_prev; } pp_link; u_short p_ref; } * lh_first; }
回答 选择C。 首先在include/pmap.h
中有:
1 2 3 4 5 6 7 8 9 10 11 12 13 LIST_HEAD (Page_list, Page);typedef LIST_ENTRY (Page) Page_LIST_entry_t ;struct Page { Page_LIST_entry_t pp_link; u_short pp_ref; };
其中LIST_HEAD()
和LIST_ENTRY()
是queue.h
中的宏:
1 2 3 4 5 6 7 8 9 10 #define LIST_HEAD(name, type) \ struct name { \ struct type *lh_first; \ } #define LIST_ENTRY(type) \ struct { \ struct type *le_next; \ struct type **le_prev; \ }
全部拼接起来就是C:
1 2 3 4 5 6 7 8 9 struct Page_list { struct { struct { struct Page * le_next; struct Page ** le_prev; } pp_link; u_short p_ref; } * lh_first; }
Thinking 2.4 题面 1 2 3 请思考下面两个问题: • 请阅读上面有关 TLB 的描述,从虚拟内存和多进程操作系统的实现角度,阐述 ASID的必要性。 • 请阅读 MIPS 4 Kc 文档《MIPS32® 4 K™ Processor Core Family Software User ’sManual》的 Section 3.3 .1 与 Section 3.4 ,结合 ASID 段的位数,说明 4 Kc 中可容纳不同的地址空间的最大数量。
回答
多进程操作系统中,对于不同的进程,操作系统都会分配一个页表,而每个页表都有自己的虚拟空间,因此同一虚拟地址由于在不同虚拟空间的缘故通常会映射到不同的物理地址上,假若没有ASID
来标识当前虚拟地址是在哪个虚拟空间中 ,很有可能会将该虚拟地址映射到错误的物理地址上去。
由于EntryHi
寄存器的ASID
段有8位,因此,4Kc中可容纳不同的地址空间的最大数量为256个。
Thinking 2.5 题面 1 2 3 4 请回答下述三个问题: • tlb_ invalidate 和 tlb_ out 的调用关系? • 请用一句话概括 tlb_ invalidate 的作用。 • 逐行解释 tlb_ out 中的汇编代码。
回答
tlb_invalidate
函数如下: 1 2 3 void tlb_invalidate (u_int asid, u_long va) { tlb_out((va & ~GENMASK(PGSHIFT, 0 )) | (asid & (NASID - 1 ))); }
其中在该函数中通过传递参数的形式调用了tlb_out
函数。
在页表内容发生改变后及时更新TLB。
tlb_out
函数代码如下: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 LEAF(tlb_out) #函数开头声明 .set noreorder #设定手动设置延迟槽模式 mfc0 t0, CP0_ENTRYHI #将原有的EntryHi中的值读取到t0寄存器中 mtc0 a0, CP0_ENTRYHI #将传入的原有的旧表项的Key传入EntryHi寄存器中 nop #手动延迟 /* Step 1: Use 'tlbp' to probe TLB entry */ /* Exercise 2.8: Your code here. (1/2) */ tlbp #依据旧表项的Key查找TLB将表项索引写入Index寄存器中(未找到则最高位置1,相当于将Index置负) nop #手动延迟 /* Step 2: Fetch the probe result from CP0.Index */ mfc0 t1, CP0_INDEX #获取旧表项的索引 .set reorder #设定编译器自动延迟模式 bltz t1, NO_SUCH_ENTRY #判断索引是否为负,为负就跳到未找到标签,否则继续下列操作 .set noreorder #设定手动延迟模式 mtc0 zero, CP0_ENTRYHI mtc0 zero, CP0_ENTRYLO0 mtc0 zero, CP0_ENTRYLO1 #将EntryHi、EntryLo0、EntryLo1赋值为0 nop /* Step 3: Use 'tlbwi' to write CP0.EntryHi/Lo into TLB at CP0.Index */ /* Exercise 2.8: Your code here. (2/2) */ tlbwi #使用tlbwi指令以Index中值为索引,将上述EntryHi、EntryLo0和EntryLo1写入TLB对应页表项中 .set reorder #设定自动延迟模式 NO_SUCH_ENTRY: #未找到标签 mtc0 t0, CP0_ENTRYHI #复原EntryHi原有的值 j ra #返回函数 END(tlb_out) #函数结束符
tlb_out
函数的主要流程就是先根据传给a0
寄存器的参数查找对应的旧表项,表项索引赋给Index
寄存器,然后再根据Index
寄存器中的索引值进行相应的操作,如果找到了(索引大于等于0),就通过tlbwi
指令将相应索引对应的表项的TLB的Key
和Data
置0;否则,就先把原有的EntryHi
赋给其再退出。
Thinking 2.6 题面 1 请结合 Lab2 开始的 CPU 访存流程与下图中的 Lab2 用户函数部分,尝试将函数调用与 CPU 访存流程对应起来,思考函数调用与 CPU 访存流程的关系。
回答 用户进程运行过程中,访存指令会触发CPU访存,如果查询目标页面在TLB中,则能够正常完成访存;否则,会触发TLB Miss,将跳转到异常处理程序,该程序通过调用tlb_invalidate()
函数使TLB对应的旧表项无效,并且通过do_tlb_refill()
函数重填TLB,其中do_tlb_refill()
函数调用了_do_tlb_refill()
函数,后者是重填的效果的主要执行者,其调用了page_lookup()
函数寻找对应的页表项,并在找不到或者找到的无效的情况下通过调用passive_alloc()
函数其中的page_alloc()
函数和page_insert()
函数分配新的页面,并且建立好虚实地址间的映射关系。最后回写TLB,重新进行访存,完成CPU访存。 因此,多个函数调用组成了一个完整的CPU访存。
Thinking 2.7 题面 1 2 3 4 从下述三个问题中任选其一回答: • 简单了解并叙述 X 86 体系结构中的内存管理机制,比较 X 86 和 MIPS 在内存管理上的区别。 • 简单了解并叙述 RISC-V 中的内存管理机制,比较 RISC-V 与 MIPS 在内存管理上的区别。 • 简单了解并叙述 LoongArch 中的内存管理机制,比较 LoongArch 与 MIPS 在内存管理上的区别。
回答 我选择X86与MIPS的比较。 X86体系结构中的内存管理机制:
虚拟地址,同实验所用机制类似,为每个进程分配独立的虚拟地址空间,通过MMU进行虚地址向实地址的转换。
分页和分段,前者同实验所用机制基本一致,不过X86一般采用多级页表结构;而后者通过用来提供一种保护机制,限制程序访问内存的范围。
内存保护,提供内存保护机制,防止不同进程间的相互访问以及访问越界。 与MIPS的区别:
MIPS页表结构更为简答,X86为多级页表结构。
MIPS通常不采用分段机制。
虚拟内存实现上,具体细节可能不同,比如替换算法的选择。
内存保护上,实现方式不一定一致,比如X86的特权级数量更多,提供更细致的权限控制。
lab2课下 Exercise 2.1 题面 1 2 请参考代码注释,补全 mips_detect_memory 函数。 在实验中,指定了硬件可用内存大小 memsize ,请你用内存大小 memsize 完成总物理页数 npage 的初始化。
回答 根据网站给出的测试点可以推断出其值与memsize的KB值呈差3倍的关系,也就是推断出来每一页占4KB,但是这是为什么呢?貌似在到该页之前指导书并没有明确指出,一页是多少B?(莫名其妙的题目)解密了阅读到后面的mips_vm_init()
函数中有用到一个名为PAGE_SIZE
,几经查找,终于在./include/mmu.h
中找到了其定义,正如其名就是一页的大小(以字节为单位,4096B),因此这里直接用内存大小去除以PAGE_SIZE
就行,虽然但是此前的指导书一点未提及,我怎么会知道有一个PAGE_SIZE的宏已经被定义了。
Exercise 2.2 题面 1 2 完成 include /queue.h 中空缺的函数 LIST_INSERT_AFTER。其功能是将一个元素插入到已有元素之后,可以仿照 LIST_INSERT_BEFORE函数来实现。
回答 先来看看LIST_INSERT_BEFORE
函数:
1 2 3 4 5 6 7 #define LIST_INSERT_BEFORE(listelm, elm, field) \ do { \ (elm)->field.le_prev = (listelm)->field.le_prev; \ LIST_NEXT((elm), field) = (listelm); \ *(listelm)->field.le_prev = (elm); \ (listelm)->field.le_prev = &LIST_NEXT((elm), field); \ } while (0)
关于这里为什么使用do{...}while()
,我认为这里是因为宏定义的缘故,为了只让其中的四行代码只运行一次使用do...while
是再合适不过了。 一行一行分析,第一行将elm的le_prev与listelm保持一致;第二行将elm的le_next指向listelm;第三行将原有链表中listelm上游的节点的le_next(通过对listelm的le_prev解引用获得)指向elm;第四行将listelm的le_prev指向elm的le_next,完成插入。 因此LIST_INSERT_AFTER()
只需依葫芦画瓢即可,不过需要注意的是与前插不同的是,后插的节点很有可能是最后一个,这是需要特殊考虑一下的。
Exercise 2.3 题面 1 2 3 4 5 6 7 完成 page_init 函数。 请按照函数中的注释提示,完成上述操作。此外,这里也给出一些提示: 1. 使用链表初始化宏 LIST_ INIT。2. 将 freemem 按照 PAGE_SIZE 进行对齐(使用 ROUND 宏为 freemem 赋值)。 3. 将 freemem 以下页面对应的页控制块中的 pp_ ref 标为 1。4. 将其它页面对应的页控制块中的 pp_ref 标为 0 并使用 LIST_ INSERT_HEAD 将 其插入空闲链表。
回答 根据提示,第一、二步依葫芦画瓢的使用宏定义即可,主要是第三、四步,其中在pmap.c
中定义了struct Page * pages
、u_long npage
和u_long freemem
等全局变量,其中pages
通过alloc()
函数已经分配了相应的内存空间,作为每页的页控制块,其部分页面已被分配出去,也就是到freemem
对齐后对应的页面之前的页(不包括其对应的页),通过PADDR(freemem) / npage
即可得到相应的未分配的起始页号,这里的页号实际上是页框号也就是物理实际的内存,因此需要将freemem
转换成实地址后再除去页大小才可获得页号,则只需通过循环即可将此前的页面的页控制块的pp_ref
和此后的分别置1和置0 ,主要是如何将未分配的页面的页控制块利用LIST_INSERT_HEAD
加入到空闲链表中令人犯难,主要是该宏内部又使用了许多宏,导致过多的宏定义令人眼花缭乱,这里拽出相关的宏:
1 2 3 4 5 6 7 8 9 10 #define LIST_FIRST(head) ((head)->lh_first) #define LIST_INSERT_HEAD(head, elm, field) \ do { \ if ((LIST_NEXT((elm), field) = LIST_FIRST((head))) != NULL) \ LIST_FIRST((head))->field.le_prev = &LIST_NEXT((elm), field); \ LIST_FIRST((head)) = (elm); \ (elm)->field.le_prev = &LIST_FIRST((head)); \ } while (0) #define LIST_NEXT(elm, field) ((elm)->field.le_next)
仔细阅读可以发现,LIST_INSERT_HEAD()
的参数中head
会进行LIST_FIRST()
的宏操作,因此其应该是一个指向拥有lh_first
域的头结点的指针,而elm
参数会进行LIST_NEXT()
操作,因此其应该是一个指向拥有一个拥有le_prev
和le_next
域的field
域的普通节点的指针,回看Thinking 2.3
发现,Page_list
、Page
和pp_link
分别符合这三个参数的要求,因此,此处的对于该宏的使用要注意前两个空一定要是Page_list *
和Page *
类型的指针。
Exercise 2.4 题面 1 2 3 4 完成 page_alloc 函数。 在 page_init 函数运行完毕后,在 MOS 中如果想申请存储空间,都需要通过这个函数来申请分配。该函数的逻辑简单来说,可以表述为:1 . 如果空闲链表没有可用页,返回异常返回值。2 . 如果空闲链表有可用的页,取出链表头部的一页;初始化后,将该页对应的页控制块的地址放到调用者指定的地方。填空时,你可能需要使用链表宏`LIST_EMPTY`与函数`page2kva`。
回答 page2kva
函数定义如下:
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 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)); }#define KADDR(pa) \ ({ \ u_long _ppn = PPN(pa); \ if (_ppn >= npage) { \ panic("KADDR called with invalid pa %08lx" , (u_long)pa); \ } \ (pa) + ULIM; \ }) #define PPN(pa) (((u_long)(pa)) >> PGSHIFT) #define ULIM 0x80000000
第一步先通过LIST_EMPTY()
来判断空闲链表是否为空。然后若不为空,再通过LIST_FIRST()
将空闲链表的第一个空闲页控制块赋值给pp,运用LIST_REMOVE()
将其从空闲链表中删去,为了使用memset()
将pp对应的页的所有内存置0,观察到在alloc()
函数中有使用memset()
初始化分配的页面,其中三个参数分别为(void *)alloced_mem
、0
和n
(npage * sizeof(struct Page)),其中(void *)alloced_mem
是指向初始分配内存的地址,0
代表写入的值,而n
则代表了总共要写入的空间大小(字节数),因此依葫芦画瓢,此处只需要通过page2kva()
函数获得相应的分配地址,写入0到sizeof(struct Page)
空间即可。
Exercise 2.5 题面 1 2 完成 page_free 函数。 提示:使用链表宏 LIST_INSERT_ HEAD,将页结构体插入空闲页结构体链表。
回答 注意:LIST_INSERT_HEAD()
的参数为head
、elm
和field
。
Exercise 2.6 题面 1 2 3 4 5 6 完成 pgdir_walk 函数。 该函数的作用是:给定一个虚拟地址,在给定的页目录中查找这个虚拟地址对应的页表项,将其地址写入 *ppte。 • 如果这一虚拟地址对应的二级页表存在,则设置 *ppte 为这一页表项的地址; • 如果这一虚拟地址对应的二级页表不存在(即这一虚拟地址对应的页目录项无效), 则当 create 不为 0 时先创建二级页表再查找页表项,为 0 时则将 *ppte 设置为空指针。 注意,这里可能会在页目录项无效且 create 为真时,使用 page_alloc 创建一个页表,此时应维护申请得到的物理页的 pp_ref 字段。
回答 首先需要理解在二级页表机制中,虚地址与实地址的映射关系,根据代码一行一行给出查找过程。
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 static int pgdir_walk (Pde *pgdir, u_long va, int create, Pte **ppte) { Pde *pgdir_entryp; struct Page *pp ; pgdir_entryp = pgdir + PDX(va); if ((PTE_FLAGS(*pgdir_entryp) & PTE_V) == 0 ) { if (create != 0 ) { if (page_alloc(&pp) != 0 ) { return -E_NO_MEM; } pp->pp_ref++; *pgdir_entryp = page2pa(pp) | PTE_C_CACHEABLE | PTE_V; } else { *ppte = NULL ; return 0 ; } } u_long validTablePPN = PTE_ADDR(*pgdir_entryp); *ppte = (Pte *)KADDR(validTablePPN) + PTX(va); return 0 ; }
Exercise 2.7 题面 1 完成 page_insert 函数(补全 TODO 部分)。
回答 该函数本质上是想将给定的虚地址va
和页控制块指针pp
对应的页的第一个页表项基地址,也就是第一个4B,建立映射关系。具体分析见逐行注释
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 int page_insert (Pde *pgdir, u_int asid, struct Page *pp, u_long va, u_int perm) { Pte *pte; pgdir_walk(pgdir, va, 0 , &pte); if (pte && (*pte & PTE_V)) { if (pa2page(*pte) != pp) { page_remove(pgdir, asid, va); } else { tlb_invalidate(asid, va); *pte = page2pa(pp) | perm | PTE_C_CACHEABLE | PTE_V; return 0 ; } } tlb_invalidate(asid, va); if (pgdir_walk(pgdir, va, 1 , &pte) != 0 ) { return -E_NO_MEM; } *pte = page2pa(pp) | perm | PTE_C_CACHEABLE | PTE_V; pp->pp_ref++; return 0 ; }
Exercise 2.8 题面 1 2 3 完成 kern/tlb_asm.S 中的 tlb_out 函数。该函数根据传入的参数(TLB 的Key)找到对应的 TLB 表项,并将其清空。 具体来说,需要在两个位置插入两条指令,其中一个位置为 tlbp,另一个位置为 tlbwi。 因流水线设计架构原因,tlbp 指令的前后都应各插入一个 nop 以解决数据冒险。
回答 根据提示给出相应答案即可:
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 LEAF(tlb_out) #函数开头声明 .set noreorder #设定手动设置延迟槽模式 mfc0 t0, CP0_ENTRYHI #将原有的EntryHi中的值读取到t0寄存器中 mtc0 a0, CP0_ENTRYHI #将传入的原有的旧表项的Key传入EntryHi寄存器中 nop #手动延迟 /* Step 1: Use 'tlbp' to probe TLB entry */ /* Exercise 2.8: Your code here. (1/2) */ tlbp #依据旧表项的Key查找TLB将表项索引写入Index寄存器中(未找到则最高位置1,相当于将Index置负) nop #手动延迟 /* Step 2: Fetch the probe result from CP0.Index */ mfc0 t1, CP0_INDEX #获取旧表项的索引 .set reorder #设定编译器自动延迟模式 bltz t1, NO_SUCH_ENTRY #判断索引是否为负,为负就跳到未找到标签,否则继续下列操作 .set noreorder #设定手动延迟模式 mtc0 zero, CP0_ENTRYHI mtc0 zero, CP0_ENTRYLO0 mtc0 zero, CP0_ENTRYLO1 #将EntryHi、EntryLo0、EntryLo1赋值为0 nop /* Step 3: Use 'tlbwi' to write CP0.EntryHi/Lo into TLB at CP0.Index */ /* Exercise 2.8: Your code here. (2/2) */ tlbwi #使用tlbwi指令以Index中值为索引,将上述EntryHi、EntryLo0和EntryLo1写入TLB对应页表项中 .set reorder #设定自动延迟模式 NO_SUCH_ENTRY: #未找到标签 mtc0 t0, CP0_ENTRYHI #复原EntryHi原有的值 j ra #返回函数 END(tlb_out) #函数结束符
Exercise 2.9 题面 1 完成 kern/tlbex.c 中的 _do_tlb_refill 函数。
回答 根据提示完成相应填空即可:
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 void _do_tlb_refill(u_long *pentrylo, u_int va, u_int asid) { tlb_invalidate(asid, va); Pte *ppte; while (1 ) { if (page_lookup(cur_pgdir, va, &ppte) == NULL ) { passive_alloc(va, cur_pgdir, asid); } else { break ; } } ppte = (Pte *)((u_long)ppte & ~0x7 ); pentrylo[0 ] = ppte[0 ] >> 6 ; pentrylo[1 ] = ppte[1 ] >> 6 ; }
Exercise 2.10 题面 1 完成 kern/tlb_asm.S 中的 do_tlb_refill 函数。
回答 根据提示完成相应填空即可:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 NESTED(do_tlb_refill, 24, zero) mfc0 a1, CP0_BADVADDR #将发生异常的访存地址存入a1中 mfc0 a2, CP0_ENTRYHI andi a2, a2, 0xff /* ASID is stored in the lower 8 bits of CP0_ENTRYHI */ .globl do_tlb_refill_call; 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 #tlbwr指令将EntryHi和EntryLo的值随机填写到一个TLB页表项中 jr ra END(do_tlb_refill)
lab2难点分析 物理内存管理 在完成跳转后,为了建立物理内存管理机制,先要预先利用alloc()
函数初始化分配物理页框的页控制块的存储空间,并建立好空闲页控制块链表,alloc
函数(跳板机版本)如下:
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 void *alloc (u_int n, u_int align, int clear) { extern char end[]; u_long alloced_mem; if (freemem == 0 ) { freemem = (u_long)end; } freemem = ROUND(freemem, align); alloced_mem = freemem; freemem = freemem + n; panic_on(PADDR(freemem) >= memsize); if (clear) { memset ((void *)alloced_mem, 0 , n); } return (void *)alloced_mem; }
有意思的是,指导书给的版本有亿点小不同,首先是函数声明处,指导书还有static
修饰。此外,在Step 3之后的panic_on()处,指导书给的是如下一个条件判断:
1 2 3 if (PADDR(freemem) >= memsize) { panic("out of memory" ); }
处于好奇我去找了一下跳板机上给的这个panic_on
函数,发现其在./include/printK.h
中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 void _panic(const char *, int , const char *, const char *, ...)#ifdef MOS_HANG_ON_PANIC __attribute__((noreturn))#endif ;#define panic(...) _panic(__FILE__, __LINE__, __func__, __VA_ARGS__) #define panic_on(expr) \ do { \ int _r = (expr); \ if (_r != 0) { \ panic("'" #expr "' returned %d" , _r); \ } \ } while (0) #endif
其中panic_on
是预处理包装的一个条件判断”函数”,当传入的值转化为int
类型后不为0,就会触发panic()
另一个宏定义的”函数”,进而将其中的参数以变长参数形式触发_panic(__FILE__, __LINE__, __func__, __VA_ARGS__)
函数,此处只有该函数的声明,具体的定义在./kern/panic.c
函数中:
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 void _panic(const char *file, int line, const char *func, const char *fmt, ...) { u_long sp, ra, badva, sr, cause, epc; asm ("move %0, $29" : "=r" (sp) :); asm ("move %0, $31" : "=r" (ra) :); asm ("mfc0 %0, $8" : "=r" (badva) :); asm ("mfc0 %0, $12" : "=r" (sr) :); asm ("mfc0 %0, $13" : "=r" (cause) :); asm ("mfc0 %0, $14" : "=r" (epc) :); printk("panic at %s:%d (%s): " , file, line, func); va_list ap; va_start(ap, fmt); vprintfmt(outputk, NULL , fmt, ap); va_end(ap); printk("\n" "ra: %08x sp: %08x Status: %08x\n" "Cause: %08x EPC: %08x BadVA: %08x\n" , ra, sp, sr, cause, epc, badva);#if !defined(LAB) || LAB >= 3 extern struct Env envs []; extern struct Env *curenv ; extern struct Pde *cur_pgdir ; if ((u_long)curenv >= KERNBASE) { printk("curenv: %x (id = 0x%x, off = %d)\n" , curenv, curenv->env_id, curenv - envs); } else if (curenv) { printk("curenv: %x (invalid)\n" , curenv); } else { printk("curenv: NULL\n" ); } if ((u_long)cur_pgdir >= KERNBASE) { printk("cur_pgdir: %x\n" , cur_pgdir); } else if (cur_pgdir) { printk("cur_pgdir: %x (invalid)\n" , cur_pgdir); } else { printk("cur_pgdir: NULL\n" , cur_pgdir); }#endif #ifdef MOS_HANG_ON_PANIC while (1 ) { }#else halt();#endif } ``` 此外,不同于常见的双向链表,存储页控制块的链表封装了许多宏方便操作: - LIST_HEAD(name, type),创建一个名为`name`链表的头部结构体,包含一个指向type类型结构体的可指向链表首个元素的指针。 - LIST_ENTRY(type),作为一个特殊的类型出现,例:`LIST_ENTRY(Page) a;`,其本质是一个包括**指向下一个元素的指针`le_next`和指向前一个元素链表项`le_next`的指针`le_prev`**的链表项,作为指针的指针,`le_prev`便利于当删除一个元素时更改前一个元素链表项的`le_next`。 - LIST_EMPTY(head),判断head是否为空。 - LIST_FIRST(head),返回head对应链表的首个元素 - LIST_INIT(head),初始化head - LIST_NEXT(elm, field),返回**指针elm**指向的元素在对应链表中的下一个元素的指针,定义如下:`#define LIST_NEXT(elm, field) ((elm)->field.le_next)`,因此`elm`指向的结构体包含一个名为`field`的字段,该字段存储该节点的`le_next`和`le_prev`的值,类型是一个链表项`LIST_ENTRY(type)`,此后的`field`同义。 - LIST_INSERT_AFTER(listelm, elm, field),将`elm`插到已有元素`listelm`后 - LIST_INSERT_BEFORE(listelm, elm, field),插到前 - LIST_INSERT_HEAD(head, elm, field),插到head对应的链表头部 - LIST_REMOVE(elm, field),删除elm 此外,还封装了一些操作链表的函数: ```cvoid page_init (void ) ; int page_alloc (struct Page **pp) ; void page_free (struct Page *pp) ; void page_decref (struct Page *pp) ;
这些都是需要理解并牢牢掌握的。
虚拟存储管理 其实,虚存是假象出来的,它的唯一用途就是通过映射关系来访问物理内存,而不同段的虚存有不同的映射方式: 由于kseg0
和kseg1
是近乎直接映射,所以此处的难点主要是通过二级页表机制转换的kuseg
和kseg2
段: ,由于只能对虚拟内存进行访问,因此在通过一级页表偏移后得到的一级页表项虚地址转换后得到的一级页表项物理地址不能直接就在物理内存上通过得到的物理页号然后访问相应物理的二级页表项,而是仍需要先转换成虚地址(由于在低512MB是可以直接利用kseg0
段的映射方式进行转换),再利用kseg0
段的映射方式得到相应的物理地址,这也是不同虚拟段的不同映射方式但是映射关系确定导致的。 其余就是需要掌握一些与页表机制相关的宏和函数:
PDX(va)
(获取虚地址va的31-22位)和PTX(va)
(获取21-12).
PTE_V
,有效位,为1说明页表项有效。
PTE_D
,可写位,为1说明允许经由页表项对物理页进行写操作 。
PTE_G
,全局位,为1则TLB仅由虚页号匹配表项,不匹配ASID
。
PTE_C_CACHEABLE
,可缓存位,配置对应页面访问属性为可缓存。
PTE_COW
,写时复制位。
PTE_LIBRARY
,共享页面位。
int pgdir_walk(Pde *pgdir, u_long va, int create, Pte **ppte)
, 将一级页表基地址pgdir
对应的两级页表结构中的va
虚地址所在的二级页表项的指针 存储在ppte
指向空间上。若create!=0
且对应二级页表不存在,则会使用page_alloc
分配一页物理内存用于存放二级页表,满了直接报错。
int page_insert(Pde *pgdir, u_int asid, struct Page * pp, u_long va, u_int perm)
,将一级页表基地址pgdir
对应的两级页表结构中虚地址va
映射到页控制块pp
对应的物理页,并将权限设置为perm
。
struct Page * page_lookup(Pde * pgdir, u_long va, Pte **ppte)
,返回一级页表基地址pgdir
对应两级页表结构中虚地址va
映射的物理页面的页控制块 ,同时将ptte
指向的空间设为对应的二级页表项地址,没找到是会返回NULL的,要看清楚。
void page_remove(Pde * pgdir, u_int asid, u_long va)
,删除一级页表基地址pgdir
对应两级页表结构中va
对于物理地址的映射,若存在,则相应物理页面引用次数自减一 。
访存与TLB重填 TLB前置知识 拓展了计组学习到的TLB,每个TLB
实际上由一组Key
和两组Data
组成。其中Key
由EntryHi
寄存器的高19位VPN
和低8位ASID
组成,而Data
则由EntryLo
寄存器中的高20位PFN
和中间6位权限位组成: 此外,还有对TLB进行查找和填入的软件指令需要掌握:
tlbr
:以Index
寄存器中值为索引,读出TLB中对应表项到EtryHi
与EntryLo1
、EntryLo0
中。
tlbwi
,以Index
寄存器值为索引,将EtryHi
与EntryLo1
、EntryLo0
值写入索引指定TLB
表项中。
tlbwr
,将EtryHi
与EntryLo1
、EntryLo0
的数据随机写到一个TLB
表项中(使用Random
进行随机指定表项)
tlbp
,根据EtryHi
中的(VPN
+ASID
),查找TLB
中与之对应的表项,并将其索引存入Index
寄存器中,若未找到,则Index最高位置1 。
以及为了进行TLB维护而定义的函数: 通过tlb_invalidate()
函数删除特定虚地址在TLB
中的旧表项,其主要依靠在kern/tlb_asm.S
中的tlb_out
函数,tlb_out
被调用时,a0
寄存器中存放传入的参数,值为旧表项的Key
(VPN
+ASID
),使用mtc0
将Key
写入EntryHi
,再用tlbp
根据EntryHi
中Key
查找相应旧表项,将其索引存入Index
,索引值>=0,向EtryHi
与EntryLo1
、EntryLo0
中写入0,之后使用tlbwi
指令,将EtryHi
与EntryLo1
、EntryLo0
的值写入索引指定表项。此时旧表项key
和Data
清零,已无效。 由do_tlb_refill
函数完成TLB重填的工作,因为4Kc中有奇偶页设计,其需要重填触发异常的页面及邻居页面。重填时先将两个页面对应页表项写入EntryLo
寄存器,再填入TLB
。 TLB维护大体流程如下:
从BadVAddr
中取出引出缺失的虚地址。
从EntryHi
的0-7位取出当前进程的ASID
。
先在栈上为返回地址、待填入TLB页表项及函数参数传递预留空间 。以存储奇偶页表项地址、触发异常虚地址和ASID
为参数,调用_do_tlb_refill
函数,其完成了依据虚地址和ASID查找页表并将对应奇偶页表项写回第一个参数指定地址 。
页表项存入EntryLo0和EntryLo1
,执行tlbwr
将EtryHi
与EntryLo1
、EntryLo0
随机写入TLB一个页表项中。 当page_llokup()
函数在页表中找不到对应表项时,可通过passive_alloc()
函数进行处理。若虚地址合法,可为其申请二级页表物理页面(page_alloc
),并将虚地址映射到物理页面(page_insert)上。
心得体会
lab2一来代码量激增(不只是编写的更有阅读的),因为为了理解一个函数的用处,由于封装了过多的宏函数(对于熟练者那是一件美事,但是对于初学者简直要命),一开始我总是要多开窗口去查看相应的宏函数的作用,直到舍友跟我提及ctags
,我才将这个开学时一学而过的东西重新拾起来,在家目录的.vimrc
文件中在原有的第一行末尾:set tags=tags
加上,../tags
加可以完成对于当前目录和下一级目录的ctags
跳转,简直是阅读代码的利器,妈妈再也不用担心我读不懂代码了。
当然这单元更重要的还是理解整个内存管理机制的构建以及运作方式是如何,下为两张为了便于理解我的作图: 不是很好看,但是画出来写一下确实好理解了很多,因为真正理解下来,那些曾经认为的晦涩和繁琐也已然化作祛魅,成为理解OS路上的奠基之石。