Lab2实验报告

Lab2实验报告

一、思考题

Thinking2.1

请你根据上述说明,回答问题:

在我们编写的程序中,指针变量中存储的地址是虚拟地址还是物理地址?

MIPS 汇编程序中lw, sw使用的是虚拟地址还是物理地址?

虚拟地址;

虚拟地址。

Thinking2.2

请从可重用性的角度,阐述用宏来实现链表的好处。

C语言中没有多态,那么就会出现以下的情况:如果在一个工程中想要同时使用整型的链表和浮点型的链表,就要定义两个结构体,并且同样的操作节点的代码要写两遍;若在C语言中借助void*指针,利用强制类型转换的方法实现多态,可能会出现无法确定数据存储位置而导致出错的情况。

所以用宏实现链表,可以避免重复劳动,也可以提高代码的效率 ,减少出错。

请你查看实验环境中的 /usr/include/sys/queue.h,了解其中单向链表与循环链表的实现,比较它们与本实验中使用的双向链表,分析三者在插入与删除操作上的性能差异。

sys/queue.h是Linux的内嵌链表,其中:

  • SLIST为sys/queue.h中的单向无尾链表。它的结构很简单,删除与插入操作与普通的链表操作相同。进行插入操作时只需要将新插入节点的指针(field.sle_next)指向要插入的位置的下一个节点 ,并将要插入的上一个节点的指针(field.sle_next)指向它即可;删除时跟插入操作基本相反。
  • STAILQ为sys/queue.h中的单向有尾链表。它的删除与插入操作相比SLIST多了对尾部的操作,在处理时多了if是尾部的判断,其余基本相同。
  • CIRCLEQ为sys/queue.h中的循环链表。它的结构相比单向链表稍微复杂,因为它的最后一个节点的指针指向头节点。

三者的性能差异:

  • 单向链表的插入操作的时间复杂度为O(1),删除操作的时间复杂度为O(n);
  • 循环链表的插入操作的时间复杂度为O(1),删除操作的时间复杂度为O(n);
  • 双向链表的插入操作的时间复杂度为O(1),删除操作的时间复杂度为O(1);
Thinking2.3

请阅读 include/queue.h 以及 include/pmap.h, 将 Page_list 的结构梳理清楚,选择正确的展开结构。

正确的结构是C

C:
struct Page_list{
    struct {
        struct {
            struct Page *le_next;
            struct Page **le_prev;
        } pp_link;
        u_short pp_ref;
    }* lh_first;
}
Thinking2.4

请你寻找上述两个 boot_* 函数在何处被调用。

  • boot_pgdir_walk函数在boot_map_segment中被调用;
  • boot_map_segment函数在mips_vm_init中被调用。
Thinking2.5

请阅读上面有关 R3000-TLB 的叙述,从虚拟内存的实现角度,阐述 ASID 的必要性

引用自网络上对ASID的解释:ASID唯一标识每个进程,并用于为该进程提供地址空间保护,当TLB尝试解析虚拟页码时,它确保当前正在运行的进程的ASID与与虚拟页面关联的ASID匹配。 如果ASID不匹配,则将该尝试视为TLB未命中 。

ASID是TLB中每个条目的额外位,用于检查正在访问该条目的进程是否属于该进程 ,ASID允许TLB同时包含多个进程的条目。在进程切换的时候,对于那些nG的地址映射,它会有一个ASID,对于TLB的entry而言,即便是保存多个相同虚拟地址到不同物理地址的映射也可以,只要他们有不同的ASID。

请阅读《IDT R30xx Family Software Reference Manual》的 Chapter 6,结合 ASID 段的位数,说明 R3000 中可容纳不同的地址空间的最大数量

ASID 为11-6位,共6位,故能容纳不同地址空间的最大数量为64。

Thinking2.6

请你完成如下三个任务:

  • tlb_invalidate 和 tlb_out 的调用关系是怎样的?
  • 请用一句话概括 tlb_invalidate 的作用
  • 逐行解释 tlb_out 中的汇编代码
  • tlb_invalidate 调用了tlb_out

  • 在页表更新时更新 TLB
    在这里插入图片描述

  • 如上图为tlb_out的结构,其中的汇编代码的含义是:

    • 将CP0_ENTRYHI中原有的值写入k1寄存器,然后将a0寄存器中的值保存到k1中;
    • 待mtc0指令退出流水线后执行tlbp指令查询参数的地址是否存在于TLB中,查找TLB中的对应表项,将索引写入Index寄存器,如果查找失败,则Index的最高位置1;
    • 由于前一步可能写入Index寄存器,后一步需要获取Index寄存器的数据,因此需要等写入指令出流水线后再执行下一条指令,对于五级流水线,需要暂停4个nop指令。
    • 将Index寄存器值存入k0寄存器,判断k0 寄存器的值是否小于0,如果k0寄存器的值小于0,就跳转到NOFOUND所在位置,如果k0寄存器值不小于0,把HI和LO寄存器清空,以Index寄存器中的值为索引,将此时 ENTYRYHI与ENTRYLO的值写到索引所对应的TLB中;
    • 将k1的值存回HI寄存器,之后返回。
Thinking2.7

在现代的 64 位系统中,提供了 64 位的字长,但实际上不是 64 位页式存储系统。假设在 64 位系统中采用三级页表机制,页面大小 4KB。由于 64 位系统中字长为 8B,且页目录也占用一页,因此页目录中有 512 个页目录项,因此每级页表都需要 9 位。因此在 64 位系统下,总共需要 3 × 9 + 12 = 39 位就可以实现三级页表机制,并不需要 64 位。现考虑上述 39 位的三级页式存储系统,虚拟地址空间为 512 GB,若记三级页表的基地址为 PTbase ,请你计算:

  • 三级页表页目录的基地址

第0x7fc00000>>12个页表项相对于页表基地址的偏移为(0x7fc00000>>12)*8=0x3fe000,页目录基地址为0x7fffe000

  • 映射到页目录自身的页目录项(自映射)

(0x7fc00000 >> 22) * 8 + 0x7fc00000 = 0x7fffeff8

Thinking2.8

回答:

  • 简单了解并叙述 X86 体系结构中的内存管理机制,比较 X86 和 MIPS 在内存管理上的区别。

差别主要在对TLB不命中的处理上:

  • X86在TLB不命中时,MMU以CR3为当前进程的PGD基址,并且索引获得PFN后输出 PA,MMU会填充TLB加快下次转换速度。
  • MIPS会触发TLB异常,内核的tlb_refill_handler会以pgd_current为当前进程的PGD基址,索引获得转换失败的虚址对应的PTE,并将其填入TLB。

二、实验难点图示

1、Page结构体存储结构

理解Page结构体的结构,是本次实验比较大的一个难点。
在这里插入图片描述

2、LIST_INSERT_TAIL

LIST_INSERT_TAIL 的功能是将一个元素插入链表尾部,由于没有记录链表的尾指针,因此该函数的实现和 LIST_INSERT_HEAD 有很大区别,下图是LIST_INSERT_TAIL函数的实现流程图。

在这里插入图片描述

3、两级页表结构

摘自指导书:
在这里插入图片描述

我在理解这一部分的时候花了很长的时间,尤其是在做Exercise2.6时,由于没有很彻底地理解指导书在第二部分最开始时的一段话 “CPU 发出的地址均为虚拟地址,因此获取相关物理地址后,需要转换为虚拟地址再访问。”所以花了很长的时间把2.6做出来。指针变量存储的地址是虚拟地址,所以在Exercise2.6中,pgdir、pgdir_entryp、pgtable、pgtable_entry这几个存储的都是虚拟地址,通过基地址+偏移量确定入口位置,通过PADDR和KADDR在虚拟地址和物理地址之间进行转换,并且通过设置权限位来控制页面的属性信息,达到管理的目的。

4、boot_pgdir_walk函数

用于建立页目录和页表的联系。

函数/宏定义解释
PDX(va)va右移22位后低10位是页目录索引,剩余高位都是0,再与0x03FF并是为了确保高位为0
PTX(va)va右移12位后,低10位是页表索引,11-20位是页目录索引,与0x03FF并可以使得11-32位都为0,从而得到页目录索引
PADDA虚拟地址->物理地址,所做操作是将最高位清零
KADDR物理地址->虚拟地址,所做操作是将最高位置1
PTE_ADDR将页目录项的低12位清零,得到对应页表的物理地址

同时,若页目录没有对应的页表,则需要新建:

  • 用alloc函数分配一块内存空间
  • 将页目录项低12位标志位全部清零
  • 将高20位更改为当前页表的物理地址
  • 更改有效位PTE_V
5、自映射机制

页目录中有一条页目录项指向自身的物理地址。

一些计算:因为1M个页表项和4G的地址空间是线性映射,因此页目录地址(简称PD)对应的应该是第PT>>12个页表项,也就是第一个页目录项。而一个页目录项32位,4字节,因此该项相对于起始地址的偏移为(PT>>12)<<2 = PT>>10,因此PD = PT | PT>>10。同理,PT>>10是页目录在整个页表上的偏移,对应(PT>>10)>>10是自映射页目录项在页目录这张页表上的偏移。
在这里插入图片描述

三、体会与感想

这次lab用时比较长,第一部分大概7小时左右,第二部分12小时+的样子,理解的过程比较曲折,一直在我懂了—我会了—我做错了—我又不懂了之间徘徊不定,经历了好几次理解体系的重构。本节的一大难点在于链表的理解,其实并不是特别复杂,但是写得特别精妙,每读一遍都有新的收获与更深的认识。lab2代码上下连贯性非常强,要多读代码对各种函数有较强的敏感度,在阅读时才能够减少障碍。

遇到的第一个坎首先是Page结构体的理解,lab1-1考的结构体和链表,然而历时一年之久,链表忘了怎么写了orz,所以从重学链表知识开始。关于考试,搞错了lab2的重点,以为课上会重点考链表,所以课下认真研究了exercise2.2,而将2.3—2.5粗略带过,结果课上挂了…课后经过复盘,lab2-1确实不难,只要熟悉了Exercise 2.3—2.5的三个函数,熟练了Page结构体的结构,稍微做一下改动,增添一下判断的标志位就解决了,挂掉真的很可惜,毕竟分值不小。

因为挂了lab2-1,所以在学lab2-2的课下内容时格外认真,对于两级页表机制,理解了很长时间才弄懂,(在这里真心的感谢助教gg的耐心讲授),Exercise2.6—2.9也都花了很长时间学习,不过课上很可惜还是挂了。课上考试自认为题目不难,逻辑很清楚,给的样例也跑对了,但是交上去还是0分,课下和助教讨论时,发现因为用了双层的for循环嵌套,所以应该是TLE了,这次挂掉的主要原因是解题时的思考方向不对。

挂掉lab2-2后很长一段时间情绪都有点低落,一个原因是lab2占的分值有20分,但是课上部分一分都没拿到,但是更重要的原因是,顿时不知道该怎么学了,我对lab2课下自己的掌握程度还是挺满意的,但是课上考试还是通过不了,就好像写代码时遇到了无法复现的bug,有点无能为力。

但是失落归失落,还是要积极学下去的,希望lab3能有好消息叭。

四、残留难点

感觉在Exercise2.9之后的部分,关于TLB使用的汇编代码不是很熟练,并且对各个CP0寄存器的功能也不熟,希望能在lab3加深理解。

  • 5
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
实验概述 本次实验是MIT 6.828操作系统课程的第一次实验,主要内容是编写一个简单的操作系统内核,并在QEMU虚拟机上运行。本次实验共有9个练习,其中练习9要求实现一个简单的用户程序并运行。 练习9要求我们实现一个简单的用户程序,该程序能够在屏幕上输出一些信息,并等待用户输入,输入结束后将输入内容输出到屏幕上。用户程序的具体要求如下: - 输出一些信息,例如“Hello World!”。 - 等待用户输入,可以使用getchar()函数实现。 - 将用户输入内容输出到屏幕上。 实验过程 1. 编写用户程序 我们首先在lab1目录下创建一个user文件夹,用于存放用户程序。然后创建一个名为“test.c”的文件,编写用户程序的代码如下: ``` #include <stdio.h> int main() { printf("Hello World!\n"); char c = getchar(); printf("You entered: %c\n", c); return 0; } ``` 这段代码的功能是输出“Hello World!”并等待用户输入,输入结束后将输入内容输出到屏幕上。 2. 修改Makefile文件 为了能够编译用户程序,我们需要修改Makefile文件。具体修改如下: ``` UPROGS=\ _cat\ _echo\ _forktest\ _grep\ _init\ _kill\ _ln\ _ls\ _mkdir\ _rm\ _sh\ _stressfs\ _usertests\ _wc\ _test\ # 添加用户程序的名称 $(OBJDIR)/_test: $(OBJDIR)/test.o $(LIBDIR)/ulib.o | $(OBJDIR) $(LD) $(LDFLAGS) -N -e main -Ttext 0 -o $@ $^ $(OBJDIR)/test.o: test.c | $(OBJDIR) $(CC) $(CFLAGS) -c -o $@ $< ``` 在UPROGS变量中添加上刚刚编写的用户程序的名称“_test”,然后在Makefile文件的末尾添加如上代码。 3. 编译内核和用户程序 在终端运行命令“make”,编译内核和用户程序。 4. 运行QEMU虚拟机 在终端运行命令“make qemu”,启动QEMU虚拟机。 5. 运行用户程序 在QEMU虚拟机中,输入“test”,即可运行刚刚编写的用户程序。运行结果如下: ``` Hello World! This is a test. You entered: T ``` 可以看到,程序首先输出了“Hello World!”这个信息,然后等待用户输入。我们输入了“This is a test.”这个字符串,然后按下回车键,程序将输入内容输出到了屏幕上。 实验总结 本次实验要求我们实现一个简单的用户程序并运行。通过编写代码、修改Makefile文件、编译内核和用户程序、启动虚拟机以及运行用户程序等步骤,我们成功地完成了本次实验,并学会了如何在操作系统内核中运行用户程序。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值