北航操作系统课程lab2实验报告

lab2 OS实验报告

实验思考题

Thinking 2.1

指针变量存储的是虚拟地址,MIPS汇编程序中使用的也是虚拟地址。因为实验使用的R3000 CPU只会发出虚拟地址,然后虚拟地址映射到物理地址,使用物理地址进行访存。

Thinking 2.2

宏的一个本身的特性就是可重用,跟函数一样,可以将一段代码封装成一条语句。当这段代码的具体实现需要更改时,只需要改宏这一处就行。宏相比函数也更加轻便,可以用于结构体定义等,由于是字符串的替换,因此不必进行地址的跳转和栈的保存,但值得注意的是在编写宏的时候需要着重注意语法是否有漏洞。此外do/while(0)的架构也大大方便了调用这些宏,可以直接将其当做函数看待。

在实验环境中,只看到了单向链表、双向链表、单向队列、双向队列、循环队列,感觉循环队列在插入和删除操作方面和循环链表没太大差异,据此进一步分析。

插入操作:单向链表插入操作十分简单,两行代码,双向链表插入操作一般运行四行代码,需要额外判断是否next指向了NULL,循环链表与双向链表运行代码量基本相等,需额外判断是否next指向了头指针。特别的是,插入到头结点对三种链表而言性能相似,单向链表与双向链表插入到尾结点均要遍历完整个链表。

删除操作:单向链表的删除操作复杂度为O(n),因为需要靠循环才能找到上一个链表节点的位置,双向链表及循环链表的删除操作与插入性能相近,也还是需要额外判断NULL或HEAD。删除头结点对三种链表而言性能相似,而单向链表与双向链表删除尾结点还是要遍历。

Thinking 2.3

 typedef LIST_ENTRY(Page) Page_LIST_entry_t;
 ​
 struct Page {
     Page_LIST_entry_t pp_link;    /* free list link */
 ​
     // Ref is the count of pointers (usually in page table entries)
     // to this page.  This only holds for pages allocated using
     // page_alloc.  Pages allocated at boot time using pmap.c's "alloc"
     // do not have valid reference count fields.
 ​
     u_short pp_ref;
 };
 ​
 #define LIST_HEAD(name, type)                                               \
         struct name {                                                           \
                 struct type *lh_first;  /* first element */                     \
         }
 ​
 #define LIST_ENTRY(type)                                                    \
         struct {                                                                \
                 struct type *le_next;   /* next element */                      \
                 struct type **le_prev;  /* address of previous next element */  \
         }
 ​

答案选C。Page_list中含有的是Page结构体指针头。每一个Page内存控制块都有一个pp_ref用于表示其引用次数(为0时便可remove),还有一个结构体用于存放实现双向链表的指针。

Thinking 2.4

 //在boot_map_segment()函数中调用到了boot_pgdir_walk()函数
 //以此得到虚拟地址所对应的二级页表项
 pgtable_entry = boot_pgdir_walk(pgdir, va_temp, 1); //create 
 ​
 //在mips_vm_init()函数中调用到了boot_map_segment函数
 boot_map_segment(pgdir, UPAGES, n, PADDR(pages), PTE_R);
 boot_map_segment(pgdir, UENVS, n, PADDR(envs), PTE_R);
 //alloc已经分配好了虚拟地址
 //boot_map_segment分别将页面结构体与进程控制块结构体的虚拟地址映射成物理地址

Thinking 2.5

ASID的必要性:同一虚拟地址在不同地址空间中通常映射到不同物理地址,ASID可以判断是在哪个地址空间。例如有多个进程都用到了这个虚拟地址,但若该虚拟地址对应的数据不是共享的,则基本可以表明指向的是不同物理地址,这也是一种对地址空间的保护。

可容纳不同地址空间的最大数量:64个,参考原文如下:

Instead, the OS assigns a 6-bit unique code to each task’s distinct address space. Since the ASID is only 6 bits long, OS software does have to lend a hand if there are ever more than 64 address spaces in concurrent use; but it probably won’t happen too often.

Thinking 2.6

tlb_invalidate调用tlb_out

调用tlb_invalidate可以将该地址空间的虚拟地址对应的表项清除出去,一般用于这个虚拟空间引用次数为0时释放tlb空间

 LEAF(tlb_out)
 //1: j 1b
 nop
     mfc0    k1,CP0_ENTRYHI  //保存ENTRIHI原有值
     mtc0    a0,CP0_ENTRYHI  //将传进来的参数放进ENTRYHI中
     nop
     tlbp// insert tlbp or tlbwi //检测ENTRYHI中的虚拟地址在tlb中是否有对应项
     nop
     nop
     nop
     nop
     mfc0    k0,CP0_INDEX    //INDEX可以用来判断是否命中
     bltz    k0,NOFOUND  //若未命中,则跳转
     nop
     mtc0    zero,CP0_ENTRYHI    //将ENTRYHI清零
     mtc0    zero,CP0_ENTRYLO0   //将ENTRYLO清零
     nop
     tlbwi// insert tlbp or tlbwi    //将清零后的两寄存器值写入到对应tlb表项中
                                     //相当于删除原有的tlb表项
 NOFOUND:
 ​
     mtc0    k1,CP0_ENTRYHI  //将原来的ENTRYHI恢复
     
     j   ra  //return address
     nop
 END(tlb_out)

Thinking 2.7

页表基地址(page table) 为PTbase

页中间目录基地址(page middle directory) PMDbase:

(PTbase << 12) >> 3 + PTbase

页全局目录(page global directory) PGDbase:

(PTbase << 21) >> 3 + PMDbase(三级页表页目录的基地址)

页全局目录项(page global directory entry)PGDE:

(PTbase << 30) >> 3 + PGDbase(映射到页目录自身的页目录项)

Thinking 2.8

X86用到三个地址空间的概念:物理地址、线性地址和逻辑地址。而MIPS只有物理地址和虚拟地址两个概念。相对而言,段机制对大量应用程序分散地使用大内存的支持能力较弱。所以Intel公司又加入了页机制,每个页的大小是固定的(一般为4KB),也可完成对内存单元的安全保护,隔离,且可有效支持大量应用程序分散地使用大内存的情况。x86体系中,TLB表项更新能够由硬件自己主动发起,也能够有软件主动更新。

分段机制和分页机制都启动:逻辑地址--->段机制处理--->线性地址--->页机制处理--->物理地址

RISC-V提供三种权限模式(MSU),而MIPS只提供内核态和用户态两种权限状态。RISC-V SV39支持39位虚拟内存空间,每一页占用4KB,使用三级页表访存。

实验难点展示

填写代码的主要难点在于对C语言指针的运用理解,同时也需要了解一些宏的知识,并且要记住不同的宏可以用来做什么事。

Exercise 2.2

编写代码如下

 /* Exercise 2.2 */
 /*
  * Insert the element "elm" *after* the element "listelm" which is
  * already in the list.  The "field" name is the link element
  * as above.
  */
 #define LIST_INSERT_AFTER(listelm, elm, field) do{    \
     LIST_NEXT((elm), field) = LIST_NEXT((listelm), field);  \
         if (LIST_NEXT((listelm),field) != NULL)         \
             LIST_NEXT((listelm), field)->field.le_prev = &LIST_NEXT((elm), field);  \
         LIST_NEXT((listelm), field) = (elm);    \
         (elm)->field.le_prev = &LIST_NEXT((listelm), field);    \
     } while(0)
         // Note: assign a to b <==> a = b
         //Step 1, assign elm.next to listelm.next.
         //Step 2: Judge whether listelm.next is NULL, if not, then assign listelm.next.pre to a proper value.
         //step 3: Assign listelm.next to a proper value.
         //step 4: Assign elm.pre to a proper value.
 ​
 /*
  * Insert the element "elm" at the tail of the list named "head".
  * The "field" name is the link element as above. You can refer to LIST_INSERT_HEAD.
  * Note: this function has big differences with LIST_INSERT_HEAD !
  */
 #define LIST_INSERT_TAIL(head, elm, field) do { \
                 if (LIST_FIRST((head)) != NULL) { \
                         LIST_NEXT((elm), field) = LIST_FIRST((head)); \
                         while (LIST_NEXT(LIST_NEXT((elm), field), field) != NULL) {  \
                             LIST_NEXT((elm), field) = LIST_NEXT(LIST_NEXT((elm), field), field); \
                         } \
                         LIST_NEXT(LIST_NEXT((elm), field), field) = (elm); \
                         (elm)->field.le_prev = &LIST_NEXT(LIST_NEXT((elm), field), field); \
                         LIST_NEXT((elm), field) = NULL; \
                 } else \
                     LIST_INSERT_HEAD((head), (elm), field); \
         } while (0)
 ​

结构示意图如上,每一个框其实就是可以清晰地看到后者的le_prev指针指向的是前者的le_next地址。这个地址下的值类型是一个指向后者结构体的指针。也即le_prev = &le_next。在我看来指针的指针在删除节点时可以少做更快捷,但增加了读代码的难度,或许会有点多此一举。

LIST_NEXT((elm), field)这个宏实际上就是表示的elm结构体指向的下一个结构体(elm)->field.le_nextfield事实上就是包含两个指针*le_next**le_prev的结构体,感觉也是有点绕。这么绕的这些指令还真就促成了一些易懂的表达式,le_prev = &LIST_NEXT((elm), field)其实质就是le_prev = &le_next

Exercise 2.3

 void page_init(void)
     //对物理页面控制块进行操作
     //以下是最重要的两个部分
     struct Page *now;
     for (now = pages; page2kva(now) < freemem; now++) {
         now -> pp_ref = 1;
     }//将已分配好的页面引用次数置1
     for (; page2ppn(now) < npage; now ++) {
         now -> pp_ref = 0;
         LIST_INSERT_HEAD(&page_free_list, now, pp_link);
     }//将未分配的页面引用次数置0,并加入到空闲列表中

Exercise 2.4

 int page_alloc(struct Page **pp)
     //用于分配物理页面
     ppage_temp = LIST_FIRST(&page_free_list);
     //得到空闲列表头的一个页面
     LIST_REMOVE(ppage_temp, pp_link);
     //因为要分配了,所以在原有空闲列表头中删掉

这个list其实就是物理内存的链表了,此时建立了内存管理,故可用链表进行物理内存的分配,相比于alloc的顺序分配不同。

Exercise 2.6

 static Pte *boot_pgdir_walk(Pde *pgdir, u_long va, int create)
     //用于得到二级页表的地址
     //……
     *pgdir_entryp = PADDR(alloc(BY2PG,BY2PG,1));    //allocate one page
     //得到一级表项中二级表项的物理地址(PADDR将低12位标志位清除)
     *pgdir_entryp = *pgdir_entryp | PTE_V | PTE_R;  //set valid and dirty bit
     //一级表项中低12位用于设置标志位
     //向一级页表项中填入所在二级页表物理地址及标志位

Exercise 2.7

 void boot_map_segment(Pde *pgdir, u_long va, u_long size, u_long pa, int perm)
     //用于将实页物理地址存到二级页表项中
     //……
     for (i = 0, size = ROUND(size, BY2PG); i < size; i += BY2PG) { 
     //Step 1. use `boot_pgdir_walk` to "walk" the page directory \*/* 
     pgtable_entry = boot_pgdir_walk(pgdir,va + i, 1);
         /* create if entry of page directory not exists yet  
          * 把二级页表项的地址找出来 */
     //Step 2. fill in the page table
     *pgtable_entry = (PTE_ADDR(pa + i)/* III. Physical Frame Address of `pa + i`*/| perm | PTE_V; 
     //向二级页表项中填入所在页物理地址及标志位
 } 

值得一提的是,boot_pgdir_walk()在一级页表项中填入了二级页表头的物理地址,并返回了虚拟地址va的对应二级页表项虚拟地址,完成页目录的初始化。boot_map_segment()在二级页表项中填入了实页的物理地址,完成页表的初始化。

Exercise 2.8

 int pgdir_walk(Pde *pgdir, u_long va, int create, Pte **ppte)
     //用于得到二级页表的地址
     *pgdir_entryp = (page2pa(ppage)/* Physical Address of `page` */) | PTE_V | PTE_R;
     ppage->pp_ref++; // 因为该页被分配了,所以引用次数++

Exercise 2.9

 int page_insert(Pde *pgdir, struct Page *pp, u_long va, u_int perm)
     //用于将实页物理地址存到二级页表项中
     pgdir_walk(pgdir, va, 0, &pgtable_entry);
     //把二级页表项的地址找出来
     tlb_invalidate(pgdir, va);
     //update tlb
     *pgtable_entry = page2pa(pp) | PERM;
     //将实页物理地址和标志位放进去
     pp->pp_ref++;
     //该页被分配,引用次数++

启动时区间地址映射函数是用返回值返回二级页表项虚拟地址,而运行时区间地址映射函数是直接用指针作为参数传递该地址,取而代之返回了一个是否运行失败的int值。

体会与感想

感觉好难……就很乱,记不住呀。不太懂tlb是怎么形成的,只会一个tlb_invalidate使tlb表项无效的一个函数。

Exercise 2.1和2.2属于准备工作,用宏定义链表为后面的代码重用带来了巨大的方便,而且宏名字也是很清晰简洁的。Exercise 2.3—2.5也是为struct Page的链表做准备,书写了管理物理页面的链表的一个方法。Exercise 2.6和2.7用于初始化两级页表,这是在mips_vm_init()中调用的,分配一级页表和struct Pagestruct Env的空间及各自的二级页表。Exercise 2.8和2.9将物理页面和虚拟页面结合起来了,分配物理页面,可以让链表减少对应的节点,并让页表增加对应的表项。

物理存储Exercise主要是对struct Page进行处理,虚拟存储Exercise主要是对页表项赋值

posted @ 2022-04-13 21:11  南风北辰  阅读(2734)  评论(2编辑  收藏  举报