ucore lab3(无challenge)

6,054 阅读13分钟

虚拟内存及其数据结构

首先一点就是我们现在讲的虚拟内存都是采取段页式管理的

关于虚拟内存的知识可以看我写的blog(malloc & free的实现(malloc lab) - 掘金 (juejin.cn))看到动态存储器分配之前就行和这篇blog(xuan-insr.github.io/%E6%A0%B8%E…)

总体上说就是运用抽象来做到程序享受所有的存储器,好像整个电脑中只有我们写的程序(加一个os)。具体点就是你在google上搜memory layout,就会搜出来这样一幅图

image-20221125185208424

虚拟内存我的理解就是这整个存储器映像作为程序的存储空间。

并且存储器映像每一个段有了一个新名字叫vma(virtual memory area)

struct vma_struct {
    // the set of vma using the same PDT
    struct mm_struct *vm_mm; //指向一个更高级的数据结构
    uintptr_t vm_start; // start addr of vma
    uintptr_t vm_end; // end addr of vma
    uint32_t vm_flags; // flags of vma 就是可读可写那些
    //linear list link which sorted by start addr of vma
    list_entry_t list_link; //链表
};

这个高级的数据结构mm表示了目前ucore认为合法的所有虚拟内存空间集合(因为在目前这个阶段只有一个内核进程)

struct mm_struct {
    // linear list link which sorted by start addr of vma
    list_entry_t mmap_list; //虚拟页的链表,链接了所有属于同一页目录表的虚拟内存空间
    // current accessed vma, used for speed purpose
    struct vma_struct *mmap_cache; //页的cache,指向当前正在使用的虚拟内存空间
    pde_t *pgdir; // the PDT of these vma 页表
    int map_count; // the count of these vma 链表链接的 vma_struct的个数
    void *sm_priv; // the private data for swap manager 指向用来链接记录页访问情况的链表头
};

具体关系看下面的图

我的暴论:虚拟内存就是存储器映像,就是由mm_struct所描述。

程序加载与虚拟内存的关系

vma mapping

我们写的程序(比如a.out)是放在磁盘上,如果我们想要运行它,该怎么办?

那我们要用到一个叫存储器映射的东西(这么说好像不准确,因为是这个过程叫存储器映射),反正就是a.out(elf文件中有很多个segment,不知道的可以去看csapp 的link那一章)的segment映射到存储器的vma中。

而事实上每一个vma被映射过后(或者说初始化后)就要装入swap分区。什么是swap分区,swap怎么用?

Swap space交换空间,是虚拟内存的表现形式。系统为了应付一些需要大量内存的应用,而将磁盘上的空间做内存使用,当物理内存不够用时,将其中一些暂时不需要的数据交换到交换空间,也叫交换文件页面文件中。

关于swap,我找了一个图(jyywiki.cn/pages/OS/im…)是我在看jyy的视频中发现的。其实我感觉任何一个缓存都可以用这张图来描述

vma mapping在计算机中表示就是下面这张图

image-20221231094921223

图片来源于 C/C++ 中程序内存区域划分((56条消息) C/C++ 中程序内存区域划分Baoming ROSE的博客-CSDN博客c++内存区域划分

image-20221231133248122

image-20221125221124002

图片 来源于csapp

image-20221231093922878

图片是 我在ucore 指导书上截取的

我们现在只是有了映射关系,接下来执行a.out代码

(突然想到一个问题什么是代码开始的地方?或者说main函数是不是程序开始的地方?我觉得肯定不是,因为main函数也是被别的函数调用的。我应该会写一篇关于link的blog来解释的)

pagefault

执行时肯定会出错,因为现在关于a.out的文件都在磁盘上。不过出错在那一步?

出错在虚拟页到物理页的转换中,就是在mmu进行地址翻译的时候出错

不过巧就巧在这个出错上了,没错这个出错就是所谓的pagefault,就是内存中没有这一页。那我们怎么知道内存没有这一页呢,这意味着我们需要一个表来记录,没错正是页表。(不过页表不是因为虚拟内存才出现的,如果没有开启分页,那这个表就不是页表,但它还是存在的,因为它本质是记录 程序内使用地址和物理地址的映射关系的。如果采用分段来管理虚拟内存,那这个表就是段表)

并且我们的映射关系也是记录在页表上的。所以根据映射关系和是否在内存两项,把页表项分成了三种。(可能有人要问为什么是三,不是2^2种,因为如果没有映射自然就谈不上是否在内存了)

image-20221213214123389

第一种:全0,代表没有映射

第二种:最后一位p位为1,代表在内存中

第三种:p位为0,但是前面不为0,代表不在内存

说回到pagefault,现在我们就是根据页表项来判断和处理pagefault。目前我们的页表中关于a.out的页表项肯定是属于第三种情况了(有映射不在内存),肯定会出现pagefult(在mmu翻译a.out程序地址时出现),然后出错os肯定会管呀。那os就会把控制权交给pagefault_handler,不过在把控制权交给handler之前要把异常的线性地址存入 cr2 寄存器中 并且给出 错误码 error_code 说明页访问异常的具体原因

error_code in pagefault&segment fault

在pagefault_handler中error_code分为三种:一种是不在某个VMA的地址范围内 或 不满足正确的读写权限 则是非法访问.在我们写c语言指针时经常出现的段错误就是这个error。在程序load时完成了vma映射,但是在运行时,程序中的指针由于我们的操作不当(比如野指针)跑到奇怪的地方,这些地方并不在我们映射的vma中,段错误便出现了。

image-20221231133403623

一种是页表全零,代表没有映射

一种是p位为0,但是前面不为0,代表不在内存

swap

现在error_code表示我们现在是属于页在swap分区中,于是我们就要在swap分区中找到a.out文件所在的页并把它装入内存(因为每一个vma被映射过后就要装入swap分区)。于是就有了一个问题我们怎么在swap分区中找到页?

于是我们的ucore就用了一个tricky的方法,就是在我们上面页表的第三种情况动了一个小脑筋,因为当一个PTE用来描述一般意义上的物理页时,显然它就是描述物理页与虚拟页的关系;但p为0零时,就不存在这种关系。于是我们的ucore就利用了这一特性,既然不存在物理页与虚拟页的关系,那么就让它存虚拟页与磁盘页的关系。

ucore为了描述这种关系还构建了一种数据类型

swap_entry_t
-------------------------
| offset | reserved | 0 |
-------------------------
24 bits    7 bits   1 bit

前24位做index(磁盘的起始扇区位置),后面7位保留,最后一位0

考虑到硬盘的最小访问单位是一个扇区,而一个扇区的大小为512(2^8)字节,所以需要8个连续扇区才能放置一个4KB的页。

在ucore中,用了第二个IDE硬盘来保存被换出的扇区,就是swap区(有关于分区,文件系统的事情,以前有人向我推荐了lfs,不过我也没真的做过,所以我也只是提一嘴)

但是这样还有一个问题就是无法区别表示0扇区的pte和不存在映射的全0pte,于是ucore将 swap 分区的一个 page 空出来不用,也就是高24位不为0。

那你有没有想过为什么要换出,换入?这不是废话吗,内存又不是无限的,而且换入换出也可以视为对内存缓存的刷新。

那ucore是怎么采取换入换出的,换言之ucore是怎么进行换入换出的,什么时候进行换入换出

页换入很简单,在发生pagefault时就是执行页换入的最好时机

而换出页面的时机相对复杂一些,针对不同的策略有不同的时机。ucore目前大致有两种策略,即积极换出策略和消极换出策略。积极换出策略是指操作系统周期性地(或在系统不忙的时候)主动把某些认为“不常用”的页换出到硬盘上,从而确保系统中总有一定数量的空闲页存在,这样当需要空闲页时,基本上能够及时满足需求;消极换出策略是指,只是当试图得到空闲页时,发现当前没有空闲的物理页可供分配,这时才开始查找“不常用”页面,并把一个或多个这样的页换出到硬盘上。在lab3的基本练习中,支持上述的第二种情况。

这个查找“不常用”页面是就要考虑到一系列算法了,目前ucore采用的默认算法是FIFO

swap涉及的数据结构

ucore中对于一些函数用了一个tricky的方法进行抽象,就比如swap所涉及的函数抽象成一个swap_manager数据结构,把所有的函数做成函数指针

struct swap_manager  
{  
    const char *name;  
    /* Global initialization for the swap manager */  
    int (*init) (void);  
    /* Initialize the priv data inside mm_struct */  
    int (*init_mm) (struct mm_struct *mm);  
    /* Called when tick interrupt occured */  
    int (*tick_event) (struct mm_struct *mm);  
    /* Called when map a swappable page into the mm_struct */  
    int (*map_swappable) (struct mm_struct *mm, uintptr_t addr, struct Page *page, int swap_in);   
    /* When a page is marked as shared, this routine is called to delete the addr entry from the swap manager */
    int (*set_unswappable) (struct mm_struct *mm, uintptr_t addr);  
    /* Try to swap out a page, return then victim */  
    int (*swap_out_victim) (struct mm_struct *mm, struct Page *ptr_page, int in_tick);  
    /* check the page relpacement algorithm */  
    int (*check_swap)(void);   
};

这里关键的两个函数指针是map_swappable和swap_out_vistim,前一个函数用于记录页访问情况相关属性,后一个函数用于挑选需要换出的页。显然第二个函数依赖于第一个函数记录的页访问情况。tick_event函数指针也很重要,结合定时产生的中断,可以实现上面提到的积极的换页策略。

因为默认采用FIFO的页面置换算法,所以在page属性上要加上调用时间

struct Page {  
……   
list_entry_t pra_page_link;   
uintptr_t pra_vaddr;   
};

pra_page_link可用来构造按页的第一次访问时间进行排序的一个链表,这个链表的开始表示第一次访问时间最近的页,链表结尾表示第一次访问时间最远的页。当然链表头可以就可设置为pra_list_head(定义在swap_fifo.c中),构造的时机是在page fault发生后,进行do_pgfault函数时。pra_vaddr可以用来记录此物理页对应的虚拟页起始地址。

因为我们的物理page页所载入的内存,换言之就是虚拟page对应的物理page是变换的,所以要记录下物理page所对应的虚拟page

pagefault_handler

前面说了,要根据error_code进行处理,error_code分为三类,所以handler也要进行分类处理:

第一类:直接报错

第二类:如果是真的不存在,则需要立即为其分配一个初始化后全新的物理页,并建立映射虚实关系(设置页表)

在分配页面完要考虑到两件事:1.更新页表关于这个la的pte ucore中有一个函数page_insert就是干这个事的

2.记录这个页的访问情况相关属性

第三类:如果是被暂时交换到了磁盘中,则需要将交换扇区中的数据重新读出并覆盖所分配到的物理页。

伪代码:

    int ret = -E_INVAL;
​
    struct vma_struct *vma = find_vma(mm, addr);
​
    pgfault_num++;
    //If the addr is in the range of a mm's vma?
    if (vma == NULL || vma->vm_start > addr) {
        cprintf("not valid addr %x, and  can not find it in vma\n", addr);
        goto failed;
    }
    //check the error_code
    switch (error_code & 3) {
    default:
            /* error code flag : default is 3 ( W/R=1, P=1): write, present */
    case 2: /* error code flag : (W/R=1, P=0): write, not present */
        if (!(vma->vm_flags & VM_WRITE)) {
            cprintf("do_pgfault failed: error code flag = write AND not present, but the addr's vma cannot write\n");
            goto failed;
        }
        break;
    case 1: /* error code flag : (W/R=0, P=1): read, present */
        cprintf("do_pgfault failed: error code flag = read AND present\n");
        goto failed;
    case 0: /* error code flag : (W/R=0, P=0): read, not present */
        if (!(vma->vm_flags & (VM_READ | VM_EXEC))) {
            cprintf("do_pgfault failed: error code flag = read AND not present, but the addr's vma cannot read or exec\n");
            goto failed;
        }
    }//以上代码用于检查段错误以及进行权限检查,构建出此页所对应的权限term
​
​
pte_t pte = find_pte(mm,addr);//找到cr2中虚拟地址所在的pteif(*pte){
    pgdir_alloc_page(mm->pgdir, addr, perm);
    /*pgdir_alloc_page一个函数做两件事情
    1.更新页表关于这个la的pte  ucore中有一个函数page_insert就是干这个事的
    2.记录这个页的访问情况相关属性*/
}//全0,表示代表没有映射
else{
    struct page *page;
    swap_in(mm,page);//把磁盘页放到page中
    page_insert(mm,page);//更新页表关于这个la的pte
    swap_manager_map_swappable(page);//记录page信息,加入page链表
    page->pra_vaddr = addr;//设置page对应的la
}//表示暂时交换到了磁盘
ret = 0;
failed:
    return ret; //如果ret不是0,就代表do_pagefault出错

练习一 给未被映射的地址映射上物理页

其实练习一要做的不止这些,我们首先要给ucore加上处理缺页的中断,处理缺页的函数ucore已经提前写过了,就是do_pagefault

但是我们还要写一个handler来调用do_pagefault函数,但在中断总控函数中要调用这个handler

static int
pgfault_handler(struct trapframe *tf) {
    extern struct mm_struct *check_mm_struct;
    print_pgfault(tf);
    if (check_mm_struct != NULL) {
        return do_pgfault(check_mm_struct, tf->tf_err, rcr2());
    }
    panic("unhandled page fault.\n");
}
​
switch (tf->tf_trapno) {
    case T_PGFLT:  //page fault
        if ((ret = pgfault_handler(tf)) != 0) {
            panic("handle pgfault failed. %e\n", ret);
        }
        break;
}

然后再完成我们的do_pagefault函数,具体就按上面提到的pagefault_handler

ptep = get_pte(mm->pgdir, addr, 1);
     
    if (*ptep == 0) { // if the phy addr isn't exist, then alloc a page & map the phy addr with logical addr
        if (pgdir_alloc_page(mm->pgdir, addr, perm) == NULL) {
            cprintf("pgdir_alloc_page in do_pgfault failed\n");
            goto failed;
        }
    }
    else { // if this pte is a swap entry, then load data from disk to a page with phy addr
           // and call page_insert to map the phy addr with logical addr
        if(swap_init_ok) {
            struct Page *page=NULL;
            if ((ret = swap_in(mm, addr, &page)) != 0) {
                cprintf("swap_in in do_pgfault failed\n");
                goto failed;
            }    
            page_insert(mm->pgdir, page, addr, perm);
            swap_map_swappable(mm, addr, page, 1);
            page->pra_vaddr = addr;
        }
        else {
            cprintf("no swap_init_ok but ptep is %x, failed\n",*ptep);
            goto failed;
        }
   }

练习二 补充完成基于FIFO的页面替换算法

完成vmm.c中的do_pgfault函数,并且在实现FIFO算法的swap_fifo.c中完成map_swappable和swap_out_victim函数。通过对swap的测试。

static int
_fifo_map_swappable(struct mm_struct *mm, uintptr_t addr, struct Page *page, int swap_in)
{
    list_entry_t *head=(list_entry_t*) mm->sm_priv;
    list_entry_t *entry=&(page->pra_page_link);
 
    assert(entry != NULL && head != NULL);
    //record the page access situlation
    /*LAB3 EXERCISE 2: YOUR CODE*/ 
    //(1)link the most recent arrival page at the back of the pra_list_head qeueue.
    list_add(head,entry); //加入链表头部
    return 0;
}
​
static int
_fifo_swap_out_victim(struct mm_struct *mm, struct Page ** ptr_page, int in_tick)
{
     list_entry_t *head=(list_entry_t*) mm->sm_priv;
         assert(head != NULL);
     assert(in_tick==0);
     /* Select the victim */
     /*LAB3 EXERCISE 2: YOUR CODE*/ 
     //(1)  unlink the  earliest arrival page in front of pra_list_head qeueue
     //(2)  set the addr of addr of this page to ptr_page
     list_entry_t *last= head ->prev;
        assert(last != NULL);           
     struct Page *p = le2page(last,pra_page_link);
     list_del(last);
     *ptr_page = p;
     return 0;
}

结果&总结

结果:

image-20221231204239864

总结:

这次实验将swap分区建立的细节省略了,给我们的只有swap_in这个接口,关于swap_manager使用的也是FIFO,所以这次实验页感觉比前两次的简单

这次blog距离上次过来很久,因为我忙着很多事:各种奇怪作业的ddl,准备六级,准备期末考试(后来因为疫情没考),做两个没什么意义的课设,还"感冒"了,休息了好多天。不过现在寒假了,我现在有很多时间

本期推荐音乐:

1.What's Going On by marvin gaye

2.respect by aretha franklin

3.stand by me by Ben E. King