操作系统ucore Lab2笔记

知识点

物理内存管理


理论课知识点

  • 操作系统的内存管理方式

    • 重定位(relocation)
    • 分段(segmentation)
    • 分页(paging)
    • 虚拟存储(vitual memory)
  • 连续内存分配算法

    • 最先匹配(First Fit Allocation):
      空闲分区列表按地址排序
      分配过程时,搜索第一个合适的分区
      释放分区时,检查是否可与临近的空闲分区合并

    • 最优匹配(Best Fit Allocation):
      空闲分区列表按地址排序
      分配过程时,搜索一个合适的分区(寻找一个比所需空间大最少的)
      释放分区时,检查是否可与临近的空闲分区合并

    • 最差匹配(Worst Fit Allocation):
      空闲分区列表按地址排序
      分配过程时,选择最大的分区
      释放分区时,检查是否可与临近的空闲分区合并

  • 页与页帧

    • 帧(frame):物理内存被划分为大小相同的帧,表示为二元组(f, o) /* (帧号,帧内偏移) */
    • 页(page):进程逻辑地址空间被划分为大小相同的页,表示为二元组(p, o) /(页号,页内偏移)/
    • 页表:页表保存了物理地址——逻辑地址的映射关系(页到帧的映射)
  • 快表和多级页表
    旨在解决页表存储空间过大的问题

    • 快表(Translation Look-aside Buffer, TLB):缓存近期访问的页表项
    • 多级页表:通过间接引用将页号分为k级,上级页表记录下级页表的索引,通过上级访问下级,无法直接访问下级
  • 反置页表
    基于HASH映射值查找对应页表项中的帧号

实验课的理解

代码的理解在课程提供的gitbookhttps://chyyuu.gitbooks.io/ucore_os_docs/content/有较为全面的介绍。这里主要记载一些阅读代码时注意到的点。

  • 连续内存的分配算法
    实验中使用到的算法是最先匹配策略,算法的代码实现在kern/mm/default_pmm.c. 实验要求按照自己的设计思想修改可能涉及到的函数default_init,default_init_memmap,default_alloc_pages, default_free_pages。但阅读代码之后,发现原来的代码就已经实现了最先匹配的算法,不做修改也是可行的。与答案的代码相比,两者的区别主要是设计理念的不同,而不是答案是前者的补充。
    为了理解算法,得先知道算法相关的这几个函数是如何被调用的,可以发现pmm_init函数调用了大部分的函数。位于kern/mm/pmm.c的pmm_init的功能是完成内存管理的初始化,需要感知到被探测到的物理内存,于是向前了解到了位于boot/bootasm.S的内容。于是,例如理解default_init_memmap,可以从
kern_init --> pmm_init-->page_init-->init_memmap--> pmm_manager->default_init_memmap

去理解代码。
现在记录一下修改前的代码是如何实现算法的。算法的几个函数都在维护一个全局链表free_area.free_list(free_list)与空闲内存数量free_area.nr_free(nr_free), free_list为链表入口,链表节点为

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

是一个典型的双向链表节点的结构,除了指向前后节点的指针别无他物。但显然,链表节点需要包含更多的信息来记录一些东西,ucore这里借助了结构体page来将链表节点包装起来,也就是说将节点作为page的成员出现。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
};

为什么不将ref, flags, property这些数据成员通通作为链表节点list_entry的成员就好了,而要把数据成员和指针分开分别操作呢?我的看法是主要因为这里的算法是管理连续内存的,每个链表节点代表一个空闲内存块,可以包含多个内存页,所以链表节点list_entry是可以比Page的数量少的,分开来操作可以减少声明的数量,增加系统执行的效率。而且看宏函数le2page(le, member)实现的功能是将链表节点le所在地址向前(向低)偏移一定的长度,并返回一个Page *指针。

// convert list entry to page
#define le2page(le, member)                 \
    to_struct((le), struct Page, member)
#define to_struct(ptr, type, member)                               \
    ((type *)((char *)(ptr) - offsetof(type, member)))
#define offsetof(type, member)                                      \
    ((size_t)(&((type *)0)->member))

offsetof()的功能是测量结构体成员相比结构体首地址的地址偏移量,返回值代表了偏移的字节数(地址的数值是以字节为单位的)。具体做法是将地址0强制转换成结构体指针,代表在地址0处申请了一个结构体,并取其成员的地址自然得到了偏移量。
to_struct()将传入的指针强制转换成char *, 刚开始看很困惑,为什么一定是char *, 这里和字符操作好像没什么关系。后来想到char类型刚好是一个字节的长度,所以char *可以得到以字节为度量的地址,刚好和offsetof相恰,找到链表想要转换成结构体的地址,并将该地址的类型强制转换。这相当于申请了一个结构体,同时初始化了指定的结构成员,但基本上只涉及到地址操作,所以操作效率很高。在算法代码中,可以看到le2page的使用率非常高。
回到算法涉及到的函数。default_init初始化了free_list和nr_free; default_init_memmap在探测到的物理内存的开头地址处紧凑地写入多个结构体Page的信息,Page的个数就是页的个数,并对内存的权限进行设置。

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);            //第一个Page(base)可以分配释放
    nr_free += n;
    list_add(&free_list, &(base->page_link));
      //特别注意list_add的位置,没有在for循环里,与答案的代码可以进行对比;
      //另外还要注意的是链表头&free_list没有代表一个空闲块,只是作为双向链表的入口      
}

default_alloc_page()根据外界需要的内存页数调整空闲内存块所在的链表free_list

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) {                              //找到了符合条件的页
        list_del(&(page->page_link));                //这个地址已经不是空闲块的起始了,从链表删除
        if (page->property > n) {
            struct Page *p = page + n;
            p->property = page->property - n;
            list_add(&free_list, &(p->page_link));   //将剩余的空闲块处理完后插链表
    }
        nr_free -= n;
        ClearPageProperty(page);                     //用户态不可访问
    }
    return page;
}

default_free_page(),有借就有还,在归还被分配的内存的同时检查是否有合并的分区。

static void
default_free_pages(struct Page *base, size_t n) {
    assert(n > 0);
    struct Page *p = base;
    for (; p != base + n; p ++) {
        assert(!PageReserved(p) && !PageProperty(p)); //不要释放了本来就是空闲状态的内存,防止释放越界
        p->flags = 0;            //页未使用的标志
        set_page_ref(p, 0);      //被0个对象引用
    }
    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 += p->property;
            ClearPageProperty(p);
            list_del(&(p->page_link));
        }
        else if (p + p->property == base) {    //另一种情况,欲释放内存在已有内存块之前
            p->property += base->property;
            ClearPageProperty(base);
            base = p;
            list_del(&(p->page_link));
        }
    }
    nr_free += n;
    list_add(&free_list, &(base->page_link));
}

答案的代码则是一种不同的思路,用双向链表代表已分配的内存,而不是空闲内存块。由于内存分配每次的大小变化较大,链表节点的地址的变化估计会很频繁,干脆每个节点代表一个页,每个页不可再分,内存的分配与释放与链表的节点联动,是一种比较直接的做法。但是链表会变得很长,在沿着链表操作的时候,操作时间会明显增加;而且每个Page的page_link全不为空,也占有更大的运行内存。
剩下的练习2和练习3可以仔细看看pmm.c的pmm_init(),其中的英文注释挺详尽的,可以补充不少知识。
练习2练习3参考https://chyyuu.gitbooks.io/ucore_os_docs/content/lab2/lab2_3_3_5_3_setup_paging_map.html
Tip:关注一下boot_pgdir可以更好地理解来龙去脉~
关于KERN_BASE高虚拟地址(0xC0000000)的解释https://wiki.osdev.org/Higher_Half_bare_bones

posted @ 2020-08-03 00:13  SilenceJoice  阅读(206)  评论(0)    收藏  举报