Lab2内存管理实验报告

您所在的位置:网站首页 虚拟内存好处和坏处 Lab2内存管理实验报告

Lab2内存管理实验报告

2024-07-12 06:49| 来源: 网络整理| 查看: 265

Lab2实验报告 本次明确

实验要做的操作系统是32位的,按字节编址

采用链表来管理空闲内存空间

相同类型数据的指针相减得到的是其中间相差的该类型数据个数

对指针进行加的含义并非对地址进行加,而是加对应倍的指针类型数据大小。

操作系统运行在虚拟内存空间中,过程中使用的所有的地址都是虚拟地址。

物理内存与内核虚拟空间实际上就是简单的线性映射。

实验思考题

Thinking2.1 请思考cache用虚拟地址来查询的可能性,并且给出这种方式对访存带来的好处和坏处。另外,你能否能根据前一个问题的解答来得出用物理地址来查询的优势?

不同的程序可能拥有相同的虚拟地址,cache若利用虚拟地址来查询,那么根据虚拟地址与物理地址映射的计算参数,我们需要再给出虚拟地址的同时给出该程序的独特标记码,否则会造成访存错误;这样一来就增大了cache的复杂度,所以这种方法是可能的,但并不是可取的方案。好处:不经过MMU中TLB的地址映射,若再cache中命中则大大节省了访存时间坏处:要不增加了cache的复杂度,要不可能造成访存失败用物理地址访存的优势:能够避免因不同程序有相同虚拟地址而造成的访存失败。

Thinking2.2 在我们的实验中,有许多对虚拟地址或者物理地址操作的宏函数(详见include/mmu.h ),那么我们在调用这些宏的时候需要弄清楚需要操作的地址是物理地址还是虚拟地址,阅读下面的代码,指出x是一个物理地址还是虚拟地址。

x是一个虚拟地址,c程序中的地址均为虚拟地址。

Thinking2.3 我们在 include/queue.h 中定义了一系列的宏函数来简化对链表的操作。实际上,我们在 include/queue.h 文件中定义的链表和 glibc 相关源码较为相似,这一链表设计也应用于 Linux 系统中 (sys/queue.h 文件)。请阅读这些宏函数的代码,说说它们的原理和巧妙之处。

原理:宏函数在预处理的时候会将相应的位置替换成对应的宏,然后对其中的一些参数做替换。

具体宏代码解读:

#define LIST_HEAD(name, type) \ struct name{ \ struct type *lh_first; \ }

该宏函数为定义链表头指针结构的函数,参数name为链表头指针名,参数type为链表节点的类型。

#define LIST_HEAD_INITIALIZER(head) {NULL}

该宏函数将一个链表头指针指向一个空链表,用于初始化一个链表

#define LIST_ENTRY(type) \ struct { \ struct type *le_next; \ struct type **le_prev; \ }

定义了链表节点的link结构体,le_prev为指向前一个节点结构体的link结构体中le_next指针的指针,le_next为指向下一个节点结构体的指针。这样方便我们进行对当前结构体的删除操作,只需使用*le_prev = le_next,就能够将当前节点与链表解除关系

#define LIST_EMPTY(head) ((head)->lh_first == NULL)

链表是否为空,若为空返回1,否则返回0

#define LIST_FIRST(head) ((head)->lh_first)

取出链表的第一个节点

#define LIST_FOREACH(var, head, field) \ for ((var) = LIST_FIRST((head)); (var); \ (var) = LIST_NEXT((var), field))

遍历头指针为head的链表中的全部节点,field为节点中的link的结构体

#define LIST_INIT(head) \ do{ \ LIST_FIRST((head)) = NULL; \ } while(0)

为链表头指针初始化一个空链表

#define LIST_NEXT(elm, field) ((elm)->field.le_next)

取出链表中当前节点的指向下一个节点的指针

#define LIST_REMOVE(elm, field) \ do{ \ if(LIST_NEXT((elm), field) != NULL) \ LIST_NEXT((elm), field)->field.le_prev = \ (elm)->field.le_prev; \ *(elm)->field.le_prev = LIST_NEXT((elm), field); \ }while(0)

从链表中安全移除节点elm

#define LIST_INSERT_BEFORE(listelm, elm, field) \ do{ \ (elm)->field.le_prev = (listelm)->field.le_prev; \ LIST_NEXT((elm), field) = (listelm); \ *(listelm)->field.le_prev = (elm); \ (listelm)->field.le_prev = &LIST_NEXT((elm), field); \ }while(0)

在链表中listelm节点前面增加一个节点elm

#define LIST_INSERT_HEAD(head, elm, field) \ do{ \ if((LIST_NEXT((elm), field) = LIST_FIRST((head))) != NULL) \ LIST_FIRST((head))->field.le_prev = &LIST_NEXT((elm), field);\ LIST_FIRST((head)) = (elm); \ (elm)->field.le_prev = &LIST_FIRST((head)); }while(0)

在链表头加一个节点

#define TAILQ_HEAD(name, type) \ struct name{ \ struct type* tqh_first; \ struct type** tqh_last; \ }

定义队列的头节点指针结构体,该结构体中tqh_first为指向队首节点的指针,tqh_last为指向队尾节点

#define TAILQ_ENTRY(type) \ struct{ \ struct type* tqh_next; \ struct type** tqh_prev; \ }

定义节点的link结构体,其中tqh_next为指向下一个结构体的指针,tqh_prev为指向前一个结构体的link结构体中的tqh_next指针的指针。与上面链表定义类似

Thinking2.4 我们注意到我们把宏函数的函数体写成了 do { /* … */ } while(0)的形式,而不是仅仅写成形如 { /* … */ } 的语句块,这样的写法好处是什么?

辅助定义复杂的宏,避免引用时出错,保证调用该宏时,所有的语句都能在被调用处执行,例如如下宏定义:

#define DO() \ do1(); \ do2();

若我们这样调用:

if(start) DO()

那么,可能就会出现问题,do2();在判断条件的控制域之外了,一定会被执行。

避免;使用出现问题,使用上一个宏定义(可能有人习惯在写一个函数后面加上分号),若我们调用函数这样写:

if(start) DO();

这是我们习惯的写法,但是会编译报错,因为多了个分号……你可能会说定义宏的时候不要在最后加分号就好了呗,但是当我们定义一个复杂功能函数的宏函数时,很可能会在每一个语句后面都加一个分号,与其小心翼翼,不如使用一个规范的形式避免这种情况。

避免空宏引起的Warning

内核中由于不同架构的限制,很多时候会用到空宏,为避免编译时空宏报错,我们使用一个do{} while(0)即可实现,但是这个我暂时没有接触到。

Thinking2.5 注意,我们定义的 Page 结构体只是一个信息的载体,它只代表了相应物理内存页的信息,它本身并不是物理内存页。 那我们的物理内存页究竟在哪呢?Page 结构体又是通过怎样的方式找到它代表的物理内存页的地址呢? 请你阅读 include/pmap.h 与 mm/pmap.c 中相关代码,并思考一下。

物理内存页存在物理内存中Page结构体通过与mips_vm_init()函数中初始化得到的pages数组首地址做差,得到该页对应的物理页号,然后将物理页号左移12位就得到了其对应的物理内存页的首地址。该函数的实现为pmap.h头文件中的page2pa(struct Page *pp)。

Thinking2.6 请阅读 include/queue.h 以及 include/pmap.h, 将Page_list的结构梳理清楚,选择正确的展开结构(请注意指针)。

C

Thinking2.7 在 mmu.h 中定义了 bzero(void *b, size_t) 这样一个函数,请你思考,此处的b指针是一个物理地址, 还是一个虚拟地址呢?

虚拟地址

原因解释:

mm/pmap.c文件中给出的alloc()函数中使用了这一函数,通过阅读alloc()函数,我们能够注意到有这样几行代码:

alloced_mem = freemem; //... if(clear) { bzero((void *)alloced_mem, n); } //...

这里面的指针为alloced_mem,顺藤摸瓜我们能找到其原始值为freemem,分析这个freemem变量,他是一个静态变量,并且通过这段下面这段代码我们注意到,在第一次使用freemem对其进行了初始化,初始化为end的值。

image-20210405170607476

那么继续摸瓜,这个end是在tools/scse0_3.lds链接文件中赋值的,其值为0x80400000,这个值为我们在Lab1中加载内核之后设置的结束地址,是虚拟地址。

Thinking2.8 了解了二级页表页目录自映射的原理之后,我们知道,Win2k内核的虚存管理也是采用了二级页表的形式,其页表所占的 4M 空间对应的虚存起始地址为 0xC0000000,那么,它的页目录的起始地址是多少呢?

0 x c 0000000 + ( 0 x c 0000000 > > 12 ) < < 2 = 0 x c 0300000 0xc0000000 + (0xc0000000 >> 12) >12)


【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3