ufat2

  • Post author:
  • Post category:其他




Lab2 物理内存管理



实验目的

  • 理解基于段页式内存地址的转换机制
  • 理解页表的建立和使用方法
  • 理解物理内存的管理方法



实验内容

uCore Lab 2 – 物理内存管理

  1. 编译运行 uCore Lab 2 的工程代码;
  2. 完成 uCore Lab 2 练习 1-3 的编程作业;
  3. 思考如何实现 uCore Lab 2 扩展练习 1-2。



编译运行 uCore Lab 2 的工程代码

在文件夹下终端输入

make

,即可完成编译。

在这里插入图片描述

在这里插入图片描述

就编译成功了。



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

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

注释中Prepare部分,需要我学会用

list_init, list_add(list_add_after), list_add_before, list_del, list_next, list_prev

操作。

参看

list.h


结构体为

struct list_entry {
    struct list_entry *prev, *next;
};

一个指向前驱和一个指向后继的指针

分别阅读函数,功能大致如下

list_init初始化一个新的list_entry 
list_add,list_add_after和list_add_after都是添加一个新的条目 
list_del是 删除一个条目
list_del_init 从表中删除一个条目并重定义它 
list_empty 判断列表是否为空 
list_next 获取列表的前一项
list_prev 获取列表的后一项

阅读下面注释,在memlayout.h中找到Page的定义

struct Page {
    int ref;                        // page frame's reference counter
    uint32_t flags;                 // array of flags that describe the status of the page frame
    unsigned int property;          // the num of free block, used in first fit pm manager
    list_entry_t page_link;         // free list link
};

再根据github上实验的说明

ref:表示这页被页表的引用记数,如果这个页被页表引用了,即在某页表中有一个页表项设置了一个虚拟页到这个Page管理的物理页的映射关系,就会把Page的ref加一;反之,若页表项取消,即映射关系解除,就会把Page的ref减一。

/* Flags describing the status of a page frame */
#define PG_reserved 0  // the page descriptor is reserved for kernel or unusable
#define PG_property 1  // the member 'property' is valid

这表示flags目前用到了两个bit表示页目前具有的两种属性,bit 0表示此页是否被保留(reserved),如果是被保留的页,则bit 0会设置为1,且不能放到空闲页链表中,即这样的页不是空闲页,不能动态分配与释放。比如目前内核代码占用的空间就属于这样“被保留”的页。在本实验中,bit 1表示此页是否是free的,如果设置为1,表示这页是free的,可以被分配;如果设置为0,表示这页已经被分配出去了,不能被再二次分配。另外,本实验这里取的名字PG_property比较不直观,主要是我们可以设计不同的页分配算法(best fit, buddy system等),那么这个PG_property就有不同的含义了。

property用来记录某连续内存空闲块的大小。

成员变量page_link是便于把多个连续内存空闲块链接在一起的双向链表指针

据注释,有种“tricky method”可以强制转换列表为页指针,即

#define le2page(le, member)                 
    to_struct((le), struct Page, member)


First-Fit(首次适应算法)

是顺序查找到一个满足要求的就分配。如果空间明显大于要求,通常会切割,并把剩下的放回去。会有外部碎片



default_init

#define nr_free (free_area.nr_free)

static void
default_init(void) {
    list_init(&free_list);
    nr_free = 0;
}
typedef struct {
    list_entry_t free_list;         
    unsigned int nr_free; 
} free_area_t;

nr_free表示空闲页的数量,这个函数应该不用修改。



default_init_memmap

根据注释调用路径

kern_init

–>

pmm_init

–>

page_init

–>

init_memmap

–> *

pmm_manager

–>

init_memmap

.


set_page_ref(page,1) : means the page be referenced by one time


set_page_ref可以清除引用此页的虚拟页个数

可以重写成

static void
default_init_memmap(struct Page *base, size_t n) {
    assert(n > 0);
    struct Page *p = base;
    for (; p != base + n; p ++) {
        assert(PageReserved(p));
        p->flags = p->property = 0;
        set_page_ref(p, 0);
    }
    base->property = n;
    SetPageProperty(base);
    nr_free += n;
    list_add_before(&free_list, &(base->page_link));
}

函数先声明一个base的Page,再生成base为起始地址的n个页

如果有保留页,就终止程序

对每页都设置标志为Property为1

被引用次数ref为0

接好双向指针

退出循环后,使空闲页总数加n

base的连续空闲页数为n



default_alloc_pages()

根据注释,我要从free链表指针开始往后找,找到第一个property >=n列表地址,重写安排剩余块的大小,并返回分配出去的块的地址。

根据前面的代码,可知

le2page

宏可以由链表元素获得对应的Page指针

注释讲得非常详细,一步步写即可,找不到就返回NULL

改写后的函数

static struct Page *
default_alloc_pages(size_t n) {
    assert(n > 0);
    if (n > nr_free) {
        return NULL;
    }
    struct Page *page = NULL;
    list_entry_t *le = &free_list;
    while ((le = list_next(le)) != &free_list) {
        struct Page *p = le2page(le, page_link);
        if (p->property >= n) {
            page = p;
            break;
        }
    }
    if (page != NULL) {
        if (page->property > n) {
            struct Page *p = page + n;
            p->property = page->property - n;
            SetPageProperty(p);
            list_add_after(&(page->page_link), &(p->page_link));
        }
        list_del(&(page->page_link));
        nr_free -= n;
        ClearPageProperty(page);
    }
    return page;
}

顺着指针向后找,找到第一个后续有联系n个空闲页的位置。然后枚举这n个空闲页,设置它们不空闲了,并且后续没有连续页了。并且可以这对于这n块的链表删去。

若这个链表其实有超过n页,则把剩下的加回去。返回分配的n个页的起始地址p。若没有,最后返回NULL。



default_free_pages

根据注释,重新链接这些页到自由页链表,也许可以合并小的链表项并成大的

修改后如下

static void
default_free_pages(struct Page *base, size_t n) {
    assert(n > 0);
    assert(PageReserved(base));

    list_entry_t *le = &free_list;
    struct Page * p;
    while((le=list_next(le)) != &free_list) {
      p = le2page(le, page_link);
      if(p>base){
        break;
      }
    }
    for(p=base;p<base+n;p++){
      list_add_before(le, &(p->page_link));
    }
    base->flags = 0;
    set_page_ref(base, 0);
    ClearPageProperty(base);
    SetPageProperty(base);
    base->property = n;
    
    p = le2page(le,page_link) ;
    if( base+n == p ){
      base->property += p->property;
      p->property = 0;
    }
    le = list_prev(&(base->page_link));
    p = le2page(le, page_link);
    if(le!=&free_list && p==base-1){
      while(le!=&free_list){
        if(p->property){
          p->property += base->property;
          base->property = 0;
          break;
        }
        le = list_prev(le);
        p = le2page(le,page_link);
      }
    }

    nr_free += n;
    return ;
}

首先遍历链表,找到这些页应该插入的位置

然后把n个页依次插入,插入时设置下flags为0,被引用次数ref为0

base的property可设为n,标志这里开始有n页连续可用

然后尝试合并base与后续的一块

然后设le为该页的前一页,将le向free链表头枚举,尝试向前合并,即将自己的property转移到前一页上,相当于resize了整个链表。

最后空闲页数加n。



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

在分配与释放内存时,在双向链表上操作复杂度为O(n),可以用线段树等树形结构维护,在分配过程为一次二分查找,但仍需分配处理分配的N个页,复杂度仍是O(N)。FREE时直接二分查找即可,复杂度O(logn)。

也可以使用朴素的分块算法,每个块大小为sqrt(总空间)维护自己负责的块头和块尾指针,分配与释放复杂度均为O(sqrt(n))



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

通过设置页表和对应的页表项,可建立虚拟内存地址和物理内存地址的对应关系。其中的get_pte函数是设置页表项环节中的一个重要步骤。此函数找到一个虚地址对应的二级页表项的内核虚地址,如果此二级页表项不存在,则分配一个包含此项的二级页表。本练习需要补全get_pte函数 in kern/mm/pmm.c,实现其功能。请仔细查看和理解get_pte函数中的注释。get_pte函数的调用关系图如下所示:

在这里插入图片描述
根据实验说明的科普,地址分为逻辑地址(虚地址)、线性地址和物理地址。

逻辑地址是指令使用的地址。

物理地址是实际在内存中的地址。

段页式结构中,

逻辑地址通过段式管理的映射得到线性地址,线性地址通过页式管理映射得到物理地址。

get_pte的功能是得到线性地址(在二级页表中对应的项)

参看注释,让我先读pmm.h,了解一些宏。

struct pmm_manager {
    const char *name;                                 // XXX_pmm_manager's name
    void (*init)(void);                               // initialize internal description&management data structure
                                                      // (free block list, number of free block) of XXX_pmm_manager 
    void (*init_memmap)(struct Page *base, size_t n); // setup description&management data structcure according to
                                                      // the initial free physical memory space 
    struct Page *(*alloc_pages)(size_t n);            // allocate >=n pages, depend on the allocation algorithm 
    void (*free_pages)(struct Page *base, size_t n);  // free >=n pages with "base" addr of Page descriptor structures(memlayout.h)
    size_t (*nr_free_pages)(void);                    // return the number of free pages 
    void (*check)(void);                              // check the correctness of XXX_pmm_manager 
};

有个核心结构体用作物理内存分配,即前一个练习实现的功能,有初始化,分配,释放等功能。

注释里简单介绍了些宏

在这里插入图片描述

KADDR可以从物理地址得到内核虚拟地址
PADDR可以从内核虚地址得到物理地址
PTE_P 0x001 表示物理内存页存在
PTE_W 0x002 表示物理内存页内容可写
PTE_U 0x004 表示可以读取对应地址的物理内存页内容

其中KADDR如下,实验说明也有涉及,主要方法就是加上内核基址。

 #define KADDR(pa) ({                                                    \
            uintptr_t __m_pa = (pa);                                    \
            size_t __m_ppn = PPN(__m_pa);                               \
            if (__m_ppn >= npage) {                                     \
                panic("KADDR called with invalid pa %08lx", __m_pa);    \
            }                                                           \
            (void *) (__m_pa + KERNBASE);                               \
        })

在这里插入图片描述

根据mmu.h中的注释,页式管理将32位的线性地址拆分为高10位,Directory,中10位Table和低12位Offset三部分。

Directory即一级页表

PDX(la)可以获取Directory

Table为二级页表

PTX(la)可以获取Table

改写的get_pte函数如下

pte_t *
get_pte(pde_t *pgdir, uintptr_t la, bool create) {
   
#if 0
    pde_t *pdep = NULL;   // (1) find page directory entry
    if (0) {              // (2) check if entry is not present
                          // (3) check if creating is needed, then alloc page for page table
                          // CAUTION: this page is used for page table, not for common data page
                          // (4) set page reference
        uintptr_t pa = 0; // (5) get linear address of page
                          // (6) clear page content using memset
                          // (7) set page directory entry's permission
    }
    return NULL;          // (8) return page table entry
#endif
    pde_t *pdep = &pgdir[PDX(la)];
    if (!(*pdep & PTE_P)) {
        struct Page *page;
        if (!create || (page = alloc_page()) == NULL) {
            return NULL;
        }
        set_page_ref(page, 1);
        uintptr_t pa = page2pa(page);
        memset(KADDR(pa), 0, PGSIZE);
        *pdep = pa | PTE_U | PTE_W | PTE_P;
    }
    return &((pte_t *)KADDR(PDE_ADDR(*pdep)))[PTX(la)];
}

先用PDX获取一级页表的位置,如果成功就可以直接返回

根据create位判断是否创建这个二级页表,若为0则不创建,否则创建。

将这页引用次数+1,并得到该页的物理地址

随后用

KADDR

转换为虚拟地址并初始化

设置下控制位

最后返回这个页表所对应的线性地址。



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

根据注释,页表目录项有这些标志

在这里插入图片描述

PTE_P 存在位
PTE_W 是否可写
PTE_U 该页访问需要的特权级
PTE_PWT 是否使用write through存储方法
PTE_PCD 若为1则不对该页进行缓存
PTE_A 该页是否被使用过
PTE_D 脏位
PTE_PS 设置Page大小
PTE_MBZ 恒为0

剩下的9~11位未被CPU使用可以OS支配

根据理论课知识

PDE的高二十位为PDE对应的页表起始位置。

PTE的高二十位为PTE指向的物理页的物理地址。

根据理论课知识,页表项由页号,页框号,页帧地址等组成

在这里插入图片描述

有存在位,读写有效位,访问权限位,脏位,系统程序使用有效位,和一些没有定义的标记。



对ucore的用处

页目录项和页表项中的保留位可以帮助ucore实现功能的扩展,可以建立数据结构,实现LRU之类的内存管理算法,页置换算法,和cache等配合等。



如果ucore执行过程中访问内存,出现了页访问异常,请问硬件要做哪些事情?

  • 保存发生页访问异常的地址保存到寄存器中
  • 设置错误代码,并切换到内核态;
  • 引发Page Fault,根据中断描述符表查询到对应Page Fault的中断服务程序ISR,跳转到对应的ISR处执行,由软件进行Page Fault处理,将外存的数据换到内存中;
  • 处理完后上下文切换,返回中断之前的状态



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

函数调用关系如下

在这里插入图片描述
注释讲了一些数据结构及注释的含义

struct Page *page pte2page(*ptep):获取对应的物理页
free_page:释放一页
page_ref_dec(page):引用数减一,并返回引用数,若引用数为0,则释放
tlb_invalidate(pde_t *pgdir, uintptr_t la):无效一个TLB的访问,如果它在被使用
PTE_P 0x001:位1,表示物理内存页存在

函数如下

static inline void
page_remove_pte(pde_t *pgdir, uintptr_t la, pte_t *ptep) {
#if 0
    if (0) {                      //(1) check if page directory is present
        struct Page *page = NULL; //(2) find corresponding page to pte
                                  //(3) decrease page reference
                                  //(4) and free this page when page reference reachs 0
                                  //(5) clear second page table entry
                                  //(6) flush tlb
    }
#endif
    if (*ptep & PTE_P) {
        struct Page *page = pte2page(*ptep);
        if (page_ref_dec(page) == 0) {
            free_page(page);
        }
        *ptep = 0;
        tlb_invalidate(pgdir, la);
    }
}

注释写的很好,检查这个页项目录是否存在,找到页目录项对应的页,将它的引用数-1,若为0则把它释放,清除页目录项,如果修改的页表正在使用,则无效



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

每个页目录项记录了一个页表项的信息,一个页表项也记录一个物理页的信息。

数据结构Page的全局变量的每一项记录的也是一个物理页的信息。

则页目录项和页表项中所存的物理页地址就是Page的全局变量的某一页。

所以有对应关系



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

lab2中建立了从虚拟地址到物理地址的映射,那么只需要取消映射,就可以实现虚拟地址以及物理地址相等。

因为根据LAB1的ld链接方式,虚拟地址与物理地址都是从

0x100000

开始。

可以在kernal.ld中,将链接脚本的0xC0100000改为0x100000,再把偏移量

KERNBASE

改为0即可。



实验结果

输入make qemu

在这里插入图片描述

能成功分配内存,实现first fit

输入 make grade

在这里插入图片描述

说明代码能实现相应的功能



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

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

可以用树状数组的方式存储内存,每种块的数量是是固定的,但实际分配又可以调整。

树状数组结构如下所示

在这里插入图片描述

每个数字所代表的块的大小为该数字二进制中,最低位的1的权重。故每个块虽然大小不同,但可以存在同一个线性空间中,地址转化方便。

每个块不仅要维护自己的存在位,脏位等信息,还有整合维护自己对应的线段所覆盖的块的信息。

申请分配时,可以随机一个位置i,每次向上跳,即i+=i&-i

找到一个刚好能放下的块就停止,返回这个块的地址。复杂度log(N)

释放时可直接找到这个块,但同样要log(N)时间修改覆盖这个块的块的信息。

(不保证正确性)



扩展练习Challenge:任意大小的内存单元slub分配算法(需要编程)

这其实是linux内核中比较成熟的算法,但我还是不能理解它的精髓。

单个物理页内分配小内存用slub分配,页级别的物理内存用buddy系统分配

SLAB 分配器为每种使用的内核对象建立单独的缓冲区。
每种缓冲区由多个 slab 组成,每个 slab就是一组连续的物理内存页框,被划分成了固定数目的对象。
根据对象大小的不同,缺省情况下一个 slab 最多可以由 1024 个物理内存页框构成。
内核使用 kmem_cache 数据结构管理缓冲区。
由于 kmem_cache 自身也是一种内核对象,所以需要一个专门的缓冲区。
所有缓冲区的 kmem_cache 控制结构被组织成以 cache_chain 为队列头的一个双向循环队列,同时 cache_cache 全局变量指向kmem_cache 对象缓冲区的 kmem_cache 对象。
每个 slab 都需要一个类型为 struct slab 的描述符数据结构管理其状态,同时还需要一个 kmem_bufctl_t(被定义为无符号整数)的结构数组来管理空闲对象。
如果对象不超过 1/8 个物理内存页框的大小,那么这些 slab 管理结构直接存放在 slab 的内部,位于分配给 slab 的第一个物理内存页框的起始位置;否则的话,存放在 slab 外部,位于由 kmalloc 分配的通用对象缓冲区中。



版权声明:本文为q582116859原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。