ucore_lab2_物理内存管理

练习0:填写已有实验

本处采用meld工具进行比较,使用方法:

①打开meld工具

②选择需要比对的文件/文件夹

③点击compare开始进行比较,不同的文件用星星标出

练习1:实现 first-fit 连续物理内存分配算法

算法介绍

该算法从空闲分区链首开始查找,直至找到一个能满足其大小要求的空闲分区为止。然后再按照需求的大小,从该分区中划出一块内存分配给请求者,余下的空闲分区仍留在空闲分区链中。

特点: 该算法倾向于使用内存中低地址部分的空闲区,在高地址部分的空闲区很少被利用,从而保留了高地址部分的大空闲区。显然为以后到达的大作业分配大的内存空间创造了条件。

缺点:低地址部分不断被划分,留下许多难以利用、很小的空闲区,而每次查找又都从低地址部分开始,会增加查找的开销。

算法实现

首先观察在memlayout.h中的结构体Page:

struct Page {
    int ref;                        // 表示该页被页表的引用记数(映射此物理页的虚拟页的个数)
    uint32_t flags;                 // 表示此物理页的状态标记
    unsigned int property;          // 用来记录某连续内存空闲块的大小
    list_entry_t page_link;         // 是便于把多个连续内存空闲块链接在一起的双向链表指针
};

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

进一步查看kern/mm/memlayout.h中的定义

/* Flags describing the status of a page frame */

#define PG_reserved 0 // the page descriptor is reserved for ker
nel 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,表示这页已经被分配出去了,不能被再二次分配。

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

在memlayout.h中的结构体free_area_t:

typedef struct {
    list_entry_t free_list;         // 一个list_entry结构的双向链表指针
    unsigned int nr_free;           // 记录当前空闲页的个数
} free_area_t;

pmm.h中的物理内存管理类pmm_manager:

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);            // 分配页
    void (*free_pages)(struct Page *base, size_t n);  // 释放页
    size_t (*nr_free_pages)(void);                    // 返回空闲页数
    void (*check)(void);                              // 检验管理器的正确性
};

default_pmm.c中的default_init():

static void
default_init(void) {           // 插入free_list并将nr_free置为0
    list_init(&free_list);     // 记录freemem块
    nr_free = 0;               // free mem块总数.
}

default_pmm.c中的default_init_memmap():
用来初始化空闲页链表的,初始化每一个空闲页,然后计算空闲页的总数。

static void
default_init_memmap(struct Page *base, size_t n) {
    assert(n > 0);   //使用assert宏,当为假时中止程序
    struct Page *p = base;//声明一个base的Page,随后生成起始地址为base的n个连续页
    for (; p != base + n; p ++) { //初始化n块物理页
        assert(PageReserved(p)); //确保此页不是保留页,如果是,中止程序
        p->flags = p->property= 0; //标志位置为0
        SetPageProperty(p);       //设置为保留页
        set_page_ref(p, 0);  //清除引用此页的虚拟页的个数
        list_add_before(&free_list, &(p->page_link)); //加入空闲链表
    }
    nr_free += n;  //空闲页总数置为n
    base->property = n; //修改base的连续空页值为n
}

default_pmm.c中的default_alloc_pages():
遍历空闲页块的链表,找到第一块大小大于n的块,分配出来,从空闲页链表中移除,若有多余的,再加入空闲页链表中。

static struct Page *
default_alloc_pages(size_t n) {
    assert(n > 0); 
    if (n > nr_free) { //如果需要分配的页少于空闲页的总数,返回NULL
        return NULL;
    }
    list_entry_t *le, *len; //声明一个空闲链表的头部和长度
    le = &free_list;  //空闲链表的头部

    while((le=list_next(le)) != &free_list) 
    {//遍历整个链表
      struct Page *p = le2page(le, page_link); //转换为页
      if(p->property >= n)
      {//找到页(whose first `n` pages can be malloced)
        int i;
        for(i=0;i<n;i++)
        {//对前n页进行操作
          len = list_next(le); 
          struct Page *pp = le2page(le, page_link); //列表转换为页,#define le2page(le, member)
            										//to_struct((le), struct Page, member)
          SetPageReserved(pp); //PG_reserved = '1'
          ClearPageProperty(pp);//PG_property = '0'
          list_del(le); //将此页从free_list中清除
          le = len;
        }
        if(p->property>n)
        { //如果页块大小大于所需大小,分割页块
          (le2page(le,page_link))->property = p->property-n;
        }
        ClearPageProperty(p);//取消保留,对照之前初始化部分
        SetPageReserved(p);//占用标记
        nr_free -= n; //减去已经分配的页块大小
        return p;
      }
    }
    return NULL;
}

default_free_pages()函数:用于将释放的页重新加入到页链表中,做法为遍历链表,找到第一个页地址大于所释放的第一页的地址,首先判断所释放的页的基地址加上所释放的页的数目是否刚好等于找到的地址,如果是则进行合并,同理找到这个页之前的第一个property不为0的页,判断所释放的页和上一个页是否是连续的,如果连续则进行合并操作。

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);   //修改引用次数为0 
    ClearPageProperty(base);
    SetPageProperty(base);
    base->property = n;      //设置连续大小为n
    //如果是高位,则向高地址合并
    p = le2page(le,page_link) ;
  //满足base+n==p,因此,尝试向后合并空闲页。
  //如果能合并,那么base的连续空闲页加上p的连续空闲页,且p的连续空闲页置为0;
  //如果之后的页不能合并,那么p->property一直为0,下面的代码不会对它产生影响。
    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;
          //不断更新前一个页p的property值,并将base->property置为0
          break;
        }
        le = list_prev(le);
        p = le2page(le,page_link);
      }
    }

    nr_free += n;//空闲页数量加n
    return ;
} 

改进空间:实验中的first-fit算法使用链表进行查找,时间复杂度为O(N),可以使用树状结构,尽管alloc的过程变成DFS后复杂度仍然是O(N),但free过程可以使用二分查找,复杂度为O(logn)。

练习二:实现寻找虚拟地址对应的页表项

实现细节

x86体系结构将内存地址分成三种:逻辑地址(也称虚地址)、线性地址和物理地址。
逻辑地址即是程序指令中使用的地址。
物理地址是实际访问内存的地址。
逻辑地址通过段式管理的地址映射可以得到线性地址,线性地址通过页式管理的地址映射得到物理地址。
get_pte函数给出了线性地址,即linear address。
三者的关系是:线性地址(Linear Address)是逻辑地址到物理地址变换之间的中间层。程序代码会产生逻辑地址,或说是段中的偏移地址,加上相应段的基地址就生成了一个线性地址。如果启用了分页机制,那么线性地址能再经变换以产生一个物理地址。

get_pte的注释中给出了一些宏和定义:

PDX(la): 返回虚拟地址la的页目录索引
KADDR(pa): 返回物理地址pa对应的虚拟地址
set_page_ref(page,1): 设置此页被引用一次
page2pa(page): 得到page管理的那一页的物理地址
struct Page * alloc_page() : 分配一页出来
memset(void * s, char c, size_t n) : 设置s指向地址的前面n个字节为‘c’
PTE_P 0x001 表示物理内存页存在
PTE_W 0x002 表示物理内存页内容可写
PTE_U 0x004 表示可以读取对应地址的物理内存页内容

注释中提及的KADDR()

/* *
 * KADDR - takes a physical address and returns the corresponding kernel virtual
 * address. It panics if you pass an invalid physical address.
 * */
#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中的注释:

// A linear address 'la' has a three-part structure as follows:
//
// +--------10------+-------10-------+---------12----------+
// | Page Directory |   Page Table   | Offset within Page  |
// |      Index     |     Index      |                     |
// +----------------+----------------+---------------------+
//  \--- PDX(la) --/ \--- PTX(la) --/ \---- PGOFF(la) ----/
//  \----------- PPN(la) -----------/
//
// The PDX, PTX, PGOFF, and PPN macros decompose linear addresses as shown.
// To construct a linear address la from PDX(la), PTX(la), and PGOFF(la),
// use PGADDR(PDX(la), PTX(la), PGOFF(la)).
// page directory index
#define PDX(la) ((((uintptr_t)(la)) >> PDXSHIFT) & 0x3FF)
// page table index
#define PTX(la) ((((uintptr_t)(la)) >> PTXSHIFT) & 0x3FF)
// page number field of address
#define PPN(la) (((uintptr_t)(la)) >> PTXSHIFT)
// offset in page
#define PGOFF(la) (((uintptr_t)(la)) & 0xFFF))
  
#define PTXSHIFT        12                      
// offset of PTX in a linear address
#define PDXSHIFT        22                      
// offset of PDX in a linear address

// address in page table or page directory entry
#define PTE_ADDR(pte)   ((uintptr_t)(pte) & ~0xFFF)
#define PDE_ADDR(pde)   PTE_ADDR(pde)

页式管理将32位的线性地址拆分为三部分:分别是 Directory,Table和Offset。
Ucore的页式管理通过一个二级的页表实现。一级页表存放在高10位中,二级页表存放于中间10位中,最后的12位表示偏移量,据此可以证明,页大小为4KB(2的12次方,4096)。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0oQIXbTx-1611590267231)(C:\Users\hp\AppData\Roaming\Typora\typora-user-images\image-20201119092645078.png)]

Directory为一级页表

PDX(la)可以获取Directory(获取一级页表)

Table为二级页表

PTX(la)可以获取Table(获取二级页表)

get_pte的实现:

pte_t *
get_pte(pde_t *pgdir, uintptr_t la, bool create) {
     /* LAB2 EXERCISE 2: YOUR CODE
     *
     * If you need to visit a physical address, please use KADDR()
     * please read pmm.h for useful macros
     *
     * Maybe you want help comment, BELOW comments can help you finish the code
     *
     * Some Useful MACROs and DEFINEs, you can use them in below implementation.
     * MACROs or Functions:
     *   PDX(la) = the index of page directory entry of VIRTUAL ADDRESS la.
     *   KADDR(pa) : takes a physical address and returns the corresponding kernel virtual address.
     *   set_page_ref(page,1) : means the page be referenced by one time
     *   page2pa(page): get the physical address of memory which this (struct Page *) page  manages
     *   struct Page * alloc_page() : allocation a page
     *   memset(void *s, char c, size_t n) : sets the first n bytes of the memory area pointed by s
     *                                       to the specified value c.
     * DEFINEs:
     *   PTE_P           0x001                   // page table/directory entry flags bit : Present
     *   PTE_W           0x002                   // page table/directory entry flags bit : Writeable
     *   PTE_U           0x004                   // page table/directory entry flags bit : User can access
     */
#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)];
    //使用PDX,获取一级页表的位置,如果成功,直接返回
    if (!(*pdep & PTE_P)) {//如果失败
        struct Page *page;
        //根据create位判断是否创建这个二级页表
        //如果为0,不创建,不为0则创建
        if (!create || (page = alloc_page()) == NULL) {
            return NULL;
        }
        set_page_ref(page, 1);//要查找该页表,则引用次数+1
        uintptr_t pa = page2pa(page);//得到该页的物理地址
        memset(KADDR(pa), 0, PGSIZE);//转成虚拟地址并初始化
        //因为这个页所代表的虚拟地址都没有被映射
        *pdep = pa | PTE_U | PTE_W | PTE_P;
        //设置控制位,同时设置PTE_U,PTE_W和PTE_P,存在,可读,可写
    }
return &((pte_t *)KADDR(PDE_ADDR(*pdep)))[PTX(la)];
//用KADDR返回二级页表所对应的线性地址
//这里不是要求物理地址,而是需要找对应的二级页表项,在查询完二级页表之前,都还是在虚拟地址的范围。
}

回答问题

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

/* page table/directory entry flags */
#define PTE_P           0x001                   // Present
#define PTE_W           0x002                   // Writeable
#define PTE_U           0x004                   // User
#define PTE_PWT         0x008                   // Write-Through
#define PTE_PCD         0x010                   // Cache-Disable
#define PTE_A           0x020                   // Accessed
#define PTE_D           0x040                   // Dirty
#define PTE_PS          0x080                   // Page Size
#define PTE_MBZ         0x180                   // Bits must be zero
#define PTE_AVAIL       0xE00                   // Available for software use
                                                
// The PTE_AVAIL bits aren't used by the kernel or interpreted by the hardware, so user processes are allowed to set them arbitrarily.

#define PTE_USER        (PTE_U | PTE_W | PTE_P)

组成部分地址含义
PTE_P0存在位
PTE_W1Writeable
PTE_U2该页访问需要的特权级
PTE_PWT3是否使用write through
PTE_PCD4若为1则不对该页进行缓存
PTE_A5该页是否被使用过
PTE_D6脏位
PTE_PS7设置Page大小
PTE_MBZ8恒为0

| PTE_AVAIL | 9-11 | 未被CPU使用可以保留给OS |

PDE的高二十位表示该PDE对应的页表起始位置。(物理地址)
PTE的高二十位表示该PTE指向的物理页的物理地址。

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

  • 将引发页访问异常的地址保存在cr2寄存器中。

  • 在中断栈中依次压入EFLAGS,CS, EIP,以及页访问异常码error code,

    如果page fault是发生在用户态,则还需要先压入ss和esp,并且切换到内核栈;

  • 引发Page Fault,根据中断描述符表查询到对应Page Fault的ISR,

    跳转到对应的ISR处执行,接下来将由软件进行Page Fault处理;

练习三:释放某虚地址所在的页并取消对应二级页表项的映射

实现细节

page_remove_pte()函数的调用关系图如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OLMNmXan-1611590267232)(C:\Users\hp\AppData\Roaming\Typora\typora-user-images\image-20201123154326616.png)]

page_remove_pte()的注释:

 Please check if ptep is valid, and tlb must be manually updated if mapping is updated
     
      Maybe you want help comment, BELOW comments can help you finish the code
     
      Some Useful MACROs and DEFINEs, you can use them in below implementation.
      MACROs or Functions:
        struct Page page pte2page(ptep): get the according page from the value of a ptep
        free_page : free a page
        page_ref_dec(page) : decrease page->ref. NOTICE: ff page->ref == 0 , then this page should be free.
        tlb_invalidate(pde_t pgdir, uintptr_t la) : Invalidate a TLB entry, but only if the page tables being
                             edited are the ones currently in use by the processor.
      DEFINEs:
        PTE_P           0x001                   
          // page table/directory entry flags bit : Present

注释中提到的宏定义和函数定义:

static inline struct Page *
pte2page(pte_t pte) {//从ptep值中获取相应的页面
    if (!(pte & PTE_P)) {
        panic("pte2page called with invalid pte");
    }
    return pa2page(PTE_ADDR(pte));
}

static inline int //减少该页的引用次数,返回剩下的引用次数。
page_ref_dec(struct Page *page) {
    page->ref -= 1;
    return page->ref;
}

// invalidate a TLB entry, but only if the page tables being
// edited are the ones currently in use by the processor.
void
tlb_invalidate(pde_t *pgdir, uintptr_t la) {
    if (rcr3() == PADDR(pgdir)) {
        invlpg((void *)la);
    }
}
//当修改的页表目前正在被进程使用时,使之无效。

/* *
 * PADDR - takes a kernel virtual address (an address that points above KERNBASE),
 * where the machine's maximum 256MB of physical memory is mapped and returns the
 * corresponding physical address.  It panics if you pass it a non-kernel virtual address.
 * */
#define PADDR(kva) ({                                                   \
            uintptr_t __m_kva = (uintptr_t)(kva);                       \
            if (__m_kva < KERNBASE) {                                   \
                panic("PADDR called with invalid kva %08lx", __m_kva);  \
            }                                                           \
            __m_kva - KERNBASE;                                         \
        })
  
#define PTE_P           0x001                   // Present

page_remove_pte():

static inline void
page_remove_pte(pde_t *pgdir, uintptr_t la, pte_t *ptep) {
    /* LAB2 EXERCISE 3: YOUR CODE
     *
     * Please check if ptep is valid, and tlb must be manually updated if mapping is updated
     *
     * Maybe you want help comment, BELOW comments can help you finish the code
     *
     * Some Useful MACROs and DEFINEs, you can use them in below implementation.
     * MACROs or Functions:
     *   struct Page *page pte2page(*ptep): get the according page from the value of a ptep
     *   free_page : free a page
     *   page_ref_dec(page) : decrease page->ref. NOTICE: ff page->ref == 0 , then this page should be free.
     *   tlb_invalidate(pde_t *pgdir, uintptr_t la) : Invalidate a TLB entry, but only if the page tables being
     *                        edited are the ones currently in use by the processor.
     * DEFINEs:
     *   PTE_P           0x001                   // page table/directory entry flags bit : Present
     */
#if 0
    if (0) {                      //(1) check if this page table entry 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) {  //PTE_P代表页存在
        struct Page *page = pte2page(*ptep); 
        if (page_ref_dec(page) == 0) {
          //如果只被上一级页表引用一次,那么-1后就是0,页和对应的二级页表都能被直接释放
            free_page(page); //释放页
        }
    		//但如果有更多的页表引用了它,则不能释放这个页,但可以取消对应二级页表的映射。
    		//即把传入的二级页表置为0
        *ptep = 0;
        tlb_invalidate(pgdir, la); 
    }
}

回答问题

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

    存在对应关系:由于页表项中存放着对应的物理页的物理地址,因此可以通过这个物理地址来获取到对应到的Page数组的对应项。

  2. 如果希望虚拟地址与物理地址相等,则需要如何修改lab2,完成此事?

    阅读实验参考书“系统执行中地址映射的四个阶段”

    物理地址和虚拟地址之间存在offset:

    phy addr + KERNBASE = virtual addr

    所以,KERNBASE = 0时,phy addr = virtual addr

    所以把memlayout.h中的:

    
    /* All physical memory mapped at this address */
     
    #define KERNBASE            0xC0000000
    
    

    KERNBASE改为0即可。

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值