虚拟存储器
- 分别实现实模式、分段式、段页式三种内存的地址转换与数据加载功能。
- 地址转换
- 在MMU类中,实现三个地址转换的方法,将逻辑地址转换为线性地址再转换为物理地址。
- private String toRealLinearAddr(String logicAddr)
- private String toSegLinearAddr(String logicAddr)
- private String toPagePhysicalAddr(String linearAddr)
- 数据加载
- 在Memory类中,实现三个数据加载方法。
- public void real_load(String pAddr, int len)
- public void seg_load(int segIndex)
- public void page_load(int vPageNo)
- 融合cache与TLB
- 将cache与TLB融合到MMU中。
- 逻辑地址:指令中给出的地址, 48 位(16位段寄存器 + 32位段内偏移量)。
- CPU在运行指令时,如果想要访问内存,它并不是用直接使用内存的地址访问,而是给出一个由 16 位段寄存器和 32 位段内偏移量拼起来的 48 位的逻辑地址。
- 比如,如果CPU想知道当前指令的地址,他给出的逻辑地址应该是(CS:EIP)。
- 其中,CS是 16 位代码段寄存器,EIP是 32 位指令指针寄存器(也就是程序计数器PC)。
- CPU在运行指令时,如果想要访问内存,它并不是用直接使用内存的地址访问,而是给出一个由 16 位段寄存器和 32 位段内偏移量拼起来的 48 位的逻辑地址。
- 线性地址:逻辑地址到物理地址的中间层, 32 位。
- 如果没有启用分页机制,那么线性地址就等于物理地址。
- 如果启用了分页机制,那么线性地址需要通过再一次变换才能得到物理地址。
- 物理地址:内存中的地址, 32 位。
实模式
Memory的real_load
方法
- SEGMENT和PAGE均为false时,为实模式
- 变量:
pAddr
与len
- 直接用地址和长度从磁盘disk.read加载data
- 把data再写进内存(注意实模式下,内存地址对应磁盘地址,即pAddr)
System.arraycopy(data, 0, memory, start, len);
- 将原数组data从起始位置0,复制到目标数组memory的start位置开始,复制的元素数量为len
1 | /** |
MMU
- ``toRealLinearAddr`方法: 逻辑地址转线性地址。
- 段寄存器左移4位 + 段内偏移量的低16位 再补齐32位
- 截取前16位段寄存器;再截取offset低16位(实际截取32位),转成int。
- 在实际计算中,我们把高 16 位看作基址,低 32 位看作偏移量(实际有用的只有低 16 位)
- 将段寄存器和偏移量化为整数
- 段寄存器左移4位
- 可以直接使用Java中的<<运算符将整数左移4位
- 两个数字相加,转成二进制,高位补0到32位
1 | /** |
分段式
Memory类:实现seg_load段加载方法
- 从磁盘上加载该段的数据到内存。
- 如何从磁盘上读取一整段呢?你应该使用段基址作为访问磁盘的地址,用**段限长(即段大小)**作为读取的长度。
- 至于段基址和段限长是多少,参考我们3.2.3的规定。
- private char[] base = new char[32]; // 32位基地址
- private char[] limit = new char[20]; // 20位限长
- 那么加载过来之后写到内存的哪里呢?由于分段式下每个段大小只有1MB,不会超出内存大小,所以我们默认把数据放在物理地址为 0 的地方。
- 除了加载数据,你还需要填好全局描述符表GDT,需要填入的内容还是按照3.2.3的规定进行填写。下面为该规定的原文。
- 每个由MMU装载进入GDT的段,其段基址均为全 0 ,其限长均为全 1 ,未开启分页时粒度为false,开启分页后粒度为true。
- 变量:segIndex段索引
- 获取段描述符:
- (有方法)通过 segIndex 获取对应的段描述符 segDes
- 获取段基址和段限长:
- 段基址:32位全0
- 段限长:20位全1
- 判断是否开启分页
- 如果未开启分页模式(PAGE 为 false),
- 则从磁盘中读取数据,并将数据写入内存的物理地址为0的地方。
- 注意,段限长是从0开始计数,因此读取长度需要加1。
- 更新段描述符:
- 基址
- 限长
- 有效位:validBit 设置为 true 表示段已在内存中
- 粒度:granularity 设置为 PAGE(未开启分页时粒度为false,开启分页后粒度为true)
1 | /** |
MMU类:toSegLinearAddr 逻辑地址转线性地址。
- 在分段式下,逻辑地址转线性地址应该要查全局描述符表GDT,按照2.3.4的流程进行计算。
- 48 位的逻辑地址包含 16 位的段选择符和 32 位的段内偏移量。
- MMU首先通过段选择符内的 13 位索引值,
- 从段描述符表中找到对应的段描述符,从中取出 32 位的基地址,与逻辑地址中 32 位的段内偏移量相加,就得到 32 位线性地址。
- 注意,不要以为可以偷懒直接把逻辑地址的前 16 位去掉
- 直接调用已有的函数获得段索引segIndex
- 根据段索引获得段基址,也是直接调用函数。将段基址转成int
- 截取32位段内偏移offset,转成int
- 段基址与段内偏移相加,转成32位二进制,得到线性地址
1 | /** |
段页式
Memory类:page_load页加载方法。
- 从磁盘上加载该页数据到内存。
- 如何在磁盘上读取该页数据呢?你应该使用该虚页的起始地址作为作为访问磁盘的地址,用页大小作为读取的长度。
- 至于该虚页的起始地址是多少,可以直接根据虚页号得到。
- 那么加载过来之后写到内存的哪里呢?这就需要你找出一个空闲的物理页框然后放下去啦。
- 除了加载数据,你还需要填好页表,如果你使用有效位数组的话还需要填好有效位数组。
- 变量:vPageNo 虚拟页号
1. 加载页数据
- 使用该虚页的起始地址作为访问磁盘的地址,起始地址=虚页号×一页大小
- 由于一页的大小是4KB,需要把虚拟页号乘以2的12次方,
- 即转为二进制作为地址,二进制后面加12个0
- 从磁盘读出一页数据data。(长度为PAGE_SIZE_B页长)
2. 写入内存
- 寻找空闲内存的物理页框——遍历valid找false。
- 范围
0 ~ pageValid.length
,- 当pageValid[i]为false,说明不在内存中,即空闲,将其改为占用,并记录页框号frameNO。
- 接着将数据装入页框。将页框号×页大小,获得物理地址,将数据写入内存。(长度为PAGE_SIZE_B页长)
3. 填页表
- 由虚拟页号获得该页(getPageItem(vPageNo)),
- 将pageFrame设为页框号二进制的低20位转成的数组(物理页框号)
- 将isInMem设为true。(装入了内存)
- 填好有效位(刚刚占用的设为true)
1 | /** |
修改seg_load方法。
- 因此开启分页之后,seg_load应该跳过加载数据这一步,它的作用在开启分页之后仅仅是填写GDT,加载数据的任务应该交给page_load来完成。
- 即if(!PAGE) {加载数据}
MMU类:toPagePhysicalAddr页级地址转换方法
- 在段页式下,线性地址转物理地址需要查页表,然后进行虚拟页号到物理页号的替换,具体流程可以参考课件。
- 从线性地址中提取前 20 位作为虚拟页号,后 12 位作为页内偏移。
- 物理页号的获取(TLB是否可用):都是使用
getFrameOfPage
函数,区别在于类不同- 如果开启了tlb,TLB
- 调用tlb类的函数,由虚拟页号获取
- 未开启,内存
- 调用Memory类的函数,由虚拟页号获取
- 如果开启了tlb,TLB
- 最后,将物理页号与页内偏移拼接获得物理地址。
1 | /** |
4. cache与TLB的融合
a. 将cache融合进MMU中
- 这一步相对简单。需要注意,由于cache是memory的缓存,所以任何涉及到访问主存数据的地方都要添加对cache的调用。
- 只有两个标了todo的地方需要改,MMU类的read和write。直接加入当cache有效时,通过cache读/写即可。
1 | public byte[] read(String logicAddr, int length) { |
b. 将TLB融合进MMU中
- MMU类
addressTranslation
函数。在标出的地方判断- 若TLB有效:若tlb中没有该页且内存中没有该页,缺页中断
- 内存从磁盘加载该页的数据。
- 并且将该页写入tlb。
tlb.write(i);
- 若TLB无效:直接判断内存中有没有该页(应该是有代码的)
- 若TLB有效:若tlb中没有该页且内存中没有该页,缺页中断
1 | // TODO: add tlb here |
toPagePhysicalAddr
方法,上面讲过了。
1 | if(TLB.isAvailable){ |
If you like my blog, you can approve me by scanning the QR code below.