ucore实验三

与LAB2对比

  1. kern目录下增加fs目录,表示文件系统相同内容。
  2. makefile中新增了用dd命令创建swapping文件(bin/swap.img)作为qemu磁盘的swapping区的代码,以及qemu和debug相关命令都新增了使用swapping区功能的代码。

makefile

225 QEMUOPTS = -hda $(UCOREIMG) -drive file=$(SWAPIMG),media=disk,cache=writeback 

注意这里新增的qemu选项,主要是-drive file=$(SWAPIMG),media=disk,cache=writeback,含义如下:(通过man qemu-system-i386查询)

       -drive option[,option[,option[,...]]]
           Define a new drive. Valid options are:

           file=file
               This option defines which disk image to use with this drive. If the filename contains comma, you must double it (for instance,
               "file=my,,file" to use file "my,file").

               Special files such as iSCSI devices can be specified using protocol specific URLs. See the section for "Device URL Syntax" for more
               information.
...
           media=media
               This option defines the type of the media: disk or cdrom.
...
           cache=cache
               cache is "none", "writeback", "unsafe", "directsync" or "writethrough" and controls how the host cache is used to access block data.
...

By default, the cache=writeback mode is used. It will report data writes as completed as soon as the data is present in the host page cache. This is safe as long as your guest OS makes sure to correctly flush disk caches where needed. If your guest OS does not handle volatile disk write caches correctly and your host crashes or loses power, then the guest may experience data corruption.

从上面看,当设置为cache=writeback时,只要往缓冲里写入数据,那么数据就会被马上自动写入对应的磁盘。往缓冲里面写数据的过程应该是要依赖于OS的,即这个部分是要靠内核控制的。

虚拟内存基本原理

什么是虚拟内存?简单地说是指程序员或CPU“看到”的内存。但有几点需要注意:

  • 虚拟内存单元不一定有实际的物理内存单元对应,即实际的物理内存单元可能不存在;
  • 如果虚拟内存单元对应有实际的物理内存单元,那二者的地址一般是不相等的;
  • 通过操作系统实现的某种内存映射可建立虚拟内存与物理内存的对应关系,使得程序员或CPU访问的虚拟内存地址会自动转换为一个物理内存地址。

那么这个“虚拟”的作用或意义在哪里体现呢?在操作系统中,虚拟内存其实包含多个虚拟层次,在不同的层次体现了不同的作用。首先,在有了分页机制后,程序员或CPU“看到”的地址已经不是实际的物理地址了,这已经有一层虚拟化,我们可简称为内存地址虚拟化。有了内存地址虚拟化,我们就可以通过设置页表项来限定软件运行时的访问空间,确保软件运行不越界,完成内存访问保护的功能。

通过内存地址虚拟化,可以使得软件在没有访问某虚拟内存地址时不分配具体的物理内存,而只有在实际访问某虚拟内存地址时,操作系统再动态地分配物理内存,建立虚拟内存到物理内存的页映射关系,这种技术称为按需分页(demand paging)。把不经常访问的数据所占的内存空间临时写到硬盘上,这样可以腾出更多的空闲内存空间给经常访问的数据;当CPU访问到不经常访问的数据时,再把这些数据从硬盘读入到内存中,这种技术称为页换入换出(page swap in/out)。这种内存管理技术给了程序员更大的内存“空间”,从而可以让更多的程序在内存中并发运行。

实验执行流程概述

本次实验主要完成ucore内核对虚拟内存的管理工作。其总体设计思路还是比较简单,即首先完成初始化虚拟内存管理机制,即需要设置好哪些页需要放在物理内存中,哪些页不需要放在物理内存中,而是可被换出到硬盘上,并涉及完善建立页表映射、页访问异常处理操作等函数实现。然后就执行一组访存测试,看看我们建立的页表项是否能够正确完成虚实地址映射,是否正确描述了虚拟内存页在物理内存中还是在硬盘上,是否能够正确把虚拟内存页在物理内存和硬盘之间进行传递,是否正确实现了页面替换算法等。lab3的总体执行流程如下。

  • 首先是初始化过程。参考ucore总控函数init的代码,可以看到在调用完成虚拟内存初始化的vmm_init函数之前,需要首先调用pmm_init函数完成物理内存的管理,这也是我们lab2已经完成的内容。接着是执行中断和异常相关的初始化工作,即调用pic_init函数和idt_init函数等,这些工作与lab1的中断异常初始化工作的内容是相同的。
  • 在调用完idt_init函数之后,将进一步调用三个lab3中才有的新函数vmm_init、ide_init和swap_init。这三个函数涉及本次实验中的两个练习。第一个函数vmm_init是检查我们的练习1是否正确实现了。为了表述不在物理内存中的“合法”虚拟页,需要有数据结构来描述这样的页,为此ucore建立了mm_struct和vma_struct数据结构,假定我们已经描述好了这样的“合法”虚拟页,当ucore访问这些“合法”虚拟页时,会由于没有虚实地址映射而产生页访问异常。如果我们正确实现了练习1,则do_pgfault函数会申请一个空闲物理页,并建立好虚实映射关系,从而使得这样的“合法”虚拟页有实际的物理页帧对应。这样练习1就算完成了。
  • ide_init和swap_init是为练习2准备的。由于页面置换算法的实现存在对硬盘数据块的读写,所以ide_init就是完成对用于页换入换出的硬盘(简称swap硬盘)的初始化工作。完成ide_init函数后,ucore就可以对这个swap硬盘进行读写操作了。swap_init函数首先建立swap_manager,swap_manager是完成页面替换过程的主要功能模块,其中包含了页面置换算法的实现。然后会进一步调用执行check_swap函数在内核中分配一些页,模拟对这些页的访问,这会产生页访问异常。如果我们正确实现了练习2,就可通过do_pgfault来调用swap_map_swappable函数来查询这些页的访问情况并间接调用实现页面置换算法的相关函数,把“不常用”的页换出到磁盘上。

ucore在实现上述技术时,需要解决三个关键问题:

  1. 当程序运行中访问内存产生page fault异常时,如何判定这个引起异常的虚拟地址内存访问是越界、写只读页的“非法地址”访问还是由于数据被临时换出到磁盘上或还没有分配内存的“合法地址”访问?
  2. 何时进行请求调页/页换入换出处理?
  3. 如何在现有ucore的基础上实现页替换算法?

关键数据结构和相关函数分析

对于第一个问题的出现,在于实验二中有关内存的数据结构和相关操作都是直接针对实际存在的资源--物理内存空间的管理,没有从一般应用程序对内存的“需求”考虑,即需要有相关的数据结构和操作来体现一般应用程序对虚拟内存的“需求”。一般应用程序的对虚拟内存的“需求”与物理内存空间的“供给”没有直接的对应关系,ucore是通过page fault异常处理来间接完成这二者之间的衔接。

page_fault函数不知道哪些是“合法”的虚拟页,原因是ucore还缺少一定的数据结构来描述这种不在物理内存中的“合法”虚拟页。为此ucore通过建立mm_struct和vma_struct数据结构,描述了ucore模拟应用程序运行所需的合法内存空间。当访问内存产生page fault异常时,可获得访问的内存的方式(读或写)以及具体的虚拟内存地址,这样ucore就可以查询此地址,看是否属于vma_struct数据结构中描述的合法地址范围中,如果在,则可根据具体情况进行请求调页/页换入换出处理(这就是练习2涉及的部分);如果不在,则报错。mm_struct和vma_struct数据结构结合页表表示虚拟地址空间和物理地址空间的示意图如下所示:

在ucore中描述应用程序对虚拟内存“需求”的数据结构是vma_struct(定义在vmm.h中),以及针对vma_struct的函数操作。这里把一个vma_struct结构的变量简称为vma变量。vma_struct的定义如下:

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;
};

vm_start和vm_end描述了一个连续地址的虚拟内存空间的起始位置和结束位置,这两个值都应该是PGSIZE 对齐的,而且描述的是一个合理的地址空间范围(即严格确保 vm_start < vm_end的关系);list_link是一个双向链表,按照从小到大的顺序把一系列用vma_struct表示的虚拟内存空间链接起来,并且还要求这些链起来的vma_struct应该是不相交的,即vma之间的地址空间无交集;vm_flags表示了这个虚拟内存空间的属性,目前的属性包括:

#define VM_READ 0x00000001  //只读
#define VM_WRITE 0x00000002 //可读写
#define VM_EXEC 0x00000004  //可执行

vm_mm是一个指针,指向一个比vma_struct更高的抽象层次的数据结构mm_struct,这里把一个mm_struct结构的变量简称为mm变量。这个数据结构表示了包含所有虚拟内存空间的共同属性,具体定义如下

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;
    pde_t *pgdir; // the PDT of these vma
    int map_count; // the count of these vma
    void *sm_priv; // the private data for swap manager
};

mmap_list是双向链表头,链接了所有属于同一页目录表的虚拟内存空间,mmap_cache是指向当前正在使用的虚拟内存空间,由于操作系统执行的“局部性”原理,当前正在用到的虚拟内存空间在接下来的操作中可能还会用到,这时就不需要查链表,而是直接使用此指针就可找到下一次要用到的虚拟内存空间。由于mmap_cache 的引入,可使得 mm_struct 数据结构的查询加速 30% 以上。pgdir 所指向的就是 mm_struct数据结构所维护的页表。通过访问pgdir可以查找某虚拟地址对应的页表项是否存在以及页表项的属性等。map_count记录mmap_list 里面链接的 vma_struct的个数。sm_priv指向用来链接记录页访问情况的链表头,这建立了mm_struct和后续要讲到的swap_manager之间的联系。

涉及vma_struct的操作函数也比较简单,主要包括三个:

  1. vma_create--创建vma。vma_create函数根据输入参数vm_start、vm_end、vm_flags来创建并初始化描述一个虚拟内存空间的vma_struct结构变量。
  2. insert_vma_struct--插入一个vma。insert_vma_struct函数完成把一个vma变量按照其空间位置[vma->vm_start,vma->vm_end]从小到大的顺序插入到所属的mm变量中的mmap_list双向链表中。
  3. find_vma--查询vma。find_vma根据输入参数addr和mm变量,查找在mm变量中的mmap_list双向链表中某个vma包含此addr,即vma->vm_start<=addr end。这三个函数与后续讲到的page fault异常处理有紧密联系。

涉及mm_struct的操作函数比较简单,只有mm_create和mm_destroy两个函数,从字面意思就可以看出是是完成mm_struct结构的变量创建和删除。在mm_create中用kmalloc分配了一块空间,所以在mm_destroy中也要对应进行释放。在ucore运行过程中,会产生描述虚拟内存空间的vma_struct结构,所以在mm_destroy中也要对这些mmap_list中的vma进行释放。

Page Fault异常处理

实现虚存管理的一个关键是page fault异常处理,其过程中主要涉及到函数 -- do_pgfault的具体实现。比如,在程序的执行过程中由于某种原因(页框不存在/写只读页等)而使 CPU 无法最终访问到相应的物理内存单元,即无法完成从虚拟地址到物理地址映射时,CPU 会产生一次页访问异常,从而需要进行相应的页访问异常的中断服务例程。这个页访问异常处理的时机被操作系统充分利用来完成虚存管理,即实现“按需调页”/“页换入换出”处理的执行时机。当相关处理完成后,页访问异常服务例程会返回到产生异常的指令处重新执行,使得应用软件可以继续正常运行下去。

具体而言,当启动分页机制以后,如果一条指令或数据的虚拟地址所对应的物理页框不在内存中或者访问的类型有错误(比如写一个只读页或用户态程序访问内核态的数据等),就会发生页访问异常。产生页访问异常的原因主要有:

  1. 目标页帧不存在(页表项全为0,即该线性地址与物理地址尚未建立映射或者已经撤销);
  2. 相应的物理页帧不在内存中(页表项非空,但Present标志位=0,比如在swap分区或磁盘文件上),这在本次实验中会出现,我们将在下面介绍换页机制实现时进一步讲解如何处理;
  3. 不满足访问权限(此时页表项P标志=1,但低权限的程序试图访问高权限的地址空间,或者有程序试图写只读页面).

当出现上面情况之一,那么就会产生页面page fault(#PF)异常。CPU会把产生异常的线性地址存储在CR2中,并且把表示页访问异常类型的值(简称页访问异常错误码,errorCode)保存在中断栈中。

提示】页访问异常错误码有32位。位0为1表示对应物理页不存在;位1为1表示写异常(比如写了只读页;位2为1表示访问权限异常(比如用户态程序访问内核空间的数据)。

提示】CR2是页故障线性地址寄存器,保存最后一次出现页故障的全32位线性地址。CR2用于发生页异常时报告出错信息。当发生页异常时,处理器把引起页异常的线性地址保存在CR2中。操作系统中对应的中断服务例程可以检查CR2的内容,从而查出线性地址空间中的哪个页引起本次异常。

产生页访问异常后,CPU硬件和软件都会做一些事情来应对此事。首先页访问异常也是一种异常,所以针对一般异常的硬件处理操作是必须要做的,即CPU在当前内核栈保存当前被打断的程序现场,即依次压入当前被打断程序使用的EFLAGS,CS,EIP,errorCode;由于页访问异常的中断号是0xE,CPU把异常中断号0xE对应的中断服务例程的地址(vectors.S中的标号vector14处)加载到CS和EIP寄存器中,开始执行中断服务例程。这时ucore开始处理异常中断,首先需要保存硬件没有保存的寄存器。在vectors.S中的标号vector14处先把中断号压入内核栈,然后再在trapentry.S中的标号__alltraps处把DS、ES和其他通用寄存器都压栈。自此,被打断的程序执行现场(context)被保存在内核栈中。接下来,在trap.c的trap函数开始了中断服务例程的处理流程,大致调用关系为:

trap--> trap_dispatch-->pgfault_handler-->do_pgfault

下面需要具体分析一下do_pgfault函数。do_pgfault的调用关系如下图所示:

产生页访问异常后,CPU把引起页访问异常的线性地址装到寄存器CR2中,并给出了出错码errorCode,说明了页访问异常的类型。ucore OS会把这个值保存在struct trapframe 中tf_err成员变量中。而中断服务例程会调用页访问异常处理函数do_pgfault进行具体处理。这里的页访问异常处理是实现按需分页、页换入换出机制的关键之处。

ucore中do_pgfault函数是完成页访问异常处理的主要函数,它根据从CPU的控制寄存器CR2中获取的页访问异常的物理地址以及根据errorCode的错误类型来查找此地址是否在某个VMA的地址范围内以及是否满足正确的读写权限,如果在此范围内并且权限也正确,这认为这是一次合法访问,但没有建立虚实对应关系,所以需要分配一个空闲的内存页,并修改页表完成虚地址到物理地址的映射,刷新TLB(快表,见LAB2),然后调用iret中断,返回到产生页访问异常的指令处重新执行此指令。如果该虚地址不在某VMA范围内,则认为是一次非法访问。

关于page fault异常中断号的理解

向量号

助记符

类型

描述

来源

0

#DE

错误

除零错误

DVI和IDIV指令

1

#DB

错误/陷阱

调试异常,用于软件调试

任何代码或数据引用

2

中断

NMI中断

不可屏蔽的外部中断

3

#BP

陷阱

断点

INT 3指令

4

#OF

陷阱

溢出

INTO指令

5

#BR

错误

数组越界

BOUND指令

6

#UD

错误

无效指令(没有定义的指令)

UD2指令(奔腾Pro CPU引入此指令)或任何保留的指令

7

#NM

错误

数学协处理器不存在或不可用

浮点或WAIT/FWAIT指令

8

#DF

终止

双重错误(Double Fault)

任何可能产生异常的指令、不可屏蔽中断或可屏蔽中断

9

#MF

错误

向协处理器传送操作数时检测到页错误(Page Fault)或段不存在,486及以后集成了协处理器,本错误就保留不用了

浮点指令

10

#TS

错误

无效TSS

任务切换或访问TSS

11

#NP

错误

段不存在

加载段寄存器或访问系统段

12

#SS

错误

栈段错误

栈操作或加载SS寄存器

13

#GP

错误

通用/一般保护异常,如果一个操作违反了保护模式下的规定,而且该情况不属于其他异常,CPU就是认为是该异常

任何内存引用或保护性检查

14

#PF

错误

页错误

任何内存引用

15

保留

16

#MF

错误

浮点错误

浮点或WAIT/FWAIT指令

17

#AC

错误

对齐检查

对内存中数据的引用(486CPU引入)

18

#MC

终止

机器检查(Machine Check)

错误代码和来源与型号有关(奔腾CPU引入)

19

#XF

错误

SIMD浮点异常

SIMD浮点指令(奔腾III CPU引入)

20~31

保留

32~255

用户自定义中断

中断

可屏蔽中断

来自INTR的外部中断或INT n指令

问题:缺页中断是怎么被触发的?CPU产生页访问异常从而自动触发相应的中断,但是对应哪个中断是人为设置的还是CPU默认的?

答:从代码里设置时钟中断clock_init(void)那里可以看出,如果要设置中断,得先初始化相应的设备或者相应的中断,然后该设备才能产生中断并被写好的处理中断处理函数处理。但是,查看页访问异常的代码会发现,没有设置该中断的过程,仿佛CPU在发现页访问异常的时候有一个可以自动对应该异常的号码。原因很简单,就在于上表,[0,31]这32个中断号是CPU固定好了的,不用人为设置,只要写好对应中断号的中断处理函数即可,不用像设置时钟中断那样专门初始化一遍硬件设备,当出现对应的异常或中断时,CPU可以识别出来应该使用哪个中断号以触发什么样的处理函数。而且注意到,page fault的中断号是14,在31之内,而时钟中断的中断号是32+0。由此也就可以想明白为什么中断处理代码里,[0,31]直接使用中断号,而从32开始往后要使用IRQ_OFFSET + IRQ_xxIRQ_xx就是从0开始到“255-31”,说白了就是以32为基到255的偏移量,就是为了从可读性上区分哪些中断号是CPU固定好的,哪些是可以用户自定义的。

vmm_init()待分析

该函数主要用于构造一种使用内存的环境以检查相关练习是否正确,估计后面学习了进程管理之后就不用check_vmm()了,直接查看进程管理是否正确就能判断内存使用是否正确了。

154 // vmm_init - initialize virtual memory management
155 //          - now just call check_vmm to check correctness of vmm
156 void
157 vmm_init(void) {                                                                                                                                     
158     check_vmm();           
159 }   



161 // check_vmm - check correctness of vmm
162 static void
163 check_vmm(void) {                                                                                                                                    
164     size_t nr_free_pages_store = nr_free_pages();
165     
166     check_vma_struct();
167     check_pgfault();
168     
169     assert(nr_free_pages_store == nr_free_pages());
170     
171     cprintf("check_vmm() succeeded.\n");
172 } 

(k/m/vmm.c)

do_pgfault()

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

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

(k/m/vmm.h)

# 读取cr2寄存器中的数据
# CR2是页故障线性地址寄存器,保存最后一次出现页故障的全32位线性地址。
# CR2用于发生页异常时报告出错信息。当发生页异常时,处理器把引起页异常的线性地址保存在CR2中。
# 操作系统中对应的中断服务例程可以检查CR2的内容,从而查出线性地址空间中的哪个页引起本次异常。
184 static inline uintptr_t 
185 rcr2(void) {                                                                                                                                         
186     uintptr_t cr2;
187     asm volatile ("mov %%cr2, %0" : "=r" (cr2) :: "memory");
188     return cr2;
189 }   

(l/x86.h)

166 static int
167 pgfault_handler(struct trapframe *tf) {                                                                                                              
168     extern struct mm_struct *check_mm_struct;
169     print_pgfault(tf);
170     if (check_mm_struct != NULL) {
          	# check_mm_struct,目前没必要关心,后面讲了进程管理再关心
          	# tf->tf_err:产生中断时,由硬件压栈的errcode,页访问异常错误码有32位。
            # 位0为1表示对应物理页不存在;
            # 位1为1表示写异常(比如写了只读页);
            # 位2为1表示访问权限异常(比如用户态程序访问内核空间的数据)
          	# rcr2()读取cr2寄存器的直并将其传进去
171         return do_pgfault(check_mm_struct, tf->tf_err, rcr2());
172     }
173     panic("unhandled page fault.\n");
174 }

179 static void
180 trap_dispatch(struct trapframe *tf) {                                                                                                                
181     char c;
182 
183     int ret;
184 
185     switch (tf->tf_trapno) {
186     case T_PGFLT:  //page fault
187         if ((ret = pgfault_handler(tf)) != 0) {
188             print_trapframe(tf);            
189             panic("handle pgfault failed. %e\n", ret);
190         }
191         break;
......
234     }
235 }

(k/t/trap.c)

当CPU触发“page fault”的时候,最终进入中断处理函数do_pgfault()去解决页错误异常。

153 //alloc_pages - call pmm->alloc_pages to allocate a continuous n*PAGESIZE memory 
154 struct Page *
155 alloc_pages(size_t n) {
156     struct Page *page=NULL;
157     bool intr_flag;
158                                                                                                                                                      
159     while (1)
160     {
             # 分配页的时候要关闭中断,这个过程不能被打断
161          local_intr_save(intr_flag);
162          {
163               page = pmm_manager->alloc_pages(n);
164          }
165          local_intr_restore(intr_flag);
166 
167          if (page != NULL || n > 1 || swap_init_ok == 0) break;
168 
169          extern struct mm_struct *check_mm_struct;
170          //cprintf("page %x, call swap_out in alloc_pages %d\n",page, n);
171          swap_out(check_mm_struct, n, 0);
172     }
173     //cprintf("n %d,get page %x, No %d in alloc_pages\n",n,page,(page-pages));
174     return page;
175 }


580 // pgdir_alloc_page - call alloc_page & page_insert functions to 
581 //                  - allocate a page size memory & setup an addr map
582 //                  - pa<->la with linear address la and the PDT pgdir
583 struct Page *
584 pgdir_alloc_page(pde_t *pgdir, uintptr_t la, uint32_t perm) {                                                                                        
585     struct Page *page = alloc_page();
586     if (page != NULL) {
587         if (page_insert(pgdir, page, la, perm) != 0) {
588             free_page(page);
589             return NULL;
590         }
591         if (swap_init_ok){
592             swap_map_swappable(check_mm_struct, la, page, 0);
593             page->pra_vaddr=la;
594             assert(page_ref(page) == 1);
595             //cprintf("get No. %d  page: pra_vaddr %x, pra_link.prev %x, pra_link_next %x in pgdir_alloc_page\n", (page-pages), page->pra_vaddr,page-
596         }
597 
598     }
599 
600     return page;
601 }

(k/m/pmm.c)

swap_in()

119 int 
120 swap_in(struct mm_struct *mm, uintptr_t addr, struct Page **ptr_result)                                                                              
121 {
      	 # 首先得到一张可用的物理页帧
122      struct Page *result = alloc_page();
123      assert(result!=NULL);
124 
         # 该PTE其实是swap_entry_t
125      pte_t *ptep = get_pte(mm->pgdir, addr, 0);
126      // cprintf("SWAP: load ptep %x swap entry %d to vaddr 0x%08x, page %x, No %d\n", ptep, (*ptep)>>8, addr, result, (result-pages));
127 
128      int r;
         # 根据swap_entry_t中记录的磁盘地址,从该磁盘地址下将数据读取到内存,也就是读到刚刚
         # 申请的那一页物理页帧上。
129      if ((r = swapfs_read((*ptep), result)) != 0)
130      {
131         assert(r!=0);
132      }
133      cprintf("swap_in: load disk swap entry %d with swap_page in vadr 0x%x\n", (*ptep)>>8, addr);
134      *ptr_result=result;
135      return 0;
136 }

(k/m/swap.c)

do_pgfault()

283 /* do_pgfault - interrupt handler to process the page fault execption
284  * @mm         : the control struct for a set of vma using the same PDT
285  * @error_code : the error code recorded in trapframe->tf_err which is setted by x86 hardware
286  * @addr       : the addr which causes a memory access exception, (the contents of the CR2 register)
287  *
288  * CALL GRAPH: trap--> trap_dispatch-->pgfault_handler-->do_pgfault
289  * The processor provides ucore's do_pgfault function with two items of information to aid in diagnosing
290  * the exception and recovering from it.
291  *   (1) The contents of the CR2 register. The processor loads the CR2 register with the
292  *       32-bit linear address that generated the exception. The do_pgfault fun can
293  *       use this address to locate the corresponding page directory and page-table
294  *       entries.
         # cr2寄存器中的这个地址就是产生页访问异常的地址(线性地址),找到这个地址对应的PDE和
         # PTE,再配合error code,就可以看到底是没有物理页帧还是权限等其他问题了。
295  *   (2) An error code on the kernel stack. The error code for a page fault has a format different from
296  *       that for other exceptions. The error code tells the exception handler three things:
297  *         -- The P flag   (bit 0) indicates whether the exception was due to a not-present page (0)
298  *            or to either an access rights violation or the use of a reserved bit (1).
299  *         -- The W/R flag (bit 1) indicates whether the memory access that caused the exception
300  *            was a read (0) or write (1).
301  *         -- The U/S flag (bit 2) indicates whether the processor was executing at user mode (1)
302  *            or supervisor mode (0) at the time of the exception.
303  */
304 int 
305 do_pgfault(struct mm_struct *mm, uint32_t error_code, uintptr_t addr) {                                                                              
306     int ret = -E_INVAL;    
307     //try to find a vma which include addr
308     struct vma_struct *vma = find_vma(mm, addr);
309   
310     pgfault_num++;         
311     //If the addr is in the range of a mm's vma?
312     if (vma == NULL || vma->vm_start > addr) { 
313         cprintf("not valid addr %x, and  can not find it in vma\n", addr);
314         goto failed;
315     }
316     //check the error_code 
317     switch (error_code & 3) {
318     default:
      	# 要写,且有物理页帧
      	# 为什么这种情况也会触发缺页异常?按理说既然物理页帧存在,对于要读要写的情况,如果
        # 权限也没有问题,那么是不应该触发中断的,但是既然进了这里,说明触发了中断,且是缺页
        # 所致,只有一种可能,就是的确有对应的物理页帧,但是该页帧上的数据已经被置换到了磁盘,
        # 目前该物理页帧其实并不是给这个虚拟地址使用的,如果要使用,则必须将该物理页帧当前的
        # 数据放到磁盘,并将原本对应该虚拟地址的存储在该物理页帧的数据从磁盘取回。
        # 问题:CPU怎么知道该物理页帧上的数据不是给当前虚拟地址使用的?即这种情况下的缺页异常
        # 是怎么触发的???
        # 答:从swap_out()函数可以看出,当发生物理页帧置换的时候,和该物理页帧对应的PTE就
        # 会被转换成swap_entry_t,当访问这个虚拟地址的时候,由于该虚拟地址对应的PTE已经被转
        # 换成swap_entry_t,present位是0,所以无法正常访问,要引起页访问异常,进入中断。
319             /* error code flag : default is 3 ( W/R=1, P=1): write, present */
320     case 2: /* error code flag : (W/R=1, P=0): write, not present */
      	# 要写,但是没有对应的物理页帧
      	# (error_code & 3)是3或者是2都会进这里
321         if (!(vma->vm_flags & VM_WRITE)) {
322             cprintf("do_pgfault failed: error code flag = write AND not present, but the addr's vma cannot write\n");
323             goto failed;
324         }
325         break;
326     case 1: /* error code flag : (W/R=0, P=1): read, present */
      	# 要读,且物理页帧存在,按理说不应该触发异常进入这里,但是却进入了这里,所以这里要这么处理
327         cprintf("do_pgfault failed: error code flag = read AND present\n");
328         goto failed;
329     case 0: /* error code flag : (W/R=0, P=0): read, not present */
      	# 要读,但是没有对应的物理页帧
330         if (!(vma->vm_flags & (VM_READ | VM_EXEC))) {
331             cprintf("do_pgfault failed: error code flag = read AND not present, but the addr's vma cannot read or exec\n");
332             goto failed;
333         }
334     }
335     /* IF (write an existed addr ) OR
336      *    (write an non_existed addr && addr is writable) OR
337      *    (read  an non_existed addr && addr is readable)
338      * THEN
339      *    continue process
340      */
      	# 能走到这里,就只有三种情况:
      	# 要写,且物理页帧存在
      	# 要写,物理页帧不存在
      	# 要读,物理页帧不存在

      	# 默认是用户态访问
341     uint32_t perm = PTE_U;
      	# 表示要写
342     if (vma->vm_flags & VM_WRITE) {
343         perm |= PTE_W;
344     }
      	# addr是线性地址(ucore中也是虚拟地址),向下找到该虚拟地址对应的虚拟页帧的起始地址
345     addr = ROUNDDOWN(addr, PGSIZE);
      	# 注意,由于此时的addr已经是虚拟页帧的起始地址,意味着该地址是可以映射到物理页帧的起始地址的!
346 
      	# 修改ret的默认值,因为能走到这里说明参数肯定是没问题了
347     ret = -E_NO_MEM;
348 
      	# 准备查找相应的PTE
349     pte_t *ptep=NULL;
350     /*LAB3 EXERCISE 1: YOUR CODE
351     * Maybe you want help comment, BELOW comments can help you finish the code
352     *
353     * Some Useful MACROs and DEFINEs, you can use them in below implementation.
354     * MACROs or Functions:
355     *   get_pte : get an pte and return the kernel virtual address of this pte for la
356     *             if the PT contians this pte didn't exist, alloc a page for PT (notice the 3th parameter '1')
357     *   pgdir_alloc_page : call alloc_page & page_insert functions to allocate a page size memory & setup
358     *             an addr map pa<--->la with linear address la and the PDT pgdir
359     * DEFINES:
360     *   VM_WRITE  : If vma->vm_flags & VM_WRITE == 1/0, then the vma is writable/non writable
361     *   PTE_W           0x002                   // page table/directory entry flags bit : Writeable
362     *   PTE_U           0x004                   // page table/directory entry flags bit : User can access
363     * VARIABLES:
364     *   mm->pgdir : the PDT of these vma
365     *
366     */
367 #if 0
368     /*LAB3 EXERCISE 1: YOUR CODE*/
369     ptep = ???              //(1) try to find a pte, if pte's PT(Page Table) isn't existed, then create a PT.
370     if (*ptep == 0) {
371                             //(2) if the phy addr isn't exist, then alloc a page & map the phy addr with logical addr
372 
373     }
374     else {
375     /*LAB3 EXERCISE 2: YOUR CODE
376     * Now we think this pte is a  swap entry, we should load data from disk to a page with phy addr,
    	  # 对应(write an existed addr )这种情况,要读要写,且error code显示物理页帧存在
        # (但此时的PTE其实是swap_entry_t,从swap_out()可以看出最低位是0)
        # 但是该虚拟地址对应的这个物理页帧的数据之前已经被置换到了磁盘,该虚拟地址已经不再有对
        # 应的物理页帧了,只是通过swap_entry_t记录了它的数据被放在了磁盘的哪个地方,所以要
        # 再通过swap_entry_t找到数据在磁盘中的位置,将数据从磁盘置换回来,当然,还得再给这个
        # 虚拟地址分配一个物理页帧,用来放置从磁盘读回来的数据。
377     * and map the phy addr with logical addr, trigger swap manager to record the access situation of this page.
        # 还得重新建立映射。
378     *
379     *  Some Useful MACROs and DEFINEs, you can use them in below implementation.
380     *  MACROs or Functions:
381     *    swap_in(mm, addr, &page) : alloc a memory page, then according to the swap entry in PTE for addr,
382     *                               find the addr of disk page, read the content of disk page into this memroy page
383     *    page_insert : build the map of phy addr of an Page with the linear addr la
384     *    swap_map_swappable : set the page swappable
385     */
386         if(swap_init_ok) {
387             struct Page *page=NULL;
388                                     //(1)According to the mm AND addr, try to load the content of right disk page
389                                     //    into the memory which page managed.
390                                     //(2) According to the mm, addr AND page, setup the map of phy addr <---> logical addr
391                                     //(3) make the page swappable.
392         }
393         else {
394             cprintf("no swap_init_ok but ptep is %x, failed\n",*ptep);
395             goto failed;
396         }
397    }
398 #endif
399     //(1) try to find a pte, if pte's PT(Page Table) isn't existed, then create a PT.
400     ptep = get_pte(mm->pgdir, addr, 1);
401     if (ptep == NULL) {
402         cprintf(">>>>WQ DETECT: get_pte in do_pgfault failde.\n");
403         goto failed;
404     }
405     
406     if (0 == *ptep) {
407         //(2) if the phy addr isn't exist, then alloc a page & map the phy addr with logical addr
408         if (pgdir_alloc_page(mm->pgdir, addr, perm) == NULL) {
409             cprintf("pgdir_alloc_page in do_pgfault failed.\n");
410             goto failed;
411         }
412     }
413     else {
            # ”PTE的高24位不为0,而最低位为0“,这是swap_entry_t的特征!
414         if(swap_init_ok) {
415             struct Page *page=NULL;
416             //(1) According to the mm AND addr, try to load the content of right disk page
417             //    into the memory which page managed.
418             ret = swap_in(mm, addr, &page);
419             if (ret != 0) {
420                 cprintf("swap_in in do_pgfault failed.\n");
421                 goto failed;                                                                                                                         
422             }
423     
424             //(2) According to the mm, addr AND page, setup the map of phy addr <---> logical addr
                # 这里是我专门做的,答案没有,将最后一位置零,也就是将present位置零
                # 因为当present位是1的时候,page_insert()里面会根据PTE改变对应的page的ref,
                # 但是此时的PTE其实是swap_entry_t,不是真正的pte,据此获得的page
                # 其实是不知道谁的page,如果还要操作该page的ref,那就更不对了!!!
                # swap_entry_t从原理上看,也是没有实际对应的page的,所以不能让page_insert()
                # 去操作里面的page!!!解决方法也简单,将present位置零即可。
                # 这里用不着这样,从swap_out()可以看出,present位本身就是0!!!所以
                # CPU访问到swap_entry_t的时候才会触发页访问异常。
425             //*ptep = *ptep & 0xfffffffe;
426             ret = page_insert(mm->pgdir, page, addr, perm);
427             if (ret != 0) {
428                 goto failed;
429             }
430 
431             //(3) make the page swappable.
432             ret = swap_map_swappable(mm, addr, page, 0);
                # swap_out()那里要刷新tlb,该过程需要使用到pra_vaddr,
                # 所以这里必须给它赋值!!!
433             page->pra_vaddr = addr;
434             goto failed;
435         }
436         else {
437             cprintf("no swap_init_ok but ptep is %x, failed\n", *ptep);
438             goto failed;
439         }
440 
441     }
442 
443     ret = 0;
444 failed:
445     return ret;
446 }

(k/m/vmm.c)

swap_init()

 30 int
 31 swap_init(void)                                                                                                                                      
 32 {
         # 文件系统相关,待分析
 33      swapfs_init();
 34   
 35      if (!(1024 <= max_swap_offset && max_swap_offset < MAX_SWAP_OFFSET_LIMIT))
 36      {
 37           panic("bad max_swap_offset %08x.\n", max_swap_offset);
 38      }
 39       
 40   
         # 重点!!!设置相关函数
 41      sm = &swap_manager_fifo;
      	 # 其实这里面啥也没干
 42      int r = sm->init();   
 43       
 44      if (r == 0)
 45      {
 46           swap_init_ok = 1;
 47           cprintf("SWAP: manager = %s\n", sm->name);
            	# 检查算法是否正确
 48           check_swap();    
 49      }
 50   
 51      return r;
 52 }

(k/m/swap.c)

置换相关函数

133 struct swap_manager swap_manager_fifo =
134 {
135      .name            = "fifo swap manager",
136      .init            = &_fifo_init,
137      .init_mm         = &_fifo_init_mm,
138      .tick_event      = &_fifo_tick_event,
139      .map_swappable   = &_fifo_map_swappable,
140      .set_unswappable = &_fifo_set_unswappable,
141      .swap_out_victim = &_fifo_swap_out_victim,                                                                                                      
142      .check_swap      = &_fifo_check_swap,
143 };  

(k/m/swap_fifo.c)

_fifo_map_swappable()

调用关系:

do_pgfault()-->pgdir_alloc_page()-->swap_map_swappable()-->"sm->map_swappable()",

最初是由中断处理函数调用的。

580 // pgdir_alloc_page - call alloc_page & page_insert functions to 
581 //                  - allocate a page size memory & setup an addr map
582 //                  - pa<->la with linear address la and the PDT pgdir
583 struct Page *
584 pgdir_alloc_page(pde_t *pgdir, uintptr_t la, uint32_t perm) {
585     struct Page *page = alloc_page();
586     if (page != NULL) {
587         if (page_insert(pgdir, page, la, perm) != 0) {
588             free_page(page);
589             return NULL;
590         }
591         if (swap_init_ok){
								# la是产生页访问异常的虚拟地址,page是新申请的,
592             swap_map_swappable(check_mm_struct, la, page, 0);                                                                                        
593             page->pra_vaddr=la;
594             assert(page_ref(page) == 1);    
595             //cprintf("get No. %d  page: pra_vaddr %x, pra_link.prev %x, pra_link_next %x in pgdir_alloc_page\n", (page-pages), page->pra_vaddr,page-
596         }
597 
598     }
599 
600     return page;
601 } 

(k/m/pmm.c)

 66 int
 67 swap_map_swappable(struct mm_struct *mm, uintptr_t addr, struct Page *page, int swap_in)
 68 {           
 69      return sm->map_swappable(mm, addr, page, swap_in);
 70 } 

(k/m/swap.c)

 41 /*
 42  * (3)_fifo_map_swappable: According FIFO PRA, we should link the most recent arrival page at the back of pra_list_head qeueue
 43  */
		# swap_map_swappable(check_mm_struct, la, page, 0)
		# 注意参数是对应的,la是产生页访问异常的虚拟地址,page是新申请的
 44 static int
 45 _fifo_map_swappable(struct mm_struct *mm, uintptr_t addr, struct Page *page, int swap_in)
 46 {
 47     list_entry_t *head=(list_entry_t*) mm->sm_priv;
 48     list_entry_t *entry=&(page->pra_page_link);
 49 
 50     assert(entry != NULL && head != NULL);
 51     //record the page access situlation
 52     /*LAB3 EXERCISE 2: YOUR CODE*/
 53     //(1)link the most recent arrival page at the back of the pra_list_head qeueue.
        # 从整个代码来看,只有当空闲物理页帧全部耗尽的情况下,才会执行置换动作。
      	# 只要不执行置换动作,这里就只将新申请的page入队就好,当执行置换动作的时候,
        # 再由_fifo_swap_out_victim()去执行出队动作即可。
        # 与答案的思路不同,我把head next视为队列头,head prev视为队列尾,所以入队的时候
        # 往队尾入,就用list_add_before()
 54     list_add_before(head, entry);
 55 
 56     return 0;
 57 }

(k/m/swap_fifo.c)

_fifo_swap_out_victim()

调用关系:

do_pgfault()-->pgdir_alloc_page()-->alloc_page()-->swap_out()-->"sm->swap_out_victim()",

最初是由中断处理函数调用的。其实该函数只做一件事,就是让物理页帧出队。

  9 /* *
 10  * swap_entry_t
 11  * --------------------------------------------
 12  * |         offset        |   reserved   | 0 |
 13  * --------------------------------------------
 14  *           24 bits            7 bits    1 bit
 15  * */

(k/m/swap.h)

 58 /*
 59  *  (4)_fifo_swap_out_victim: According FIFO PRA, we should unlink the  earliest arrival page in front of pra_list_head qeueue,
 60  *                            then assign the value of *ptr_page to the addr of this page.
 61  */
 62 static int
 63 _fifo_swap_out_victim(struct mm_struct *mm, struct Page ** ptr_page, int in_tick)
 64 {
 65     list_entry_t *head=(list_entry_t*) mm->sm_priv;
 66     assert(head != NULL);                                                                                                                            
 67     assert(in_tick==0);
 68     /* Select the victim */
 69     /*LAB3 EXERCISE 2: YOUR CODE*/
 70     //(1)  unlink the  earliest arrival page in front of pra_list_head qeueue
      	# 与答案的思路不同,我把head next视为队列头,head prev视为队列尾
 71     list_entry_t *le = list_next(head);
 72     if (le == head) {
 73         panic(">>>>WQ DETECT: Wrong mm, there are not any memory for this mm(0x%p)\n", mm);
 74     }
 75     list_del(le);
 76 
 77     //(2)  assign the value of *ptr_page to the addr of this page
 78     *ptr_page = le2page(le, pra_page_link);
 79 
 80     return 0;
 81 }

(k/m/swap_fifo.c)

swap_out()

 80 int
 81 swap_out(struct mm_struct *mm, int n, int in_tick)                                                                                                   
 82 {
 83      int i;
 84      for (i = 0; i != n; ++ i)
 85      {
 86           uintptr_t v;
 87           //struct Page **ptr_page=NULL;  
 88           struct Page *page;
 89           // cprintf("i %d, SWAP: call swap_out_victim\n",i);
        	    # 注意这里的page取了二级指针,说明sm->swap_out_victim()里面
              # 是要修改它的指向的,就是让page指向被置换掉的页
 90           int r = sm->swap_out_victim(mm, &page, in_tick);
 91           if (r != 0) {
 92                     cprintf("i %d, swap_out: call swap_out_victim failed\n",i);
 93                   break;
 94           }
 95           //assert(!PageReserved(page));  
 96 
 97           //cprintf("SWAP: choose victim page 0x%08x\n", page);
 98           
 99           v=page->pra_vaddr;
100           pte_t *ptep = get_pte(mm->pgdir, v, 0);
101           assert((*ptep & PTE_P) != 0);   
102 
          	  # 把要置换出去的那一页的数据写到磁盘上
103           if (swapfs_write( (page->pra_vaddr/PGSIZE+1)<<8, page) != 0) {
104                     cprintf("SWAP: failed to save\n");
                      	# 如果写磁盘失败,则将该页重新入队(虚拟内存管理的那个队)
105                     sm->map_swappable(mm, v, page, 0);
106                     continue;
107           }
108           else {
                      	# 能进入这里说明写磁盘成功,注意此时page已经出队
109                     cprintf("swap_out: i %d, store page in vaddr 0x%x to disk swap entry %d\n", i, v, page->pra_vaddr/PGSIZE+1);

   /* *
   * swap_entry_t
   * --------------------------------------------
   * |         offset        |   reserved   | 0 |
   * --------------------------------------------
   *           24 bits            7 bits    1 bit
   * */
                        # 当发生置换的时候,该PTE对应的物理页帧上的数据被写到了磁盘上,
                        # 然后将该PTE转换成swap_entry_t!!!从而当再次访问该虚拟地址
                        # 的时候,由于PTE其实是swap_entry_t,导致根本找不到对应的物理
                        # 页帧,但是从最后一位是1判断其物理页帧是存在的,所以会触发页访问
                        # 异常……注意,将PTE设置成swap_entry_t后,也就意味着断开了PTE到
                        # 物理页帧的映射,该虚拟地址不再有实际对应的物理页帧了。
                        # swap_entry_t里面记录了磁盘中存放该虚拟地址对应的物理页帧上的数据
                        # 在磁盘上的地址,以便后面调用swap_in()的时候把数据找回来。
                        # 注意这个移位操作,低8位全部都是0,意味着此时的present位也是0!
110                     *ptep = (page->pra_vaddr/PGSIZE+1)<<8;
                      	# 将该页加入空闲物理页帧链表
111                     free_page(page);
112           }
113           
              # 由于修改了PTE,所以要刷新TLB表项
114           tlb_invalidate(mm->pgdir, v);   
115      }
116      return i;
117 }

(k/m/swap.c)

alloc_pages()

153 //alloc_pages - call pmm->alloc_pages to allocate a continuous n*PAGESIZE memory 
154 struct Page *
155 alloc_pages(size_t n) {
156     struct Page *page=NULL;
157     bool intr_flag;
158    
159     while (1)
160     {
161          local_intr_save(intr_flag);     
162          {
163               page = pmm_manager->alloc_pages(n);
164          }
165          local_intr_restore(intr_flag);  
166 
             # page == NULL && n <= 1 && swap_init_ok == 1时,
          	 # 才会执行下面的swap_out(),这说明ucore只有当全部的空闲物理页帧
             # 都用完了之后,才会执行将内存中的数据置换到磁盘的操作。这应该也是
             # 当前各个操作系统统一的处理方式,不到物理内存耗尽的情况下绝对不置换。
167          if (page != NULL || n > 1 || swap_init_ok == 0) break;
168          
169          extern struct mm_struct *check_mm_struct;
170          //cprintf("page %x, call swap_out in alloc_pages %d\n",page, n);
171          swap_out(check_mm_struct, n, 0);                                                                                                            
172     }
173     //cprintf("n %d,get page %x, No %d in alloc_pages\n",n,page,(page-pages));
174     return page;
175 }

580 // pgdir_alloc_page - call alloc_page & page_insert functions to 
581 //                  - allocate a page size memory & setup an addr map
582 //                  - pa<->la with linear address la and the PDT pgdir
583 struct Page *
584 pgdir_alloc_page(pde_t *pgdir, uintptr_t la, uint32_t perm) {
      	# 分配page的时候,如果还有空闲物理页帧,则不会触发置换操作;
    	  # 否则这里获取的page其实就是通过置换操作之后获取的最新的物理页帧。
585     struct Page *page = alloc_page();                                                                                                                
586     if (page != NULL) {
587         if (page_insert(pgdir, page, la, perm) != 0) {
588             free_page(page);
589             return NULL;
590         }
591         if (swap_init_ok){
592             swap_map_swappable(check_mm_struct, la, page, 0);
593             page->pra_vaddr=la;
594             assert(page_ref(page) == 1);
595             //cprintf("get No. %d  page: pra_vaddr %x, pra_link.prev %x, pra_link_next %x in pgdir_alloc_page\n", (page-pages), page->pra_vaddr,page-
596         }
597 
598     }
599     
600     return page;
601 }   

(k/m/pmm.c)

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值