ucore lab2 操作系统实验

LAB2:

知识准备

(通过操作系统原理教材、gitbook实验指导书、清华大学教学视频以及其他相关的资料进行学习)

1.特权级以及特权级的转换

(在清华大学教学视频中格外详细讲述了此内容,故结合视频内容并查阅相关资料掌握了此内容)

特权级的目的:用于操作系统和CPU提供给不同应用程序隔离空间、不能让用户程序任意访问操作系统空间

X86的特权级共4个,分别为:ring 0内核级,ring 1和2 操作系统的一些服务、ring 3 应用程序级(用户态),不过在Linux以及ucore中只使用了两个级别:ring 0内核级----用于操作系统访问数据、运行以及执行一些特权指令, ring 3 应用程序级(用户态)----访问应用程序层面的数据以及执行用户代码等。由于保护机制的存在,使得如果处于用户态去访问特权指令,会导致中断。

很重要!了解CPL、DPL、RPL以及它们的区别 https://blog.csdn.net/bfboys/article/details/52420211

CPL:当前特权级(Current Privilege Level),保存在 CS段寄存器(选择子)的最低两位, CPL 就是当前活动代码段的特权级,并且它定义了当前所执行程序的特权级别)

DPL:描述符特权(Descriptor Privilege Level),存储在段描述符中的权限位,用于描述对应段所属的特权等级,也就是段本身真正的特权级。

RPL:请求特权级RPL(Request Privilege Level) ,RPL保存在段选择子的最低两位。 每个段选择子有自己的RPLRPL说明的是进程对段访问的请求权限,意思是当前进程想要的请求权限。
ucore lab2 操作系统实验_第1张图片
特权级的检查:
ucore lab2 操作系统实验_第2张图片
下面说明操作系统如何实现特权级的切换(基于lab1中对中断的理解):

(在lab1的challenge中已经实现特权级切换,通过调用软中断i()的方式,在此处进行细节说明):

由lab1可知任务门描述符、中断门描述符、陷阱门描述符的格式:
ucore lab2 操作系统实验_第3张图片
发生中断时操作系统将由用户态跳转至内核态,在内核态的堆栈中保存相关信息。ucore lab2 操作系统实验_第4张图片
(1)实现从特权级0到特权级3的切换(内核态到用户态):

按照我的理解就是通过构造一个能够从ring0返回到ring3的栈来实现,这个中断栈通过内核实现模拟,保存的信息包括EIP、ERROR CODE、CS、EFLAGS。之后通过IRET指令完成数据的更新,完成寄存器的更新,从而实现转换,具体可以用下图表示:ucore lab2 操作系统实验_第5张图片
(2)实现特权级3到特权级0的转变(用户态到内核态)

通常操作系统会采用软中断或者叫做trap的方式完成。实际上,发生中断时已经实现了从用户态切换到内核态,为了实现这种切换,我们需要建立好中断门,中断门中的中断描述符表指出了中断发生后跳转至何处,并且发生中断时我们必须保存SS、ESP等信息。但是,中断会根据保存的这些信息返回到用户态中,为了实现停留在内核态,我们对CS进行修改,将其指向内核态的代码段,其次,我们将CS的CPL设为0,在此处还需要根据要执行的指令修改EIP,这样最后执行IRET指令时,CPU会将堆栈信息取出并返回到EIP以及CS所指内容去执行,从而便实现了从ring3到ring0的转换。
ucore lab2 操作系统实验_第6张图片
为了实现特权级的切换,实际上还需要访问TSS(Task State Segment)任务状态段。简单来说,任务状态段就是内存中的一个数据结构。这个结构中保存着和任务相关的信息。当发生任务切换的时候会把当前任务用到的寄存器内容(CS/ EIP/ DS/SS/EFLAGS…)保存在TSS 中以便任务切换回来时候继续使用。
ucore lab2 操作系统实验_第7张图片
为了访问TSS,还需要访问全局描述符表。全局描述符表(GDT)保存者TSS的地址,TSS最终会被加载进内存中。其中有一个Task Register 的cache缓存,最终通过基址加上偏移来确定Task所在的具体位置。ucore lab2 操作系统实验_第8张图片
通过上述内容,我了解了什么是特权级以及如何实现特权级的检验以及切换。并且对LAB1中的中断进行了回顾与深化,对中断的相关内容有了更为深刻以及细节的认识。基本掌握了特权级的相关知识。

2.物理内存检测:

(参考自本节的附录A、B以及视频教学以及相关资料)

显然,进行物理内存空间分配前,我们必须知道现在物理内存空间的信息,包括物理内存有多大、哪些地址空间可用,哪些地址空间不可用以及它们是否是连续的可用空间等。一般来说,获取内存大小的方法由 BIOS 中断调用和直接探测两种,其中BIOS中断调用方法通常只能在实模式下完成,直接探测的方法必须在保护模式下完成。在本实验中,我们通过e820h中断获取内存信息。因为e820h中断必须在实模式下使用,所以我们在 bootloader 进入保护模式之前调用这个 BIOS 中断,并且把 e820 映射结构保存在物理地址0x8000处。具体实现如下:

首先,需要知道BIOS是通过系统内存映射地址描述符(Address Range Descriptor)格式来表示系统物理内存布局,其具体表示为

Offset  Size    Description
00h    8字节   base address               #系统内存块基地址
08h    8字节   length in bytes            #系统内存大小
10h    4字节   type of address range     #内存类型

之后看一下(Values for System Memory Map address type)

Values for System Memory Map address type:
01h    memory, available to OS
02h    reserved, not available (e.g. system ROM, memory-mapped device)
03h    ACPI Reclaim Memory (usable by OS after reading ACPI tables)
04h    ACPI NVS Memory (OS is required to save this memory between NVS sessions)
other  not defined yet -- treat as Reserved

然后看e820map的定义:

struct e820map 
{
    int nr_map;
    struct {
        uint64_t addr;
        uint64_t size;
        uint32_t type;
    } __attribute__((packed)) map[E820MAX];
};

因此可以通过调用INT 15h BIOS中断,递增di的值(20的倍数),让BIOS帮我们查找出一个一个的内存布局entry,并放入到一个保存地址范围描述符结构的缓冲区中,供后续的ucore进一步进行物理内存管理。

在此处可以查看boot/bootasm.S 中利用汇编元具体实现物理内存检测的过程:

probe_memory:
#对0x8000处的32位单元清零,即给位于0x8000处的struct
#e820map的成员变量nr_map清零
    movl $0, 0x8000
    xorl %ebx, %ebx
#表示设置调用INT 15H BIOS中断后,BIOS返回的映射地址描述符的start address    
    movw $0x8004, %di
start_probe:
    movl $0xE820, %eax
#设置地址范围描述符的大小为20字节,其大小等于struct e820map的成员变量map的大小
    movl $20, %ecx
#设置edx为"SMAP",(这是通常的一个约定)
    movl $SMAP, %edx
#调用ini 0x15中断,要求BIOS返回一个用地址范围描述符表示的内存段信息
    int $0x15
#如果eflags的CF位为0,则表示还有内存段需要探测
    jnc cont
#如果探测有问题,则结束探测    
    movw $12345, 0x8000
    jmp finish_probe
cont:
#设置下一个BIOS返回的映射地址描述符的start address
    addw $20, %di
#递增struct e820map的成员变量nr_map
    incl 0x8000
#如果INT0x15返回的ebx为0,则探测结束,否则继续探测
    cmpl $0, %ebx
    jnz start_probe
finish_probe:

上述代码正常执行完毕后,在0x8000地址处保存了从BIOS中获得的内存分布信息。

从具体代码我们可以看出,要实现物理内存空间的探测基本上是以下三步骤:

  1. 设置一个存放内存映射地址描述符的物理地址(在此为0x8000)
  2. 将e820作为参数传递给INT 15h中断
  3. 通过检测eflags的CF位来判断探测是否结束。如果CF位为0,则表示探测没有结束,那么就需要设置存放下一个内存映射地址描述符的物理地址,返回步骤2继续进行;否则物理内存检测就此结束。

通过代码我们也能知道实现物理内存检测后在0x8000地址处保存了从BIOS中获得的内存分布信息,此信息按照struct e820map的设置来进行填充,之后便开始执行进入保护模式的过程。

3.连续物理内存分配

连续分配方式,是指为用户程序分配一个不小于指定大小的连续的内存空间。连续物理内存分配会产生碎片,包括内部碎片(已经被分配出去的的内存空间大于请求所需的内存空间)和外部碎片(还没有分配出去,但是由于太小而无法分配给申请空间的新进程的内存空间空闲块),实际上可以通过紧凑的方式(操作系统不时地对进程进行移动和整理)或者分区交换的方式(通过抢占并回收处于等待状态进程的分区以增大可用内存空间)解决外部碎片。

动态分区分配:当程序被加载执行时,分配一个进程指定大小可变的分区(块、内存块),分区的地址是连续的。操作系统需要维护的数据结构包括所有进程的已分配分区以及空闲分区。

常见的动态分区的分配策略如下:

首次适应(First Fit)算法:空闲分区以地址递增的次序链接。分配内存时顺序查找,找到大小能满足要求的第一个空闲分区。该算法的缺点在于:低地址部分由于不断被划分,会留下许多难以利用的小空闲分区,并且,每次都从低地址开始检索,将增大可用空闲区间查找的开销。

最佳适应(Best Fit)算法:空闲分区按容量递增形成分区链,找到第一个能满足要求的空闲分区,即找到一个满足要求且最小的空闲分区分配给作业。实际上从宏观上看,存储器将留下许多难以利用的小空闲区。

最坏适应(Worst Fit)算法:空闲分区以容量递减的次序链接。找到第一个能满足要求的空闲分区,即挑选出最大的分区。该算法优点是使剩下的空闲区不至于太小,故产生碎片的几率减小。因此该算法对中小作业有利,但对大作业不利。

4.段页式管理内存

1.以页为单位管理物理内存

在获得可用物理内存范围后,系统需要建立相应的数据结构来管理以物理页(按4KB对齐,且大小为4KB的物理内存单元)为最小单位的整个物理内存,以配合后续涉及的分页管理机制。

我们通过查阅代码可以了解:在kern/mm/memlayout.h中,给出了page的定义:

//用来描述页
//描述物理空间的页
struct Page {
    int ref;                        // page frame's reference counter
  //ref表示这个页被页表的引用量
 //如果这个页被页表引用了,即在某页表中有一个页表项设置了一个虚拟页到这个Page管理的物理页的映射关系
 //,就会把Page的ref加一;反之,若页表项取消,即映射关系解除,就会把Page的ref减一。
    uint32_t flags;         // array of flags that describe the status of the page frame
    //flags标记是否可以被分配
    unsigned int property;    // the num of free block, used in first fit pm manager
    //property用来记录连续空闲页的数量,
    list_entry_t page_link;         // free list link
    //双向链表,空闲页构成链表
};

由于所有的连续内存空闲块可用一个双向链表管理起来,便于分配和释放,为此定义了一个free_area_t数据结构,包含了一个list_entry结构的双向链表指针和记录当前空闲页的个数的无符号整型变量nr_free。

/* free_area_t - maintains a doubly linked list to record free (unused) pages */
typedef struct {
    list_entry_t free_list;         // the list header
    //列表的头
    unsigned int nr_free;           // # of free pages in this free list
    //空闲页的数量
} free_area_t;

在pmm.c中,可以通过下面代码段,更好地理解“管理页级物理内存空间所需的Page结构的内存空间从哪里开始,占多大空间”以及“空闲内存空间的起始地址在哪里“这两个问题。

//end指向bootloader加载ucore的结束地址
    extern char end[];
   //需要管理的物理页个数为
    npage = maxpa / PGSIZE;
   //由于bootloader加载ucore的结束地址以上的空间没有被使用,
   //所以我们可以把end按页大小为边界去整后,作为管理页级物理内存空间所需的Page结构的内存空间
    pages = (struct Page *)ROUNDUP((void *)end, PGSIZE);
   //对地址空间进行标记
    for (i = 0; i < npage; i ++) {
        SetPageReserved(pages + i);
    }
//从地址0到地址pages+ sizeof(struct Page) * npage)结束的物理内存空间设定为已占用物理内存空间
//(起始0~640KB的空间是空闲的),地址pages+ sizeof(struct Page) * npage)以上的空间为空闲物理内存空间,这时的空闲空间起始地址为
    uintptr_t freemem = PADDR((uintptr_t)pages + sizeof(struct Page) * npage);

//根据begin以及end,实现空闲物理内存空间的标记
    for (i = 0; i < memmap->nr_map; i ++) {
        uint64_t begin = memmap->map[i].addr, end = begin + memmap->map[i].size;
        if (memmap->map[i].type == E820_ARM) {
            if (begin < freemem) {
                begin = freemem;
            }
            if (end > KMEMSIZE) {
                end = KMEMSIZE;
            }
            if (begin < end) {
                begin = ROUNDUP(begin, PGSIZE);
                end = ROUNDDOWN(end, PGSIZE);
                if (begin < end) {
                    init_memmap(pa2page(begin), (end - begin) / PGSIZE);
                }
            }
        }
    }
}

当完成物理内存页管理初始化工作后,计算机系统的内存布局如下图所示(引用自gitbook):ucore lab2 操作系统实验_第9张图片

2.分段机制

分段机制就是把虚拟地址空间中的虚拟内存组织成一些长度可变的称为段的内存块单元。 每个段由三个参数定义:段基地址、段限长和段属性。基本思路如图(图片引用自https://www.cnblogs.com/zjzsky/p/3528526.html)
ucore lab2 操作系统实验_第10张图片
如果没有开启分页,那么处理器直接把线性地址映射到物理地址,即线性地址被送到处理器地址总线上;如果对线性地址空间进行了分页处理,那么就会使用二级地址转换把线性地址转换成物理地址。

3.分页机制

分页机制是 80x86 内存管理机制的第二部分。它在分段机制的基础上完成虚拟地址到物理地址的转换过程。分段机制把逻辑地址转换成线性地址,而分页机制则把线性地址转换成物理地址。

分页机制可以用下图说明:(图片引用自https://www.cnblogs.com/ay-a/p/8387809.html)
ucore lab2 操作系统实验_第11张图片
总的来说,地址的处理过程如下所示:
ucore lab2 操作系统实验_第12张图片
对段页式管理有了基本认识后,便可以针对具体问题去具体分析解决。

5.伙伴系统 buddy system

(通过清华大学教学视频以及查看相关csdn学习)

伙伴分配的实质就是一种特殊的**“分离适配”**,整个内存的大小是2的n次方,并将内存按2的幂进行划分。

需要分配内存的时:核心就是分配出不小于所需的最小2次幂大小的内存(如果需要25,就分配32;如果需要63,就分配64),具体分配时,如果有符合的内存块,直接分配即可;如果当前的空闲块没有满足要求的,就将大块进行二等分,继续重复分配过程。

需要释放内存时:首先将该内存块释放,然后看其相邻的块(可以进行合并的相邻块,即在分配时由一个内存块分成的两个等大内存块)是否释放,如果相邻块没有释放,则结束即可;如果相邻块释放,则将两个块合并,重复释放过程,对合并后的块进行释放。对相邻块做一个更容易实现的解释:相邻块不仅地址相邻,且二者中的低地址块的地址必须为2的整数幂。

对其原理进行一个图解(仔细观察这个图便可以了解其工作机制):
ucore lab2 操作系统实验_第13张图片
通常,我们利用满二叉树的数据结构实现buddy system。分配器的整体思想是,通过一个数组形式的完全二叉树来监控管理内存,二叉树的节点用于标记相应内存块的使用状态,高层节点对应大的块,低层节点对应小的块,在分配和释放中我们就通过这些节点的标记属性来进行块的分离合并。
ucore lab2 操作系统实验_第14张图片
讨论一下该算法的优缺点:其优点是快速搜索合并(O(logN)时间复杂度)以及低外部碎片(最佳适配best-fit);其缺点是内部碎片,因为按2的幂划分块,如果碰上65单位大小,那么必须划分128单位大小的块。

实验过程

练习0:填写已有实验

本实验依赖实验1。请把你做的实验1的代码填入本实验中代码中有“LAB1”的注释相应部分。提示:可采用diff和patch工具进行半自动的合并(merge),也可用一些图形化的比较/merge工具来手动合并,比如meld,eclipse中的diff/merge工具,understand中的diff/merge工具等。

问题解答:

实际上仅需要把LAB1的自己修改的三个.c文件copy到LAB2即可,不做赘述。

练习1:实现 first-fit 连续物理内存分配算法(需要编程)

在实现first fit 内存分配算法的回收函数时,要考虑地址连续的空闲块之间的合并操作。提示:在建立空闲页块链表时,需要按照空闲页块起始地址来排序,形成一个有序的链表。可能会修改default_pmm.c中的default_init,default_init_memmap,default_alloc_pages, default_free_pages等相关函数。请仔细查看和理解default_pmm.c中的注释。

请在实验报告中简要说明你的设计实现过程。请回答如下问题:

*你的first fit算法是否有进一步的改进空间

问题解答:

实际上LAB2已经给出了很多重要的数据结构的定义以及相关操作的定义。理解上述内容后,就基本可以着手于解决First-Fit算法。

在pmm.h中已经给出连续物理内存分配的基本框架:

struct pmm_manager {
    const char *name;  //初始化内部描述和管理数据结构                                   
    void (*init)(void);   //根据初始空闲物理内存空间设置描述和管理数据结构                                                                 
    void (*init_memmap)(struct Page *base, size_t n); 
    struct Page *(*alloc_pages)(size_t n);   //分配内存,分配>=n的页 
    //释放>=n个页
    void (*free_pages)(struct Page *base, size_t n);  //释放>=n个页
    size_t (*nr_free_pages)(void);   //返回空闲内存的数量 
   
    void (*check)(void);     //检查练习1是否正确                         
};

为了更好地辅助完成该算法的实现,我仔细阅读了list.h中关于双向链表的定义以及相关函数,了解了部分函数作用以及用法:

//双向链表的定义
struct list_entry {
    struct list_entry *prev, *next;
};
typedef struct list_entry list_entry_t;

//初始化elm
static inline void list_init(list_entry_t *elm)
//将elm插入到listelm之后,list_add等价于list_add_after
static inline void list_add_after(list_entry_t *listelm, list_entry_t *elm)
static inline void list_add(list_entry_t *listelm, list_entry_t *elm)
//将elm插入到listelm之前
static inline void list_add_before(list_entry_t *listelm, list_entry_t *elm)
//删除链表项listelm
static inline void list_del(list_entry_t *listelm) 
//把listelm从链表中删除并将其初始化
static inline void list_del_init(list_entry_t *listelm) 
//判断list是否为空,返回为bool值
static inline bool list_empty(list_entry_t *list)
//list_next返回后面的链表项
static inline list_entry_t *list_next(list_entry_t *listelm)
//list_prevf 返回它前面的链表项
static inline list_entry_t *list_prev(list_entry_t *listelm)
//_list_add主要是把elm插入到prev和next之间
static inline void __list_add(list_entry_t *elm, list_entry_t *prev, list_entry_t *next)
//将prev和next之间的链表项去掉
static inline void __list_del(list_entry_t *prev, list_entry_t *next) 
first-fit实现:

下面是我对First-Fit算法的理解以及实现思路 (实现很大一部分是直接在原文件上进行修改即可):

首先是初始化:

free_area_t free_area;

#define free_list (free_area.free_list) //列表的头
#define nr_free (free_area.nr_free)   //空闲页的数目

//初始化free_area
static void
default_init(void) {
    list_init(&free_list);  //列表只有一个头指针
    nr_free = 0;   //空闲页数设为0
}
//初始化n个空闲页链表
static void
default_init_memmap(struct Page *base, size_t n) {
    assert(n > 0);
    struct Page *p = base;  //让p为最开始的空闲页表
    for (; p != base + n; p ++)   
    {
        assert(PageReserved(p));//检查是否为保留页
        p->flags = p->property = 0;//设置标记,flag为0表示可以分配
                                   //property为0表示不是base
        set_page_ref(p, 0); //将p的ref设置为0(调用已经写过的函数)
    }
    base->property = n;  //第一个页表也就是base的property设置为n,因为有n个空闲块
    
    SetPageProperty(base);  
    nr_free += n;   //将空闲页数目设置为n
  // list_add(&free_list, &(base->page_link));
  //之前是list_add,等价于list_add_after,这样的话就把第一个块浪费了,所以有小问题,虽然检测不会出问题
   //改为list_add_before()就更合适了
    list_add_before(&free_list, &(base->page_link));
}

分配内存的思路:(伪流程图的方式)
ucore lab2 操作系统实验_第15张图片
分配内存的代码实现:

//由于是first-fit函数,故把遇到的第一个可以用于分配的连续内存进行分配即可
static struct Page * default_alloc_pages(size_t n) {
    assert(n > 0);  //n的值应该大于0
    //如果n>nf_free,表示无法分配这么大内存,返回NULL即可
    if (n > nr_free)
     {
        return NULL;
    }
    //说明够分配,因此找到即可
    struct Page *page = NULL;
    list_entry_t *le = &free_list;
    // 查找n个或以上空闲页块,若找到,则判断是否大过n,大于n的话则将其拆分 
    //  并将拆分后的剩下的空闲页块加回到链表中
    while ((le = list_next(le)) != &free_list) 
    // 如果list_next(le)) == &free_list说明已经遍历完了整个双向链表
    {
        // 此处le2page就是将le的地址-page_link 在Page的偏移,从而找到 Page 的地址
        struct Page *p = le2page(le, page_link);
        //说明找到可以满足的连续空闲内存了,让page等于p即可,退出循环
        if (p->property >= n) 
        {
            page = p;
            break;
        }
    }
    if (page != NULL) 
    {
        //如果property>n的话,我们需要把多出的内存加到链表里
        if (page->property > n) 
        {
            //创建一个新的Page,起始地址为page+n
            struct Page *p = page + n; 
            p->property = page->property - n;  
            //将其property设置为page->property-n
            SetPageProperty(p);   
            // 将多出来的插入到被分配掉的页块后面
            list_add(&(page->page_link), &(p->page_link));
        }
        // 在空闲页链表中删去刚才分配的空闲页
        list_del(&(page->page_link));
        //因为分配了n个内存,故nr_free-n即可
        nr_free -= n;
        ClearPageProperty(page);
    }
    //返回分配的内存
    return page;
}

释放内存的思路:(伪流程图的方式)
ucore lab2 操作系统实验_第16张图片
具体的代码实现如下:

//释放掉n个页块,释放后也要考虑释放的块是否和已有的空闲块是紧挨着的,也就是可以合并的
//如果可以合并,则合并,否则直接加入双向链表

static void default_free_pages(struct Page *base, size_t n) 
{
    assert(n > 0);  //n必须大于0
    struct Page *p = base;  
    //首先将base-->base+n之间的内存的标记以及ref初始化
    for (; p != base + n; p ++)
     {
        assert(!PageReserved(p) && !PageProperty(p));
        //将flags和ref设为0
        p->flags = 0; 
        set_page_ref(p, 0);
    }
    //释放完毕后先将这一块的property改为n
    base->property = n;
    SetPageProperty(base);
    list_entry_t *le = list_next(&free_list);

    // 检查能否将其合并到合适的页块中
    while (le != &free_list)
    {
        p = le2page(le, page_link);
        le = list_next(le);
        //如果这个块在下一个空闲块前面,二者可以合并
        if (base + base->property == p) 
        {
            //让base的property等于两个块的大小之和
            base->property += p->property;
            //将另一个空闲块删除即可
            ClearPageProperty(p);
            list_del(&(p->page_link));
        }
        //如果这个块在上一个空闲块的后面,二者可以合并
        else if (p + p->property == base) 
        {
            //将p的property设置为二者之和
            p->property += base->property;
            //将后面的空闲块删除即可
            ClearPageProperty(base);
            base = p;
            //注意这里需要把p删除,因为之后再次去确认插入的位置
            list_del(&(p->page_link));
        }
    }
    //整体上空闲空间增大了n
    nr_free += n;
    le = list_next(&free_list);
    // 将合并好的合适的页块添加回空闲页块链表
    //因为需要按照内存从小到大的顺序排列列表,故需要找到应该插入的位置
    while (le != &free_list) 
    {
        p = le2page(le, page_link);
        //找到正确的位置:
        if (base + base->property <= p)
        {
            break;
        }
        //否则链表项向后,继续查找
        le = list_next(le);
    }
    //将base插入到刚才找到的正确位置即可
    list_add_before(le, &(base->page_link));
}

完成相关函数后,进行make qemu指令,可以看到:
ucore lab2 操作系统实验_第17张图片

First-Fit的改进

在此对实现的First-Fit进行分析,是否可以改进?

首先我们知道该算法在分配以及释放内存时复杂度均为O(n),因为需要访问链表,故复杂度不可避免地为O(n);

我们可以用二叉搜索树来对内存进行管理。用二叉搜索树主要是通过对地址排序,使得在使用free时候可以在O(logn)时间内完成链表项位置的查找,从而实现时间上的优化。具体如下:
ucore lab2 操作系统实验_第18张图片
按照地址排序,也就是保证二叉树的任意节点的左节点的地址值小于自身地址值,右节点的地址值大于自身地址值,通过此方法优化,我们可以实现O(n)的复杂度进行内存的分配,但是可以O(logn)的复杂度进行内存的释放,因为判断合并的过程得到了优化。在这里,我们对LEN无要求,但必须保证X0

另外,还有一些优化连续内存处理的思路,不过需要改变First-Fit算法。如果访问的内存较多且尺寸比较小时,则可以将First-Fit改为Best-Fit会更合适。如果访问的内存较多且尺寸均为中小型尺寸时,采用Worst-Fit将更合适。

同时,采取紧缩的办法将较小的内存块合并到其他内存块,也可以使该算法得到优化,通过紧凑可以很大程度上消除外部碎片,提高空间的利用率,但实现紧凑较为复杂,在此不做赘述。

练习2:实现寻找虚拟地址对应的页表项(需要编程)

通过设置页表和对应的页表项,可建立虚拟内存地址和物理内存地址的对应关系。其中的get_pte函数是设置页表项环节中的一个重要步骤。此函数找到一个虚地址对应的二级页表项的内核虚地址,如果此二级页表项不存在,则分配一个包含此项的二级页表。本练习需要补全get_pte函数 in kern/mm/pmm.c,实现其功能。请仔细查看和理解get_pte函数中的注释。get_pte函数的调用关系图如下所示:
ucore lab2 操作系统实验_第19张图片
请在实验报告中简要说明你的设计实现过程。请回答如下问题:

  • 请描述页目录项(Pag Director Entry)和页表(Page Table Entry)中每个组成部分的含义和以及对ucore而言的潜在用处。
  • 如果ucore执行过程中访问内存,出现了页访问异常,请问硬件要做哪些事情?

问题解答:

虚拟地址、线性地址、物理地址的关系:

在lab2中,为了建立正确的地址映射关系,ld在链接阶段生成了ucore OS执行代码的虚拟地址,而bootloader与ucore OS协同工作,通过在运行时对地址映射的一系列“腾挪转移”,从计算机加电,启动段式管理机制,启动段页式管理机制,在段页式管理机制下运行这整个过程中,虚地址到物理地址的映射产生了多次变化,实现了最终的段页式映射关系:

 virt addr = linear addr = phy addr + 0xC0000000

把0~KERNSIZE的物理地址一一映射到页目录项和页表项的内容,其大致流程如下:

  1. 先通过alloc_page获得一个空闲物理页,用于页目录表;

  2. 调用boot_map_segment函数建立一一映射关系,具体处理过程以页为单位进行设置,即

    virt addr = phy addr + 0xC0000000
    

    设一个32bit线性地址la有一个对应的32bit物理地址pa,如果在以la的高10位为索引值的页目录项中的存在位(PTE_P)为0,表示缺少对应的页表空间,则可通过alloc_page获得一个空闲物理页给页表,页表起始物理地址是按4096字节对齐的,这样填写页目录项的内容为

页目录项内容 = (页表起始物理地址 &0x0FFF) | PTE_U | PTE_W | PTE_P

进一步对于页表中以线性地址la的中10位为索引值对应页表项的内容为

页表项内容 = (pa & ~0x0FFF) | PTE_P | PTE_W

PTE_U:位3,表示用户态的软件可以读取对应地址的物理内存页内容

PTE_W:位2,表示物理内存页内容可写

PTE_P:位1,表示物理内存页存在

ucore的内存是二级页表机制,地址的转换如图示(引自清华大学操作系统教学视频):
ucore lab2 操作系统实验_第20张图片
由此,便可以开始get_pte的填写,主要功能给定一个虚拟地址,找出这个虚拟地址在二级页表中对应的项。通过更改此项的值可以方便地将虚拟地址映射到另外的页上。

这个实验原理差不多就是给一个虚拟地址 然后根据这个虚拟地址的高10位找到页目录表中的PDE项,然后判断一下PDE是否存在,如果不存在则获取一个物理页,然后将这个物理页的线性地址写入到PDE中最后返回PTE项。具体代码:(根据注释很容易写出代码,调用的函数较多,需要之前将注释提示的函数仔细看一遍)

//此函数找到一个虚地址对应的二级页表项的内核虚地址,如果此二级页表项不存在,则分配一个包含此项的二级页表
// pgdir:PDT的内核虚拟地址  la:需要映射的线性地址  creat:决定是否为PT分配页面的逻辑值
//return vaule:这个pte的内核虚拟地址
pte_t *get_pte(pde_t *pgdir, uintptr_t la, bool create) 
{ 
    //找PDE
    pde_t *pdep = pgdir + PDX(la);
    //如果存在的话,返回对应的PTE即可
    if (*pdep & PTE_P) {
        pte_t *ptep = (pte_t *)KADDR(*pdep & ~0x0fff) + PTX(la);
        return ptep;
    }
    //如果不存在的话分配给它page
    struct Page *page;
    if (!create || ((page = alloc_page()) == NULL))
     {
        return NULL;
    }
    //设置page reference
    set_page_ref(page, 1);
    //得到page的线性地址
    uintptr_t pa = page2pa(page) & ~0x0fff;
    //用memset清除page
    memset((void *)KADDR(pa), 0, PGSIZE);
    //设置PDE权限
    *pdep = pa | PTE_P | PTE_W | PTE_U;
    //返回PTE
    pte_t *ptep = (pte_t *)KADDR(pa) + PTX(la);

    return ptep;
}

需要注意的地方:

通过 default_alloc_pages() 分配的页的地址并不是真正的页分配的地址,实际上只是 Page 这个结构体所在的地址,故而需要通过使用 page2pa() 将 Page 这个结构体的地址转换成真正的物理页地址的线性地址,需要注意的是:无论是 * 或是 memset 都是对虚拟地址进行操作的所以需要将真正的物理页地址再转换成内核虚拟地址。

还有一点要注意,check_pgdir()函数中进行检查时调用了page_remove()函数,page_remove函数调用了 page_remove_pte(pgdir, la, ptep),而这个函数是在练习3才进行函数的编程的,故写完练习2进行make qemu会产生assert,因此在写完练习3后才能进行检查。

回答问题
  • 请描述页目录项(Page Director Entry)和页表(Page Table Entry)中每个组成部分的含义和以及对ucore而言的潜在用处。

    页目录项组成以及对ucore而言的潜在作用:

    地址 名称 ucore中的对应以及对ucore而言的潜在用处
    31:12 Page Table 4KB Aligned Address 页表的起始物理地址,用于定位页表位置
    11:9 Avail PTE_AVAIL,保存给OS使用
    8 Ignored
    7 Page Size PTE_PS,用于确认页的大小
    6 PTE_MBZ
    5 Accessed PTE_A,用于确认对应页表是否被使用
    4 Cache Disabled PTE_PCD 用于cache
    3 Write Through PTE_PWT 用于cache
    2 User/Supervisor PTE_U,用于确认用户态下是否可以访问
    1 Read/Write PTE_W,用于确认页表是否可写,内存分配和释放时需要置位
    0 Present PTE_P,用于确定对应的页表是否存在,如果为1表示存在,如果为0表示不存在,需要再分配一个物理页给页表

    页表项的组成以及对ucore而言的潜在作用:

    地址 名称 在ucore中的对应以及潜在作用
    31:12 Physical Page Address 对应物理页的起始物理地址,用于定位物理也的位置
    11:9 Avail PTE_AVAIL,保留给OS使用
    8 Global
    7 0 PTE_MBZ
    6 Dirty PTE_D,脏位,用于确认数据是否有效
    5 Accessed PTE_A,用于确认对应页表是否被使用
    4 Cache Disabled PTE_PCD, 用于cache
    3 Write Through PTE_PWT, 用于cache
    2 User/Supervisor PTE_U,用于确认用户态下是否可以访问对应的物理页
    1 Read/Write PTE_W,用于确认对应的物理页是否可写
    0 Present PTE_P,用于确认页表项所对应的物理页是否存在
  • 如果ucore执行过程中访问内存,出现了页访问异常,请问硬件要做哪些事情?

    • 将引发页访问异常的地址将被保存在cr2寄存器中
    • 设置错误代码
    • 引发Page Fault,将外存的数据换到内存中
    • 进行上下文切换,退出中断,返回到中断前的状态

练习3:释放某虚地址所在的页并取消对应二级页表项的映射(需要编程)

当释放一个包含某虚地址的物理内存页时,需要让对应此物理内存页的管理数据结构Page做相关的清除处理,使得此物理内存页成为空闲;另外还需把表示虚地址与物理地址对应关系的二级页表项清除。请仔细查看和理解page_remove_pte函数中的注释。为此,需要补全在 kern/mm/pmm.c中的page_remove_pte函数。page_remove_pte函数的调用关系图如下所示:page_remove_pte函数的调用关系图
请在实验报告中简要说明你的设计实现过程。请回答如下问题:

  • 数据结构Page的全局变量(其实是一个数组)的每一项与页表中的页目录项和页表项有无对应关系?如果有,其对应关系是啥?
  • 如果希望虚拟地址与物理地址相等,则需要如何修改lab2,完成此事? 鼓励通过编程来具体完成这个问题

问题解答:

思路:检查页表项是否存在,如果存在,将其对应的映射关系取消,并将PTE清除,之后刷新tlb即可。

代码如下(按照注释提示很容易完成):

static inline void
page_remove_pte(pde_t *pgdir, uintptr_t la, pte_t *ptep) 
{
    //检查page directory是否存在
    //练习二我们已经学到 PTE_P用于表示page dir是否存在
    if (*ptep & PTE_P) 
    {
        struct Page *page = pte2page(*ptep);
    //  (page_ref_dec(page)将page的ref减1,并返回减1之后的ref  
        if (page_ref_dec(page) == 0) //如果ref为0,则free即可
        {
            free_page(page);
        }
        //清除第二个页表 PTE
        *ptep = 0; 
        //刷新 tlb  
        tlb_invalidate(pgdir, la);
    }
}

完成代码书写后,可以执行make qemu,检测练习二和练习三的代码是否正确,运行结果如下:
ucore lab2 操作系统实验_第21张图片
说明练习2和练习3的代码均正确。

回答问题:
  • 数据结构Page的全局变量(其实是一个数组)的每一项与页表中的页目录项和页表项有无对应关系?如果有,其对应关系是啥?

    当页目录项或页表项有效时,二者之间有对应关系。练习2的问题其实已经说明了一些情况,实际上,pages每一项记录一个物理页的信息,而每个页目录项记录一个页表的信息,每个页表项则记录一个物理页的信息。可以说,页目录项保存的物理页面地址(即某个页表)以及页表项保存的物理页面地址都对应于Page数组中的某一页。

  • 如果希望虚拟地址与物理地址相等,则需要如何修改lab2,完成此事? 鼓励通过编程来具体完成这个问题

    我们知道lab1中虚拟地址和物理地址便是相等的,而lab2我们通过多个步骤建立了虚拟地址到物理地址的映 射,故如果取消该映射即可完成目标:(根据"系统执行中地址映射的四个阶段"内容进行反向完成)

    首先将链接脚本改为 0x100000,只需要将tools/kernel.ld中的代码进行很小的修改即可:

    ENTRY(kern_init)
    
    SECTIONS {
                /* Load the kernel at this address: "." means the current address */
                . = 0x100000;         //修改这里为0x1000000即可
    
                .text : {
                           *(.text .stub .text.* .gnu.linkonce.t.*)
                }
    

    并且将偏移量改为0:

    //在memlayout.h中将KERNBASE 0Xc0000000改为 0x0即可
    #define KERNBASE   0xC0000000         //将“0xC0000000”改为“0x00000000”
    //修改虚拟地址基址 减去一个 0xC0000000 就等于物理地址
    

需要注意的是,需要把开启页表关闭,否则会报错,因为页表开启时认为偏移量不为0,故会报错。

但实际上,此时虚拟地址已经等于物理地址,任务完成。

扩展练习Challenge:buddy system(伙伴系统)分配算法(需要编程)**

Buddy System算法把系统中的可用存储空间划分为存储块(Block)来进行管理, 每个存储块的大小必须是2的n次幂(Pow(2, n)), 即1, 2, 4, 8, 16, 32, 64, 128…

  • 参考伙伴分配器的一个极简实现, 在ucore中实现buddy system分配算法,要求有比较充分的测试用例说明实现的正确性,需要有设计文档。
  • 问题解答:

具体实现过程较为复杂,主要思路是按照完全二叉树的模型进行物理内存分配,在这里为了较为容易地遍历物理内存,仍然使用了练习1使用的链表结构,但对具体算法思想无影响。同时,在此处考虑了给定物理内存后如何将节点信息与具体用于分配的内存进行协调,提高了内存的利用率。由于实际物理内存通常不是2的倍数,故在此处我使用可用于分配的内存(虚拟的内存)可能会大于具体物理内存,但在存储节点信息时将多出的部分进行了标记,也就是将虚拟的内存对应的实际不存在的物理内存部分标为已用即可。对内存的布局管理如下图:ucore lab2 操作系统实验_第22张图片
之后分配和释放通过完全二叉树的从上到下和从下到上进行遍历与刷新即可。alloc过程为自上而下进行判定,找到最适合用于分配地空间,并对相关的结点信息进行刷新。free过程需要判断用递归判断是否两个节点可以合并,如果可以则合并后继续判断,并对相关结点信息进行刷新。
ucore lab2 操作系统实验_第23张图片
challenge代码过长在此不详述,如需代码私聊CSDN。

如果给你带来了帮助,可点击关注,博主将继续努力推出好文。

你可能感兴趣的:(操作系统实验,UCORE,LAB2,ucore,lab系列,操作系统实验ucore)