Nachos Lab2 虚拟内存

Lab2 虚拟内存

任务完成情况

Exercise Y/N
Exercise1 Y
Exercise2 Y
Exercise3 Y
Exercise4 Y
Exercise5 Y
Exercise6 Y
Exercise7 Y
*challenge Y

Exercise 1 源代码阅读

一些值得注意的细节

如果要让Nachos运行用户程序,需要在MakeFile中添加-DUSER_PROGRAM。之后,一切的#ifdef USER_PROGRAM范围内的内容都会被编译。system.cc中初始化了一个Machine类型的对象machine,作为Nachos的模拟硬件系统,用来执行用户的程序。Nachos机器模拟的核心部分是内存和寄存器的模拟。

nachos寄存器模拟了MIPS机(R2/3000)的全部的32个寄存器,此外还包括8个用于调试的寄存器。一些特殊寄存器描述如下表:

寄存器名 编号 描述
StackReg 29 进程栈顶指针
RetAddrReg 31 存放返回地址
HiReg 32 存放乘法结果高32位
LoReg 33 存放乘法结果低32位
PCReg 34 存放当前指令的地址
NextPCReg 35 存放下一条执行指令的地址
PrevPCReg 36 存放上一条执行指令的地址
LoadReg 37 存放延迟载入的寄存器编号
LoadValueReg 38 存放延迟载入的值
BadAddReg 39 发生Exception(异常或syscall)时用户程序逻辑地址

Nachos 用宿主机的一块内存模拟自己的内存。由于 Nachos 是一个教学操作系统,在内存分配上和实际的操作系统是有区别的,且作了很大幅度的简化。

Nachos中每个内存页的大小同磁盘扇区的大小相同,而整个内存的大小远远小于模拟磁盘的大小,仅为32 * 128 = 4KB。事实上,Nachos 的内存只有当需要执行用户程序时用户程序的一个暂存地,而作为Nachos内核的数据结构并不存放在 Nachos 的模拟内存中,而是申请 Nachos 所在宿主机的内存。所以 Nachos 的一些重要的数据结构如线程控制结构等的容量可以是无限的,不受 Nachos 模拟内存大小的限制。

参考资料《Nachos中文教程》

progtest.cc

在terminal中输入nachos -x filename,main()将调用StartProcess(char *filename)函数,其执行流程为:打开文件->为当前线程分配地址空间(调用addrSpace函数,根据程序的数据段、程序段和栈的总和来分配地址空间,具体的流程为:将头文件内容加载到NoffHeader的结构体中,它定义了Nachos目标代码的格式,并且在必要的时候进行大小端的转换->计算用户程序所需内存空间大小,并计算所需页数->建立虚拟地址到物理地址的转换机制(目前Nachos的地址转换是完全对等的,即采用绝对装入的方式,只适用于单道程序)->调用machine->mainMemory初始化物理空间,将程序的数据段和代码段拷贝到内存中->关闭文件->初始化寄存器(InitRegisters)->将当前线程对应的页表始址和页目录数写入“页表寄存器”(在Machine中用两个public成员变量:pageTablepageTableSize来实现“页表寄存器”的功能(即存储currentThread的页表始址和页表大小)->调用machine->run函数,该函数会调用OneInstruction函数逐条执行用户指令,同时调用OneTick函数,让系统时间前进,直到当前进程执行结束或者主动/被动让出CPU。

machine.h(cc)

machine.h中定义了一组与Nachos模拟寄存器和内存有关的参数,比如物理页面大小、内存页面数、寄存器大小等等。枚举了Nachos的异常类型:

异常 描述
NoException 无异常
SyscallException 系统调用(个人意见,系统调用不应该属于异常)
PageFaultException 缺页异常(除了页表,使用tlb时同样会抛出此异常)
ReadOnlyException 对只读对象做写操作异常
BusErrorException 地址变换结果超过物理内存异常
AddressErrorException 地址不能对齐或者地址越界异常
OverflowException 整型数加减法结果越界异常
IllegalInstrException 非法指令异常
成员变量/函数 描述
char *mainMemory; 模拟内存,用于运行用户程序
int registers[NumTotalRegs]; 模拟寄存器,用于运行用户程序
TranslationEntry *tlb; 快表,系统中只有一个快表
TranslationEntry *pageTable;unsigned int pageTableSize; 当前线程的页表起始地址和页目录数,相当于执行“页表寄存器”的职能。 注意:pageTable变量在两个地方均有定义,一个是在thread.cc中,表示某个线程的页表,这个pageTable可以有多个,并且与线程是一一对应的;另一个是在machine.cc中,表示currentThread的页表,这个pageTable只有一个,且始终指向currentThread的页表。注意:无论是tlb还是pageTable,都不存放在模拟的mainMemory之中,而是借用宿主主机内存存放,换言之,mainMemory只用来存放用户程序指令、数据和栈。
Machine(bool debug) 初始化用户程序执行的模拟硬件环境,包括内存,寄存器
void Run(); 逐条执行用户程序指令
ReadRegister(int num) 读取寄存器的值
WriteRegister(int num, int value) 将value写入寄存器
void OneInstruction(Instruction *instr); 执行一条用户程序指令,其步骤为:读取一条指令的二进制编码->decode()解码->获得对应的操作符opCode,rs,rt和rd寄存器->根据opCode执行相应的运算->更新寄存器PC,nextPC,prevPC的值,如果有异常,调用raiseException函数处理异常,然后调用异常处理程序处理异常。在Run函数中无限循环此函数直到用户程序执行到最后一条指令。
void DelayedLoad(int nextReg, int nextVal); 结束当前运行的一切东西,与mips模拟有关

translate.h(cc)

定义了页目录项,实现了读写内存,地址转换的功能。

变量/函数 描述
int virtualPage; 逻辑地址
int physicalPage; 物理地址
bool valid; 此页是否已在内存
bool readOnly; 此页是否为只读
bool use; 此页是否被读或者修改过
bool dirty; 此页是否被修改过
bool ReadMem(int addr, int size, int* value); 读取内存
bool WriteMem(int addr, int size, int value); 写入内存
ExceptionType Translate(int virtAddr, int* physAddr, int size,bool writing); 实现逻辑地址到物理地址的转换,其流程为:通过virtualAddr计算出vpn和offset->通过vpn来查找ppn(在tlb或页表中查询,否则抛出异常,调用异常处理函数,目前这部分还未实现)->将ppn与offset相加,得到物理地址。 Nachos中只允许tlb和页表二选一,如果要实现二者并存,需要将ASSERT(tlb == NULL || pageTable == NULL)和ASSERT(tlb != NULL || pageTable != NULL) 注释掉。其实现流程与我们之前学习的地址变换机构相一致。

exception.cc

定义了异常处理函数ExceptionHandler,用来对相应的异常(系统调用,地址越界,算数结果溢出等)做出处理。nachos对于异常或者系统调用的处理过程是:调用machine->raiseException函数,该函数得到异常类型和发生错误的虚拟地址,然后将发生错误的虚拟地址存入BadVAddrReg寄存器,然后调用DelayedLoad函数来结束当前运行中的所有东西,之后设置系统状态为内核态,然后调用ExceptionHandler函数进行异常处理,处理完异常之后重新将系统设置为用户态。

Exercise 2 TLB MISS异常处理

要使用tlb,需要在userprog/MakeFile中添加-DUSE_TLB

DEFINES = -DUSE_TLB

Nachos是如何获取单条指令的(重要)

code/machine/mipssim.cc

Machine::Run() {
    ...

    while (1) {
        OneInstruction(instr);

        ...
    }
}
Machine::OneInstruction(Instruction *instr)
{
    ...

    // Fetch instruction
    if (!machine->ReadMem(registers[PCReg], 4, &raw))
        return;			// 异常发生
    instr->value = raw;
    instr->Decode();

    ...

    // 计算下一个pc的值,但是为了避免异常发生,暂时不装载
    int pcAfter = registers[NextPCReg] + 4;

    ...


    // 现在已经成功执行指令

    ...

    // 更新PC寄存器的值,为了执行下一条指令,PC会自增4,即PC+=4
    registers[PrevPCReg] = registers[PCReg];	
    registers[PCReg] = registers[NextPCReg];
    registers[NextPCReg] = pcAfter;
}

如果Nachos获取指令失败(一般是因为PageFaultException),之后它并不会执行PC+4,因为machine->ReadMem()会返回false。因此,我们只需要在exceptionHandler中更新tlb,这样Nachos下一次还会执行同一条指令并且再次尝试地址转换。这一知识点非常重要,我还会在Exercise7中提到它。

TLB miss

bool 
Machine::ReadMem(int addr, int size, int *value)
{
   
	...
	exception = Translate(addr, &physicalAddress, size, FALSE);
	if (exception != NoException)
	{
   
		machine->RaiseException(exception, addr);
		return FALSE;
	}
	...
}

当执行code/machine/translate.cc里的 Machine::ReadMem() 或者 Machine::WriteMem()时,如果translate()返回PageFaultException,会调用Machine::RaiseException()并返回FALSE。

void 
Machine::RaiseException(ExceptionType which, int badVAddr)
{
		...
    registers[BadVAddrReg] = badVAddr;
		...
		interrupt->setStatus(SystemMode);
    ExceptionHandler(which);
    interrupt->setStatus(UserMode);
}

Machine::RaiseException()会将发生异常的虚拟地址存入BadVAddrReg,因此我们可以从BadVAddrReg中获取发生异常的虚拟地址。最后调用exception.cc里的ExceptionHandler函数对异常进行处理,目前它只有对Halt系统调用的处理。这里需要添加对TLB异常的PageFaultException处理语句:

void ExceptionHandler(ExceptionType which)
{
   
    if (which = PageFaultException)
    {
   
				ASSERT(machine->tlb);//保证tlb存在
        int badVAddr = machine->ReadRegister(BadVAddrReg);//获取发生错误的虚拟地址
        TLBMissHandler(badVAddr);//调用页面置换算法对该虚拟地址进行处理
    }
		//syscall
    ...
}

Exercise 3 置换算法

准备工作

  1. TLB命中率

    为了获取TLB命中率,我在code/machine/machine.h中定义了tlbVisitCnttlbHitCnt, 并在code/machine/machine.cc中对它们初始化为0。每当调用code/machine/translate.cc中的translate函数时tlbVisitCnt++,每当tlb命中时,tlbHitCnt++

    ExceptionType
    Machine::Translate(int virtAddr, int *physAddr, int size, bool writing)
    {
         
    	tlbVisitCnt++;//每当调用translate函数时,tlbVisitCnt++
    	...
    		for (entry = NULL, i = 0; i < TLBSize; i++)
    			if (tlb[i].valid && (tlb[i].virtualPage == vpn))
    			{
         
    				tlbHitCnt++;//每当tlb命中时,tlbHitCnt++
    				...
    			}
    		...
    }
    
  2. 自定义用户程序

    在code/test中有几个用于测试的用户程序,其中halt太小了,只会用到三页,因此对任意的页面置换算法,使用halt测试出来的结果都是一样的;而其他几个又太大了,装载进mainMemory的时候会报错,例如matmult,但是我会在Exercise7实现了lazy-loading之后测试它。

    //运行matmult报错
    vagrant@precise32:/vagrant/nachos/nachos-3.4/code/userprog$ ./nachos -d a -x ../test/matmult 
      //a 表示 address,它是Nachos自带的debug flag,后面还会用到m,表示machine
    Assertion failed: line 81, file "../userprog/addrspace.cc"
    Aborted
    

    所以我自己写了一个简单的用户程序——计算99乘法表code/test/99table.c

    /*
       Author: litang
       Description: 99table.c,用于简单测试用户程序,内容为计算99乘法表
       Since: 2020/11/8
              11:33:38
    */
    
    #include "syscall.h"
    #define N 9
    
    int main()
    {
         
        int table[N][N];
        for (int i = 1; i <= N; ++i)
            for (int j = 1; j <= N; ++j)
                table[i - 1][j - 1] = i * j;
        Exit(table[N - 1][N - 1]);
    }
    

    并且将下面的内容加入 code/test/Makefile

    all: ... 99table
    
    ...
    
    # For Lab2: Test the TLB Hit Rate
    99table.o: 99table.c
        $(CC) $(CFLAGS) -c 99table.c
    99table: 99table.o start.o
        $(LD) $(LDFLAGS) start.o 99table.o -o 99table.coff
        ../bin/coff2noff 99table.coff 99table
    

    完成上述步骤之后,需要在Nachos3.4/code/test下重新make一遍。

  3. 不同算法的转换

    我用了一些宏来作为不同算法切换的开关

    • TLB_FIFO
    • TLB_LRU

    需要注意的是:每次通过修改code/machine/Makefile中的开关来调试程序时,需要对任意代码做一些修改,然后再改回来,否则Nachos编译的时候无法检测MakeFile的改动。

LRU算法

LRU(Least recently used,最近最少使用)。

  • 用tlb来记录最近访问的页
  • 若页在tlb中已存在,则将该页移到tlb头部,这样就能保证头部页是最近访问的,尾部页是最早访问的,当需要置换页的时候,直接丢弃尾部页,然后将新页插入头部即可;
  • 若页在TLB中不存在,要分两种情况讨论:若TLB还有空位,需要先在页表中查找该页,且valid为True,表明此页在内存中存在,再将该页插入TLB的头部;若TLB没有空位,需要丢弃尾部页,然后将新页插入TLB头部。
  • Nachos默认TLB是数组,每次插入都需要移动元素,而移动元素需要较大的时间开销。
优化
  • 为了减少移动元素的时间开销,可以在页表项TranslationEntry中添加成员变量lastVisitedTime,每当访问某页时,更新此成员变量为当前的totalTicks,每次需要替换页面时,需要遍历整个TLB,找出lastVisitedTime最小的表项,替换之。此方法和原始方法相比,每次更新页的访问时间时,只需要更新其lastVisitedTime即可,节约了移动元素的时间,但是增加了页表项的空间开销和查找最小的lastVisitedTime页的时间(需要遍历整个TLB,时间复杂度O(n))。考虑到Nachos的tlb很小,只有4,因此我在本次试验中将使用此方法。
  • 用数组实现LRU需要多次移动元素,而使用链表则不需要考虑这个问题。若要使用链表,则需要使用双向链表(为了维护rear指针,后面会说明),需要在TranslationEntry 中添加next和prior指针,还需要改动machine.cc中对于tlb数组的初始化,将数组改为链表。还需要一个全局变量currTLBSize来记录当前TLB的大小,维护front和rear指针指向TLB的链头和链尾,每次从尾部移除元素时,rear都需要指向前一个元素,所以需要双向链表。此方法和添加lastVisitedTime的方法相比,增加了一个额外的空间开销(next/prior指针),并增加了currTLBSize/front/rear变量,空间复杂度为O(n)+O(1)~=O(n);另一方面,由于页面的访问时间在链表中是顺序排列的,最早访问的页就在链尾,查找最早访问页面的时间复杂度为O(1),可以大大减少查找最早访问页的时间,故此方法是综合性能最优的。(TOBEDONE)

FIFO算法

和LRU算法相比,FIFO算法实现比较简单。

  • 维护一个FIFO队列,按照时间顺序将各数据(已分配页面)链接起来组成队列,并将置换指针指向队列的队首。
  • 进行置换时,只需把置换指针所指的数据(页
  • 4
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值