计算机组成原理Ⅱ课程设计 PA3.2

计算机组成原理Ⅱ课程设计 PA3.2

目录

思考题

  1. 一些问题

    • 高20位是基地址,低12位是页内偏移量,加在一起一共32位

    • 物理地址是必须的。在虚拟地址转化为虚拟地址时,需要根据 CR3 寄存器得到页目录表基址,若CR3为虚拟地址,则会现如死循环

    • (1)使用多级页表可以使得页表在内存中离散存储。多级页表实际上是增加了索引,有了索引就可以定位到具体的项。举个例子:比如虚拟地址空间大小为4G,每个页大小依然为4K,如果使用一级页表的话,共有2^20个页表项,如果每一个页表项占4B,那么存放所有页表项需要4M,为了能够随机访问,那么就需要连续4M的内存空间来存放所有的页表项。随着虚拟地址空间的增大,存放页表所需要的连续空间也会增大,在操作系统内存紧张或者内存碎片较多时,这无疑会带来额外的开销。但是如果使用多级页表,我们可以使用一页来存放页目录项,页表项存放在内存中的其他位置,不用保证页目录项和页表项连续。

      (2)使用多级页表可以节省页表内存。使用一级页表,需要连续的内存空间来存放所有的页表项。多级页表通过只为进程实际使用的那些虚拟地址内存区请求页表来减少内存使用量(出自《深入理解Linux内核》第三版51页)。举个例子:一个进程的虚拟地址空间是4GB,假如进程只使用4MB内存空间。对于一级页表,我们需要4M空间来存放这4GB虚拟地址空间对应的页表,然后可以找到进程真正使用的4M内存空间。也就是说,虽然进程实际上只使用了4MB的内存空间,但是为了访问它们我们需要为所有的虚拟地址空间建立页表。但是如果使用二级页表的话,一个页目录项可以定位4M内存空间,存放一个页目录项占4K,还需要一页用于存放进程使用的4M(4M=1024*4K,也就是用1024个页表项可以映射4M内存空间)内存空间对应的页表,总共需要4K(页表)+4K(页目录)=8K来存放进程使用的这4M内存空间对应页表和页目录项,这比使用一级页表节省了很多内存空间。当然,在这种情况下,使用多级页表确实是可以节省内存的。但是,我们需要注意另一种情况,如果进程的虚拟地址空间是4GB,而进程真正使用的内存也是4GB,如果是使用一级页表,则只需要4MB连续的内存空间存放页表,我们就可以寻址这4GB内存空间。而如果使用的是二级页表的话,我们需要4MB内存存放页表,还需要4KB内存来存放页目录项,此时多级页表反倒是多占用了内存空间。注意在大多数情况都是进程的4GB虚拟地址空间都是没有使用的,实际使用的都是小于4GB的,所以我们说多级页表可以节省页表内存。

      https://blog.csdn.net/ibless/article/details/81275009

  2. 空指针真的是'空'的吗?

    空指针应该也表示一个虚拟地址,只不过在虚拟地址转换为物理地址后,对应的物理地址会触发空指针解引用的错误。

  3. 理解_map函数

    void _map(_Protect *p, void *va, void *pa) {
      PDE *pt = (PDE*)p->ptr;
      PDE *pde = &pt[PDX(va)];
      if (!(*pde & PTE_P)) {
        *pde = PTE_P | PTE_W | PTE_U | (uint32_t)palloc_f();
      }
      PTE *pte = &((PTE*)PTE_ADDR(*pde))[PTX(va)];
      if (!(*pte & PTE_P)) {
        *pte = PTE_P | PTE_W | PTE_U | (uint32_t)pa;
      }
    }
    

    通过 p->ptr 可以获取页目录的基地址,然后根据传入的虚拟地址和基地址得到页目录项。判断是否需要申请新的页表,若需要,则可以通过回调函数 palloc_f() 向 Nanos-lite 获取一页空闲的物理页,把刚刚申请的物理页地址与其他标志位一并存入一个页目录里。使用这个页目录再申请一个页表项,最后把物理地址连同标志位一起放入这个页表项。

  4. 内核映射的作⽤

    pd_val.present报错。因为没有进行内核映射拷贝,所以没有页表来存放对应的内核区的虚拟地址,就会出现这种报错。

  5. git log 和 远程仓库截图

实验内容

1 添加分页控制相关寄存器


  • CR0, CR3 寄存器的定义

    引入头文件

    #include "memory/mmu.h"
    

    CPU_state中添加两个寄存器

    CR0 cr0;
    CR3 cr3;
    
  • 初始化 CR0, CR3 寄存器

    monitor.c中的restart()中添加

    cpu.cr0.val=0x60000011;
    

2. 修改访存函数


按照讲义的样子修改即可。

要判断是否出现数据跨越页的边界的情况,可以计算本数据地址加上读取数据长度是否比一页大即可。我们知道一页的大小为4KB,即4096=0x1000。我们可以用addr对4096取余来得到页的起始位置,然后将这个起始位置与len相加再与0x1000比较就可以知道是否跨页了。(addr%4096 <=> addr&0xFFF)

  • vaddr_read()

    uint32_t vaddr_read(vaddr_t addr, int len) {
      if(cpu.cr0.paging) {
            if ((addr&0xFFF)+len>0x1000) {
                /* this is a special case, you can handle it later. */
                assert(0);
            }
            else {
                paddr_t paddr = page_translate(addr,false);
                return paddr_read(paddr, len);
            }
        }
        else
          return paddr_read(addr,len);
    }
    
  • vaddr_write()

    void vaddr_write(vaddr_t addr, int len, uint32_t data) {
      if(cpu.cr0.paging) {
            if ((addr&0xFFF)+len>0x1000) {
                /* this is a special case, you can handle it later. */
                assert(0);
            }
            else {
                paddr_t paddr = page_translate(addr,true);
                return paddr_write(paddr, len, data);
            }
        }
        else
          return paddr_write(addr, len, data);
    }
    

3. page_translate()


按照讲义要求:

  • 该函数用于地址转换,传入虚拟地址作为参数,函数返回值为物理地址

  • 该函数的实现过程即为我们理论课学到的页级转换过程(先找页目录项,然后取出);

  • 注意使用 assert 来验证 present 位,否则会造成调试困难

  • PDEPTE 的数据结构框架已帮我们定义好,在 mmu.h 中;

  • 注意每个页目录想和每个页表项存储在内存中的地址均为物理地址,使用 paddr_read 去读取,如果使用 vaddr_read 去读取会造成死递归(为什么?);

  • 此外,还需要实现访问位和脏位的功能;

  • 需要在 page_translate 中插入 Log 并截图表示实现成功(截图后可去除 Log 以免影响性能);

  • 如何编写这个函数?

  • 根据 CR3 寄存器得到页目录表基址(是个物理地址);

  • 用这个基址和从虚拟地址中隐含的

    页目录
    

    字段项结合计算出所需页目录项地址(是个物理地址);

    • 请思考一下这里所谓的“结合”需要经过哪些处理才能得到正确地地址呢?
  • 从内存中读出这个页目录项,并对有效位进行检验;

  • 将取出的 PDE 和虚拟地址的页表字段相组合,得到所需页表项地址(是个物理地址);

  • 从内存中读出这个页表项,并对有效位进行检验;

  • 检验 PDEaccessed 位,如果为 0 则需变为 1,并写回到页目录项所在地址;

  • 检验 PTEaccessed 位如果为 0,或者 PTE 的脏位为 0 且现在正在做写内存操作,满足这两个条件之一时需要将 accessed 位,然后更新 dirty 位,最后并写回到页表项所在地址;

  • 页级地址转换结束,返回转换结果(是个物理地址).

并结合视频里王助教的小例子,很容易就实现

paddr_t page_translate(vaddr_t vaddr,bool isWrite) {

  Log("addr:0x%x\n",vaddr);
  //cr3高20位
  vaddr_t CR3 = cpu.cr3.page_directory_base<<12;
  Log("CR3:0x%x\n",CR3);
  //vaddr高10位
  vaddr_t dir = (vaddr>>22)*4;
  Log("dir:0x%x\n",dir);
  //取出cr3的高20位与vaddr的高8位结合
  paddr_t pdAddr = CR3 + dir;
  Log("pdAddr:0x%x\n",pdAddr);
  //读取
  PDE pd_val;
  pd_val.val = paddr_read(pdAddr,4);
  Log("pdAddr:0x%x  pd_val0x%x\n",pdAddr,pd_val.val);
  assert(pd_val.present);

  //获取高20位
  vaddr_t t1 = pd_val.page_frame<<12;

  //获取第二个十位page
  vaddr_t t2 = ((vaddr>>12)&0x3FF)*4;

  paddr_t ptAddr = t1 + t2;
  Log("pt_addr:0x%x\n",ptAddr);
  PTE pt_val;
  pt_val.val = paddr_read(ptAddr,4);
  assert(pt_val.present);
  Log("pt_addr:0x%x  pt_val:0x%x\n",ptAddr,pt_val.val);
  //获取高20位
  t1 = pt_val.page_frame<<12;

  //获取最后12位
  t2 = vaddr&0xFFF;

  paddr_t paddr = t1 + t2;
  Log("paddr:0x%x\n",paddr);

  pd_val.accessed = 1;
  paddr_write(pdAddr,4,pd_val.val);

  if ((pt_val.accessed == 0) || (pt_val.dirty ==0 && isWrite)){
    pt_val.accessed=1;
    pt_val.dirty=1;
  }
  paddr_write(ptAddr,4,pt_val.val);

  return paddr;

}

发现有指令没有实现

mov_r2cr() mov_cr2r()

填表

/* 0x20 */	IDEX(mov_G2E,mov_cr2r), EMPTY, IDEX(mov_E2G,mov_r2cr), EMPTY,

由于我们只用到了cr0 cr3,因此只需要写这两个就行

make_EHelper(mov_r2cr) {//给cr寄存器赋值
  switch (id_dest->reg)
  {
    case 0:
      cpu.cr0.val = id_src->val;
      break;
    case 3:
      cpu.cr3.val = id_src->val;
      break;
    default:
      assert(0);
      break;
  }

  print_asm("movl %%%s,%%cr%d", reg_name(id_src->reg, 4), id_dest->reg);
	}
make_EHelper(mov_cr2r) {//给寄存器赋值
  switch (id_dest->reg)
  {
    case 0:
      id_src->val = cpu.cr0.val;
      break;
    case 3:
      id_src->val = cpu.cr3.val;
      break;
    default:
      assert(0);
      break;
  }

  print_asm("movl %%cr%d,%%%s", id_src->reg, reg_name(id_dest->reg, 4));

#ifdef DIFF_TEST
  diff_test_skip_qemu();
#endif
}

4. 修改 loader()


navy-apps/Makefile.compile中的链接地址 -Ttext 参数改为 0x8048000重新编译

nanos-lite/src/loader.c中的中的 DEFAULT_ENTRY 也需要作相应的修改

#define DEFAULT_ENTRY ((void *)0x8048000)

nanos-lite/src/main.c中进行如下操作,别忘了声明函数load_prog()

-  uintptr_t entry = loader(NULL, "/bin/pal");-  ((void (*)(void))entry)();+  load_prog("/bin/dummy");

按要求修改loader():

  1. 打开待装入的文件后,还需要获取文件大小;
  2. 需要循环判断是否已创建足够的页来装入程序;
  3. 对于程序需要的每一页,做三个事情,即4,5,6步:
  4. 使用 Nanos-liteMM 提供的 new_page() 函数获取一个空闲物理页
  5. 使用映射函数 _map() 将本虚拟空间内当前正在处理的这个页和上一步申请到的空闲物理页建立映射
  6. 读一页内容,写到这个物理页上
  7. 每一页都处理完毕后,关闭文件,并返回程序入口点地址(虚拟地址)
uintptr_t loader(_Protect *as, const char *filename) {
  //ramdisk_read(DEFAULT_ENTRY,0,get_ramdisk_size());

  //读取文件位置
  int index=fs_open(filename,0,0);
  //读取长度
  size_t length=fs_filesz(index);
  //读取内容
  //fs_read(index,DEFAULT_ENTRY,length);

  void *va;
  void *pa;
  int page_count = length/4096 + 1;//获取页数量
  
  for (int i=0;i<page_count;i++){
    va = DEFAULT_ENTRY + 4096*i;
    pa = new_page();
    Log("Map va to pa: 0x%08x to 0x%08x",va,pa);
    _map(as,va,pa);
    fs_read(index,pa,4096);
  }
  //关闭文件
  fs_close(index);
  return (uintptr_t)DEFAULT_ENTRY;
}

重点注意一页的大小是4K,所以计算页数时要除4096

运行后发现vaddr_read()报错了,推测是出现数据跨越虚拟页边界的情况

根据视频所讲,我们可以分别读出两页的内容,再按小端方式组合即可。注意要使用page_translate()将虚拟地址转化为物理地址再去读取,最后小端方式组合数据时,就是把第二页的数据放在高位。(这里的affr&0xFFF相当于addr对4096取余,即获取本页开始位置

uint32_t vaddr_read(vaddr_t addr, int len) {
  if(cpu.cr0.paging) {
        if ((addr&0xFFF)+len>0x1000) {
            /* this is a special case, you can handle it later. */
            int len1,len2;
            len1 = 0x1000-(addr&0xfff);//获取前一页的占用空间
            len2 = len - len1;//获取后一页的占用空间

            paddr_t addr1 = page_translate(addr,false);//虚拟地址转换为物理地址
            uint32_t data1 = paddr_read(addr1,len1);//读取内容

            paddr_t addr2 = page_translate(addr+len1,false);//虚拟地址转换为物理地址
            uint32_t data2 = paddr_read(addr2,len2);//读取内容

            //len1<<3表示获取data1的位数
            uint32_t data = (data2<<(len1<<3))+data1;//把data2的数据移到高位,组合读取到的内容。
            return data;
        }
        else {
            paddr_t paddr = page_translate(addr,false);
            return paddr_read(paddr, len);
        }
    }
    else
      return paddr_read(addr,len);
}
void vaddr_write(vaddr_t addr, int len, uint32_t data) {
  if(cpu.cr0.paging) {
        if ((addr&0xFFF)+len>0x1000) {
            /* this is a special case, you can handle it later. */
            int len1,len2;
            len1 = 0x1000-(addr&0xfff);//获取前一页的占用空间
            len2 = len - len1;//获取后一页的占用空间

            paddr_t addr1 = page_translate(addr,true);//虚拟地址转换为物理地址
            paddr_write(addr1,len1,data);//写入内容

            data2 = data >> (len1<<3);
            paddr_t addr2 = page_translate(addr+len1,true);
            paddr_write(addr2,len2,data2);
        }
        else {
            paddr_t paddr = page_translate(addr,true);
            return paddr_write(paddr, len, data);
        }
    }
    else
      return paddr_write(addr, len, data);
}

成功运行

5. 在分页上运行仙剑奇侠传


mm_brk()框架已经帮忙实现

之后把原来系统调用sys_brk()换成mm_brk()就行

case SYS_brk:      SYSCALL_ARG1(r)=mm_brk(SYSCALL_ARG2(r));      break;

遇到的问题及解决办法

  1. 遇到问题:不知道如何判断跨页的情况

    解决方案:读了好几遍讲义,发现讲义里有介绍页面大小为4K,那么就可以通过取余获取起使位置。后来,我又通过查看i386,了解到表项的后12位是页内偏移量,则可以&0xFFF来获得起始位置。

实验心得

本次实验内容不多,主要了解分页机制,把虚拟地址转换为物理地址即可。主要是看了王助教的视频,有了那几个例子的讲解,看讲义以及实现就轻松多了。

其他备注

助教真帅

posted @ 2021-06-20 22:29  干饭人er  阅读(878)  评论(0)    收藏  举报