虚拟内存及其数据结构
首先一点就是我们现在讲的虚拟内存都是采取段页式管理的
关于虚拟内存的知识可以看我写的blog(malloc & free的实现(malloc lab) - 掘金 (juejin.cn))看到动态存储器分配之前就行和这篇blog(xuan-insr.github.io/%E6%A0%B8%E…)
总体上说就是运用抽象来做到程序享受所有的存储器,好像整个电脑中只有我们写的程序(加一个os)。具体点就是你在google上搜memory layout,就会搜出来这样一幅图
虚拟内存我的理解就是这整个存储器映像作为程序的存储空间。
并且存储器映像每一个段有了一个新名字叫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在计算机中表示就是下面这张图
图片来源于 C/C++ 中程序内存区域划分((56条消息) C/C++ 中程序内存区域划分Baoming ROSE的博客-CSDN博客c++内存区域划分)
图片 来源于csapp

图片是 我在ucore 指导书上截取的
我们现在只是有了映射关系,接下来执行a.out代码
(突然想到一个问题什么是代码开始的地方?或者说main函数是不是程序开始的地方?我觉得肯定不是,因为main函数也是被别的函数调用的。我应该会写一篇关于link的blog来解释的)
pagefault
执行时肯定会出错,因为现在关于a.out的文件都在磁盘上。不过出错在那一步?
出错在虚拟页到物理页的转换中,就是在mmu进行地址翻译的时候出错
不过巧就巧在这个出错上了,没错这个出错就是所谓的pagefault,就是内存中没有这一页。那我们怎么知道内存没有这一页呢,这意味着我们需要一个表来记录,没错正是页表。(不过页表不是因为虚拟内存才出现的,如果没有开启分页,那这个表就不是页表,但它还是存在的,因为它本质是记录 程序内使用地址和物理地址的映射关系的。如果采用分段来管理虚拟内存,那这个表就是段表)
并且我们的映射关系也是记录在页表上的。所以根据映射关系和是否在内存两项,把页表项分成了三种。(可能有人要问为什么是三,不是2^2种,因为如果没有映射自然就谈不上是否在内存了)
第一种:全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中,段错误便出现了。
一种是页表全零,代表没有映射
一种是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中虚拟地址所在的pte
if(*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;
}
结果&总结
结果:

总结:
这次实验将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