虚拟内存
使用虚拟内存主要为了实现隔离
内存隔离,所有程序指令存放在一个物理内存上,如果一个指令的操作位刚好是另一个指令的地址,那么会造成指令的丢失
为了解决这个问题使用地址空间
地址空间
为每一个指令程序分配自己的地址空间,每个指令程序只能在自己的地址空间上操作。我们需要做的就是复用不同的地址空间在一个物理内存上
页表
一个页的大小是4096字节
实现虚拟内存到物理内存的映射
通过内存管理单元将虚拟地址转换为物理内存地址
CPU中具有一个寄存器satp来存放物理内存的地址,依次来告诉内存管理单元如何去寻找物理内存
每个应用进程都有具有一个映射关系,当cpu切换进程时,satp存储的值也会发生变化,使得不同进程即使虚拟地址相同,寻找到的物理内存也是不一样的。
satp的改变是由内核特权指令执行的。
映射方式
理论上是由\(2^{27}\)个页表条目PTE组成的,每个PTE中由中间44位PPN和后10位标志位控制物理地址的实际地址,可以用8B大小的uint64类型来存储,一般中间44位就是真实物理地址。偏移量由虚拟地址的后12位给出,大小为4KB
实际上页表以三级的树型结构存储在物理内存中,每棵树都是一个4096byte的页表页,包含512个PTE,该PTE对应下一棵的根物理地址,最后一棵树搜索到的PTE就是真实物理地址,而在每棵树的PTE就是通过存在虚拟地址中的中间27位均分为3个9位来进行偏移求解的
对于一个64位机器,如果是一一映射那么需要大小将是\(2^{64}\)其将占用全部内存
正确实现方式是
- 为每个页4KB实现映射,而不是每个地址,所以现在的一个虚拟地址存储为一个基准+偏移量
由于在RISV中高25位空间都不会用到,所以虚拟内存的大小被限制在\(2^{39}\),前27位用来定义索引,然后偏移量写在低12位,由于一个页的大小就是4096byte
物理内存实际上是56位,56则是根据硬件设计的,前44位描述的是物理内存的索引,后12位对应偏移 - 映射方式:虚拟内存的27位会被分成3段,每段为虚拟地址中的9Bit位组成,每段为一层,用于对应寻找物理内存地址,第一段就是顶层目录,其大小就是4096byte,对于其中的每个条目大小是64bits,也就是8byte所以一个目录可以存放512个条目即512个物理内存地址,通过satp寄存器的内容寻找到存放在第一个目录中的位置中数据,以此来寻找第二层目录中的位置...
对于每个虚拟内存地址,其中间27位被分成3段,从第一段开始表示第一级目录在satp对应的物理内存下的偏移,为satp会寻找到顶级目录的物理地址索引,然后虚拟地址中的第一段9位会提供一个偏移,找到第二段物理地址的位置,第二段9位会提供第二个偏移...
PPN存放的是物理地址,而不是虚拟地址,我们需要通过该地址寻找下一个物理地址,最终找到的地址就是我们这个虚拟内存对应的物理内存地址44位,比如使用到3个目录,那么最后一个目录寻找到的ppn就是最终物理内存地址,而偏移是原始虚拟地址的偏移。
可以发现,对于每次转换,总需要对内存进行三次访问。实际上对于处理器来所,其拥有一个转换后备缓冲器(TLB)用来存储最近使用的转换方式,它只是保存了页表条目或者PTE条目的缓存,即一种映射方式{虚拟地址,对应的物理地址}。当处理器切换了新的进程,具有新的页表目录和satp寄存器内容,那么TLB中的内容就需要得到更新(sfence_vma)
xv6中的虚拟内存
xv6会为每个进程维护一个页表,采用直接映射的方式,即物理地址对应的就是虚拟地址(从0x80000000->0x86400000)
对于内核栈,每个进程都拥有自己的内核栈,其存储在进程虚拟地址中较高的位置,其下方有一个没有映射关系的页表,即当发射栈溢出时,会先访问这个没有映射关系的页表,导致出错,从而防止对内核栈的覆盖。当然内核栈也会在0x80000000->0x86400000之间有一个映射关系(即物理地址可以有多次映射到虚拟地址上)。
即一个进程拥有一个页表,进程中虚拟地址空间(0x80000000->0x86400000)每一个地址中间通过多级页表结构映射到物理内存中。
寄存器
寄存器为CPU的一部分,一些基础运算通常是在寄存器上完成的,一般通过其ABIName来调用它,比如在trace和sysinfo实验中对于trapframe中的a0和a7就是x10和x17的ABIName
对于函数的参数存放在寄存器a0-a7中,如果函数参数超过8个才会存放到内存中,同时a0和a1还会存放函数的返回值,但一般会首先存放在a0中,所以对于系统调用的trace中我们使用a0和a7来进行对mask的赋值和判断
其从保存权限(Saver)来说可以分为两类Caller和Callee,Caller是指在函数调用期间可以更改的值,而Callee属性下的寄存器会在函数调用过程中得到保留
栈
一个函数调用在栈中由一个栈帧保存,其中最重要的两个值为该函数的返回值和前一个栈帧的起始位置(fp指针控制),调用函数就是通过fp指针的移动到对应的栈帧然后进行调用,一般在当前栈帧的顶部,栈中还有一个sp指针用来控制当前栈的使用大小,在栈的最底部,栈的存储是从高地址向低地址存储的,也就是说第一个函数存放在高地址,然后第二个函数将存放在sp-num的位置
函数序言
对于一个函数在内部调用了其他一个函数,汇编后会在调用函数之前形成函数序言,在函数尾部形成函数尾部。主要作用是为了对返回地址进行保存,因为ra寄存器是caller类型,当内部进行函数调用后,内部函数会把ra寄存器覆盖,最后的返回地址仍然是内部函数的ra,导致死循环。所以需要对ra存放在callee类型的寄存器中,然后在函数调用结束后恢复到ra
标签:虚拟地址,RISC,Lec4,物理地址,地址,内存,寄存器,虚拟内存 From: https://www.cnblogs.com/XTG111/p/18103986