页面置换机制

现在我们来看看ucore页面置换机制的实现。

页面置换机制中, 我们需要维护一些”不在内存当中但是也许会用到“的页,它们存储在磁盘的交换区里,也有对应的虚拟地址,但是因为它们不在内存里,在页表里并没有对它们的虚拟地址进行映射。但是在发生Page Fault之后,会把访问到的页放到内存里,这时也许会把其他页扔出去,来给要用的页腾出地方。页面置换算法的核心任务,主要就是确定”把谁扔出去“。

页表里的信息只是"当前哪些数据在内存条里以及它们物理地址和虚拟地址的对应关系", 这里我们需要一些页表之外的数据结构来维护当前页表里没映射的页。也就是这些信息:有哪些虚拟地址对应的页当前在磁盘上,分别在磁盘上的哪个位置。有哪些虚拟地址对应的页面当前放在内存里。这两类页面(内存上的,磁盘上的)会相互转换(换入/换出内存),所以我们将这两类页面一起维护,也就是维护”所有可用的虚拟地址/虚拟页的集合“(不论当前这个虚拟地址对应的页在内存上还是在硬盘上)。之后我们将要实现进程机制,对于不同的进程,可用的虚拟地址(虚拟页)的集合常常是不一样的,因此每个进程需要一个页表,也需要一个数据结构来维护“所有可用的虚拟地址”。

我们在vmm.h定义两个结构体 (vmm: virtural memory management)。

vma_struct结构体描述一段连续的虚拟地址,从vm_startvm_end。 通过包含一个list_entry_t成员,我们可以把同一个页表对应的多个vma_struct结构体串成一个链表,在链表里把它们按照区间的起始点进行排序。

vm_flags表示的是一段虚拟地址对应的权限(可读,可写,可执行等),这个权限在页表项里也要进行对应的设置。

我们注意到,每个页表(每个虚拟地址空间)可能包含多个vma_struct, 也就是多个访问权限可能不同的,不相交的连续地址区间。我们用mm_struct结构体把一个页表对应的信息组合起来,包括vma_struct链表的首指针,对应的页表在内存里的指针,vma_struct链表的元素个数。

(参考libs/list.h

// kern/mm/vmm.h
//pre define
struct mm_struct;

// the virtual continuous memory area(vma), [vm_start, vm_end),
// addr belong to a vma means  vma.vm_start<= addr <vma.vm_end
struct vma_struct {
    struct mm_struct *vm_mm; // the set of vma using the same PDT
    uintptr_t vm_start;      // start addr of vma
    uintptr_t vm_end;        // end addr of vma, not include the vm_end itself
    uint_t vm_flags;       // flags of vma
    list_entry_t list_link;  // linear list link which sorted by start addr of vma
};

#define le2vma(le, member)                  \
    to_struct((le), struct vma_struct, member)

#define VM_READ                 0x00000001
#define VM_WRITE                0x00000002
#define VM_EXEC                 0x00000004

// the control struct for a set of vma using the same Page Table
struct mm_struct {
    list_entry_t mmap_list;        // linear list link which sorted by start addr of vma
    struct vma_struct *mmap_cache; // current accessed vma, used for speed purpose
    pde_t *pgdir;                  // the Page Table of these vma
    int map_count;                 // the count of these vma
    void *sm_priv;                   // the private data for swap manager
};

我们需要为vma_structmm_struct定义和实现一些接口:包括它们的构造函数,以及如何把新的vma_struct插入到mm_struct对应的链表里。注意这两个结构体占用的内存空间需要用kmalloc()函数动态分配。

// kern/mm/vmm.c
// mm_create -  alloc a mm_struct & initialize it.
struct mm_struct *
mm_create(void) {
    struct mm_struct *mm = kmalloc(sizeof(struct mm_struct));

    if (mm != NULL) {
        list_init(&(mm->mmap_list));
        mm->mmap_cache = NULL;
        mm->pgdir = NULL;
        mm->map_count = 0;

        if (swap_init_ok) swap_init_mm(mm);//我们接下来解释页面置换的初始化
        else mm->sm_priv = NULL;
    }
    return mm;
}

// vma_create - alloc a vma_struct & initialize it. (addr range: vm_start~vm_end)
struct vma_struct *
vma_create(uintptr_t vm_start, uintptr_t vm_end, uint_t vm_flags) {
    struct vma_struct *vma = kmalloc(sizeof(struct vma_struct));
    if (vma != NULL) {
        vma->vm_start = vm_start;
        vma->vm_end = vm_end;
        vma->vm_flags = vm_flags;
    }
    return vma;
}

在插入一个新的vma_struct之前,我们要保证它和原有的区间都不重合。

// kern/mm/vmm.c
// check_vma_overlap - check if vma1 overlaps vma2 ?
static inline void
check_vma_overlap(struct vma_struct *prev, struct vma_struct *next) {
    assert(prev->vm_start < prev->vm_end);
    assert(prev->vm_end <= next->vm_start);
    assert(next->vm_start < next->vm_end);// next 是我们想插入的区间,这里顺便检验了start < end
}

我们可以插入一个新的vma_struct, 也可以查找某个虚拟地址对应的vma_struct是否存在

// kern/mm/vmm.c

// insert_vma_struct -insert vma in mm's list link
void
insert_vma_struct(struct mm_struct *mm, struct vma_struct *vma) {
    assert(vma->vm_start < vma->vm_end);
    list_entry_t *list = &(mm->mmap_list);
    list_entry_t *le_prev = list, *le_next;

        list_entry_t *le = list;
        while ((le = list_next(le)) != list) {
            struct vma_struct *mmap_prev = le2vma(le, list_link);
            if (mmap_prev->vm_start > vma->vm_start) {
                break;
            }
            le_prev = le;
        }
    //保证插入后所有vma_struct按照区间左端点有序排列
    le_next = list_next(le_prev);

    /* check overlap */
    if (le_prev != list) {
        check_vma_overlap(le2vma(le_prev, list_link), vma);
    }
    if (le_next != list) {
        check_vma_overlap(vma, le2vma(le_next, list_link));
    }

    vma->vm_mm = mm;
    list_add_after(le_prev, &(vma->list_link));

    mm->map_count ++;//计数器
}

// find_vma - find a vma  (vma->vm_start <= addr <= vma_vm_end)
//如果返回NULL,说明查询的虚拟地址不存在/不合法,既不对应内存里的某个页,也不对应硬盘里某个可以换进来的页
struct vma_struct *
find_vma(struct mm_struct *mm, uintptr_t addr) {
    struct vma_struct *vma = NULL;
    if (mm != NULL) {
        vma = mm->mmap_cache;
        if (!(vma != NULL && vma->vm_start <= addr && vma->vm_end > addr)) {
                bool found = 0;
                list_entry_t *list = &(mm->mmap_list), *le = list;
                while ((le = list_next(le)) != list) {
                    vma = le2vma(le, list_link);
                    if (vma->vm_start<=addr && addr < vma->vm_end) {
                        found = 1;
                        break;
                    }
                }
                if (!found) {
                    vma = NULL;
                }
        }
        if (vma != NULL) {
            mm->mmap_cache = vma;
        }
    }
    return vma;
}

考虑当发生Page Fault的时候我们怎么做。回顾异常处理程序,我们的trapFrame传递了badvaddrdo_pgfault()函数。这实际上是stval这个寄存器的数值(在旧版的RISCV标准里叫做sbadvaddr),这个寄存器存储一些关于异常的数据,对于PageFault它存储的是访问出错的虚拟地址。

// kern/trap/trap.c
static int pgfault_handler(struct trapframe *tf) {
    extern struct mm_struct *check_mm_struct;//当前使用的mm_struct的指针,在vmm.c定义
    print_pgfault(tf);
    if (check_mm_struct != NULL) {
        return do_pgfault(check_mm_struct, tf->cause, tf->badvaddr);
    }
    panic("unhandled page fault.\n");
}
// kern/mm/vmm.c
struct mm_struct *check_mm_struct;

// check_pgfault - check correctness of pgfault handler
static void
check_pgfault(void) {
    /* ...... */
    check_mm_struct = mm_create();
    /* ...... */
}

do_pgfault()函数在vmm.c定义,是页面置换机制的核心。如果可行,我们要对页表做对应的修改,加入对应的页表项,并把硬盘上的数据换进内存。这时可能要把内存里的一个页换出去。

// kern/mm/vmm.c
int do_pgfault(struct mm_struct *mm, uint_t error_code, uintptr_t addr) {
    //addr: 访问出错的虚拟地址
    int ret = -E_INVAL;
    //try to find a vma which include addr
    struct vma_struct *vma = find_vma(mm, addr);
    //我们首先要做的就是在mm_struct里判断这个虚拟地址是否可用
    pgfault_num++;
    //If the addr is not 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;
    }

    /* IF (write an existed addr ) OR
     *    (write an non_existed addr && addr is writable) OR
     *    (read  an non_existed addr && addr is readable)
     * THEN
     *    continue process
     */
    uint32_t perm = PTE_U;
    if (vma->vm_flags & VM_WRITE) {
        perm |= (PTE_R | PTE_W);
    }
    addr = ROUNDDOWN(addr, PGSIZE); //按照页面大小把地址对齐

    ret = -E_NO_MEM;

    pte_t *ptep=NULL;

    ptep = get_pte(mm->pgdir, addr, 1);  //(1) try to find a pte, if pte's
                                         //PT(Page Table) isn't existed, then
                                         //create a PT.
    if (*ptep == 0) {
        if (pgdir_alloc_page(mm->pgdir, addr, perm) == NULL) {
            cprintf("pgdir_alloc_page in do_pgfault failed\n");
            goto failed;
        }
    } else {
        /*
        * Now we think this pte is a  swap entry, we should load data from disk
        * to a page with phy addr,
        * and map the phy addr with logical addr, trigger swap manager to record
        * the access situation of this page.
        *
        *    swap_in(mm, addr, &page) : alloc a memory page, then according to
        * the swap entry in PTE for addr, find the addr of disk page, read the
        * content of disk page into this memroy page
        *     page_insert : build the map of phy addr of an Page with the virtual addr la
        *   swap_map_swappable : set the page swappable
        */
        if (swap_init_ok) {
            struct Page *page = NULL;
            //在swap_in()函数执行完之后,page保存换入的物理页面。
            //swap_in()函数里面可能把内存里原有的页面换出去
            swap_in(mm, addr, &page);  //(1)According to the mm AND addr, try
                                       //to load the content of right disk page
                                       //into the memory which page managed.
            page_insert(mm->pgdir, page, addr, perm); //更新页表,插入新的页表项
            //(2) According to the mm, addr AND page, 
            // setup the map of phy addr <---> virtual addr
            swap_map_swappable(mm, addr, page, 1);  //(3) make the page swappable.
            //标记这个页面将来是可以再换出的
            page->pra_vaddr = addr;
        } else {
            cprintf("no swap_init_ok but ptep is %x, failed\n", *ptep);
            goto failed;
        }
   }

   ret = 0;
failed:
    return ret;
}

接下来我们看看FIFO页面置换算法是怎么在ucore里实现的。

最后更新于