大家好,欢迎来到IT知识分享网。
实验思考题
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
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
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_next
而field
事实上就是包含两个指针*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 Page
、struct Env
的空间及各自的二级页表。Exercise 2.8和2.9将物理页面和虚拟页面结合起来了,分配物理页面,可以让链表减少对应的节点,并让页表增加对应的表项。
物理存储Exercise主要是对struct Page
免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://yundeesoft.com/31718.html