精华内容
下载资源
问答
  • 在解释第一个问题之前,先说明一下计算机内存管理的中的四个名词:虚拟内存,虚拟内存地址,物理内存物理内存地址。先说说为什么会有虚拟内存和物理内存的区别。正在运行的一个进程,他所需的内存是有可能大于内存...
    在解释第一个问题之前,先说明一下计算机内存管理的中的四个名词:虚拟内存,虚拟内存地址,物理内存,物理内存地址。

    先说说为什么会有虚拟内存和物理内存的区别。正在运行的一个进程,他所需的内存是有可能大于内存条容量之和的,比如你的内存条是256M,你的程序却要创建一个2G的数据区,那么不是所有数据都能一起加载到内存(物理内存)中,势必有一部分数据要放到其他介质中(比如硬盘),待进程需要访问那部分数据时,在通过调度进入物理内存。所以,虚拟内存是进程运行时所有内存空间的总和,并且可能有一部分不在物理内存中,而物理内存就是我们平时所了解的内存条。有的地方呢,也叫这个虚拟内存为内存交换区。

    那么,什么是虚拟内存地址和物理内存地址呢。假设你的计算机是32位,那么它的地址总线是32位的,也就是它可以寻址0~0xFFFFFFFF(4G)的地址空间,但如果你的计算机只有256M的物理内存0x~0x0FFFFFFF(256M),同时你的进程产生了一个不在这256M地址空间中的地址,那么计算机该如何处理呢?回答这个问题前,先说明计算机的内存分页机制。

    计算机会对虚拟内存地址空间(32位为4G)分页产生页(page),对物理内存地址空间(假设256M)分页产生页帧(page frame),这个页和页帧的大小是一样大的,所以呢,在这里,虚拟内存页的个数势必要大于物理内存页帧的个数。在计算机上有一个页表(page table),就是映射虚拟内存页到物理内存页的,更确切的说是页号到页帧号的映射,而且是一对一的映射。但是问题来了,虚拟内存页的个数 > 物理内存页帧的个数,岂不是有些虚拟内存页的地址永远没有对应的物理内存地址空间?不是的,操作系统是这样处理的。操作系统有个页面失效(page fault)功能。操作系统找到一个最少使用的页帧,让他失效,并把它写入磁盘,随后把需要访问的页放到页帧中,并修改页表中的映射,这样就保证所有的页都有被调度的可能了。这就是处理虚拟内存地址到物理内存的步骤。

    现在来回答什么是虚拟内存地址和物理内存地址。虚拟内存地址由页号(与页表中的页号关联)和偏移量组成。页号就不必解释了,上面已经说了,页号对应的映射到一个页帧。那么,说说偏移量。偏移量就是我上面说的页(或者页帧)的大小,即这个页(或者页帧)到底能存多少数据。举个例子,有一个虚拟地址它的页号是4,偏移量是20,那么他的寻址过程是这样的:首先到页表中找到页号4对应的页帧号(比如为8),如果页不在内存中,则用失效机制调入页,否则把页帧号和偏移量传给MMC(CPU的内存管理单元)组成一个物理上真正存在的地址,接着就是访问物理内存中的数据了。总结起来说,虚拟内存地址的大小是与地址总线位数相关,物理内存地址的大小跟物理内存条的容量相关。

    转自:https://blog.csdn.net/fukaibo121/article/details/75105848
    展开全文
  • 虚拟内存与物理内存的联系与区别

    万次阅读 多人点赞 2018-08-30 11:55:36
    操作系统有虚拟内存与物理内存的概念。在很久以前,还没有虚拟内存概念的时候,程序寻址用的都是物理地址。程序能寻址的范围是有限的,这取决于CPU的地址线条数。比如在32位平台下,寻址的范围是2^32也就是4G。并且...

    操作系统有虚拟内存与物理内存的概念。在很久以前,还没有虚拟内存概念的时候,程序寻址用的都是物理地址。程序能寻址的范围是有限的,这取决于CPU的地址线条数。比如在32位平台下,寻址的范围是2^32也就是4G。并且这是固定的,如果没有虚拟内存,且每次开启一个进程都给4G的物理内存,就可能会出现很多问题:

    • 因为我的物理内存时有限的,当有多个进程要执行的时候,都要给4G内存,很显然你内存小一点,这很快就分配完了,于是没有得到分配资源的进程就只能等待。当一个进程执行完了以后,再将等待的进程装入内存。这种频繁的装入内存的操作是很没效率的
    • 由于指令都是直接访问物理内存的,那么我这个进程就可以修改其他进程的数据,甚至会修改内核地址空间的数据,这是我们不想看到的
    • 因为内存时随机分配的,所以程序运行的地址也是不正确的。

    于是针对上面会出现的各种问题,虚拟内存就出来了。

    在之前一篇文章中进程分配资源介绍过一个进程运行时都会得到4G的虚拟内存。这个虚拟内存你可以认为,每个进程都认为自己拥有4G的空间,这只是每个进程认为的,但是实际上,在虚拟内存对应的物理内存上,可能只对应的一点点的物理内存,实际用了多少内存,就会对应多少物理内存。

    进程得到的这4G虚拟内存是一个连续的地址空间(这也只是进程认为),而实际上,它通常是被分隔成多个物理内存碎片,还有一部分存储在外部磁盘存储器上,在需要时进行数据交换。

    进程开始要访问一个地址,它可能会经历下面的过程

    1. 每次我要访问地址空间上的某一个地址,都需要把地址翻译为实际物理内存地址
    2. 所有进程共享这整一块物理内存,每个进程只把自己目前需要的虚拟地址空间映射到物理内存上
    3. 进程需要知道哪些地址空间上的数据在物理内存上,哪些不在(可能这部分存储在磁盘上),还有在物理内存上的哪里,这就需要通过页表来记录
    4. 页表的每一个表项分两部分,第一部分记录此页是否在物理内存上,第二部分记录物理内存页的地址(如果在的话)
    5. 当进程访问某个虚拟地址的时候,就会先去看页表,如果发现对应的数据不在物理内存上,就会发生缺页异常
    6. 缺页异常的处理过程,操作系统立即阻塞该进程,并将硬盘里对应的页换入内存,然后使该进程就绪,如果内存已经满了,没有空地方了,那就找一个页覆盖,至于具体覆盖的哪个页,就需要看操作系统的页面置换算法是怎么设计的了。

     

    关于虚拟内存与物理内存的联系,下面这张图可以帮助我们巩固。

    页表的工作原理如下图

    1. 我们的cpu想访问虚拟地址所在的虚拟页(VP3),根据页表,找出页表中第三条的值.判断有效位。 如果有效位为1,DRMA缓存命中,根据物理页号,找到物理页当中的内容,返回
    2. 若有效位为0,参数缺页异常,调用内核缺页异常处理程序。内核通过页面置换算法选择一个页面作为被覆盖的页面,将该页的内容刷新到磁盘空间当中。然后把VP3映射的磁盘文件缓存到该物理页上面。然后页表中第三条,有效位变成1,第二部分存储上了可以对应物理内存页的地址的内容。
    3. 缺页异常处理完毕后,返回中断前的指令,重新执行,此时缓存命中,执行1。
    4. 将找到的内容映射到告诉缓存当中,CPU从告诉缓存中获取该值,结束。

     

    再来总结一下虚拟内存是怎么工作的

    当每个进程创建的时候,内核会为进程分配4G的虚拟内存,当进程还没有开始运行时,这只是一个内存布局。实际上并不立即就把虚拟内存对应位置的程序数据和代码(比如.text .data段)拷贝到物理内存中,只是建立好虚拟内存和磁盘文件之间的映射就好(叫做存储器映射)。这个时候数据和代码还是在磁盘上的。当运行到对应的程序时,进程去寻找页表,发现页表中地址没有存放在物理内存上,而是在磁盘上,于是发生缺页异常,于是将磁盘上的数据拷贝到物理内存中。

    另外在进程运行过程中,要通过malloc来动态分配内存时,也只是分配了虚拟内存,即为这块虚拟内存对应的页表项做相应设置,当进程真正访问到此数据时,才引发缺页异常。

    可以认为虚拟空间都被映射到了磁盘空间中(事实上也是按需要映射到磁盘空间上,通过mmap,mmap是用来建立虚拟空间和磁盘空间的映射关系的)

     

    利用虚拟内存机制的优点 

    1. 既然每个进程的内存空间都是一致而且固定的(32位平台下都是4G),所以链接器在链接可执行文件时,可以设定内存地址,而不用去管这些数据最终实际内存地址,这交给内核来完成映射关系
    2. 当不同的进程使用同一段代码时,比如库文件的代码,在物理内存中可以只存储一份这样的代码,不同进程只要将自己的虚拟内存映射过去就好了,这样可以节省物理内存
    3. 在程序需要分配连续空间的时候,只需要在虚拟内存分配连续空间,而不需要物理内存时连续的,实际上,往往物理内存都是断断续续的内存碎片。这样就可以有效地利用我们的物理内存

     

    展开全文
  • 物理内存和虚拟内存

    千次阅读 2019-07-30 11:17:06
    物理内存:真实的硬件设备(内存条) 虚拟内存:利用磁盘空间虚拟出的一块逻辑内存,用作虚拟内存的磁盘空间被称为交换空间(Swap Space)。(为了满足物理内存的不足而提出的策略) 在很久以前,还没有虚拟内存...

    1.概念

    物理内存:真实的硬件设备(内存条)
    虚拟内存:利用磁盘空间虚拟出的一块逻辑内存,用作虚拟内存的磁盘空间被称为交换空间(Swap Space)。(为了满足物理内存的不足而提出的策略)

    在很久以前,还没有虚拟内存概念的时候,程序寻址用的都是物理地址,取决于CPU的地址线条数,32位平台的话 2^32也就是4G 。且每次开启一个进程都给4G的物理内存。很显然你内存小一点,这很快就分配完了,于是没有得到分配资源的进程就只能等待。

    而后引入了虚拟内存极大解决了这方面的问题,一个进程运行时都会得到4G的虚拟内存。这个虚拟内存你可以认为,每个进程都认为自己拥有4G的空间,这只是每个进程认为的,但是实际上,在虚拟内存对应的物理内存上,可能只对应的一点点的物理内存,实际用了多少内存,就会对应多少物理内存。

    2.使用过程

    内核会将暂时不用的内存块信息写到交换空间,这样以来,物理内存得到了释放,这块内存就可以用于其它目的,当需要用到原始的内容时,这些信息会被重新从交换空间读入物理内存。

    linux的内存管理采取的是分页存取机制,Linux系统会不时的进行页面交换操作,以保持尽可能多的空闲物理内存,即使并没有什么事情需要内存,Linux也会交换出暂时不用的内存页面。这可以避免等待交换所需的时间。而liunx 常用的交换算法,根据”最近最经常使用“算法,会将一些不经常使用的页面文件交换到虚拟内存。

    有时我们会看到这么一个现象:linux物理内存还有很多,但是交换空间也使用了很多。其实,这并不奇怪,例如,一个占用很大内存的进程运行时,需要耗费很多内存资源,此时就会有一些不常用页面文件被交换到虚拟内存中,但后来这个占用很多内存资源的进程结束并释放了很多内存时,刚才被交换出去的页面文件并不会自动的交换进物理内存,除非有这个必要,那么此刻系统物理内存就会空闲很多,同时交换空间也在被使用,这是正常现象。

    进程访问一个内存时过程:

    • 进程还没开始运行时, 磁盘中程序的文件和虚拟内存先建立好映射,此时数据还在磁盘中。并没有拷贝到物理内存
    • 当运行对应程序代码段的时候,之前虚拟内存建立的映射中,会有张页表项,记录实际的磁盘地址,和是否已经加载到物理内存中。
    • 当发现数据未加载到物理内存时,发生缺页异常,对应的缺页中断响应函数会将磁盘上的数据拷贝到物理内存中。
    • 而当对应的数据已经加载过时,会直接从物理内存获取。同时要是已经拷贝到物理内存的数据好久没用,liunx又会将不经常使用的页面文件交换到虚拟内存

    虚拟内存与物理内存的联系
    虚拟内存与物理内存

    3.页表的工作原理如下图

    页表的工作原理

    • 我们的cpu想访问虚拟地址所在的虚拟页(VP3),根据页表,找出页表中第三条的值.判断有效位。 如果有效位为1,DRMA缓存命中,根据物理页号,找到物理页当中的内容,返回。
    • 若有效位为0,参数缺页异常,调用内核缺页异常处理程序。内核通过页面置换算法选择一个页面作为被覆盖的页面,将该页的内容刷新到磁盘空间当中。然后把VP3映射的磁盘文件缓存到该物理页上面。然后页表中第三条,有效位变成1,第二部分存储上了可以对应物理内存页的地址的内容。
    • 缺页异常处理完毕后,返回中断前的指令,重新执行,此时缓存命中,执行1。
    • 将找到的内容映射到告诉的缓存当中,CPU从告诉的缓存中获取该值,结束。

    总结

    • 物理内存是所有进程都可以访问的一块内存区域,物理内存又非常有限,因此liunx为了充分利用物理内存,只有当程序第一次执行到这块代码段时,根据分页表,缺页中断处理将数据拷贝到物理内存。
    • 当不同的进程使用同一段代码时,比如库文件的代码,在物理内存中可以只存储一份这样的代码,不同进程只要将自己的虚拟内存映射过去就好了,这样可以节省物理内存

    本文部分转自该楼主虚拟内存与物理内存的联系与区别 的博客,有需要可参考

    展开全文
  • lab2 系统内存的探测 参考博客 主要涉及操作系统的物理内存管理。 ...操作系统为了使用内存,还需高效地管理内存资源。...这里我们会了解并且自己动手完成一个...2. 然后了解如何建立对物理内存的初步管理,即了解连...

    lab2 系统内存的探测

    参考博客

    主要涉及操作系统的物理内存管理。
    操作系统为了使用内存,还需高效地管理内存资源。
    这里我们会了解并且自己动手完成一个简单的物理内存管理系统。
    

    实验目的

    理解基于段页式内存地址的转换机制
    理解页表的建立和使用方法
    理解物理内存的管理方法
    

    实验内容

    1. 首先了解如何发现系统中的物理内存;
    2. 然后了解如何建立对物理内存的初步管理,即了解连续物理内存管理;
    3. 最后了解页表相关的操作,即如何建立页表来实现虚拟内存到物理内存之间的映射,
       对段页式内存管理机制有一个比较全面的了解。
    本实验里面实现的内存管理还是非常基本的,
    并没有涉及到对实际机器的优化,
    比如针对 cache(缓冲区)的优化等。
    如果大家有余力,尝试完成扩展练习。
    

    程序执行顺序

    1. boot/bootasm.S  | bootasm.asm(修改了名字,以便于彩色显示)
     a. 开启A20   16位地址线 实现 20位地址访问  芯片版本兼容
        通过写 键盘控制器8042  的 64h端口 与 60h端口。
         
     ab.物理内存探测  通过 BIOS 中断获取内存布局 ==========比lab1多的部分===========
     
     b. 加载GDT全局描述符 lgdt gdtdesc
     c. 使能和进入保护模式 置位 cr0寄存器的 PE位 (内存分段访问) PE+PG(分页机制)
        movl %cr0, %eax 
        orl $CR0_PE_ON, %eax  或操作,置位 PE位 
        movl %eax, %cr0
     d. 调用载入系统的函数 call bootmain  # 转而调用 bootmain.c 
    
    2. boot/bootmain.c -> bootmain 函数
     a. 调用readseg函数从ELFHDR处读取8个扇区大小的 os 数据。
     b. 将输入读入 到 内存中以 进程(程序)块 proghdr 的方式存储
     c. 跳到ucore操作系统在内存中的入口位置(kern/init.c中的kern_init函数的起始地址)
    
    3. kern/init.c
     a. 初始化终端 cons_init(); init the console   kernel/driver/consore.c
         显示器初始化       cga_init();    
         串口初始化         serial_init(); 
         keyboard键盘初始化 kbd_init();
    
     b. 打印内核信息 & 欢迎信息 
        print_kerninfo();          //  内核信息  kernel/debug/kdebug.c
        cprintf("%s\n\n", message);// 欢迎信息 const char *message = “qwert”
    
     c. 显示堆栈中的多层函数调用关系 切换到保护模式,启用分段机制
        grade_backtrace();
    
     d. 初始化物理内存管理
        pmm_init();        // init physical memory management   kernel/mm/ppm.c
        --->gdt_init();    // 初始化默认的全局描述符表
        ---> page_init();// 内存管理等函数  ===============比lab1多的部分=================
    
     e. 初始化中断控制器,
        pic_init();        // 初始化 8259A 中断控制器   kernel/driver/picirq.c
    
     f. 设置中断描述符表
        idt_init();        // kernel/trap/trap.c 
        // __vectors[] 来对应中断描述符表中的256个中断符  tools/vector.c中
    
     g. 初始化时钟中断,使能整个系统的中断机制  8253定时器 
        clock_init();      // 10ms 时钟中断(1s中断100次)   kernel/driver/clock.c
        ----> pic_enable(IRQ_TIMER);// 使能定时器中断 
    
     h. 使能整个系统的中断机制 enable irq interrupt
        intr_enable();     // kernel/driver/intr.c
        // sti();          // set interrupt // x86.h
    
     i. lab1_switch_test();// 用户切换函数 会 触发中断用户切换中断
    
    4. kernel/trap/trap.c 
       trap中断(陷阱)处理函数
        trap() ---> trap_dispatch()   // kernel/trap/trap.c 
    
        a. 10ms 时钟中断处理 case IRQ_TIMER:
           if((ticks++)%100==0) print_ticks();//向终端打印时间信息(1s打印一次)
    
        b. 串口1 中断    case IRQ_COM1: 
           获取串口字符后打印
    
        c. 键盘中断      case IRQ_KBD: 
           获取键盘字符后打印
    
        d. 用户切换中断
    

    新添加的代码分析

    # 机器启动后有2种方式探测物理内存:直接探测、通过BIOS中断探测。 
    # 这里我们关注如何通过BIOS的0x15中断探测物理内存。
    # 由于BIOS中断需要在实模式下调用,所以我们在bootloader中探测物理内存比较合适(切换至 保护模式 之前)。
    ## BIOS的0x15中断有3个子功能:e820h、e801h、88h。这三个子功能区别是:
    ### e820h返回内存布局,信息量大,这是功能最强大的子功能,使用也最复杂。
    ### e801h返回内存容量
    ### 88h最简单,功能也最弱
    ### e820将物理内存探测的结果以 地址范围描述符 的格式放在内存中。
    # 地址范围描述符共计20字节,格式是:
    #     其具体表示如下:
    #       Offset      Size    Description
    #        00h 0~7    8字节   base address               # 内存块基地址
    #        08h 8~15   8字节   length in bytes            # 这块内存的大小
    #        10h 16~20  4字节   type of address range      # 这块内存的类型(共有4种类型)
    # 
    

    通过中断获取的一个一个内存块的信息会存入一个缓冲区中 e820map结构体

        /* memlayout.h */
        #define E820MAX 20
        struct e820map {
        int nr_map; // 表示数组元素个数,该字段是为了方便后续OS,不是BIOS访问的
        struct {
            long long addr;
            long long size;
            long type;
        } map[E820MAX];
        };
    

    e820h的调用参数

    显然在保护模式下用int $0x15来调用15h中断。
    
    但在这之前我们要将参数放置在寄存器中:
    
        eax:子功能编号,这里我们填入0xe820
        edx:534D4150h(ascii字符”SMAP”),签名,约定填”SMAP”
        ebx:每调用一次int $0x15,ebx会加1。当ebx为0表示所有内存块检测完毕。(重要!看后面的案例会明白如何使用)
        ecx:存放地址范围描述符的内存大小,至少要设置为20。
        es:di:告诉BIOS要把地址描述符写到这个地址。
        
    中断的返回值如下:
    
        CF标志位:若中断执行失败,则置为1。
        eax:     值是534D4150h(“SMAP”)
        es:di:   中断不改变该值,值与参数传入的值一致
        ebx:     下一个中断描述符的计数值(见后面的案例)
        ecx:     返回BIOS写到cs:di处的地址描述符的大小(应该就是20吧?)
        ah:      若发生错误,表示错误码
    

    boot/bootasm.S ab.物理内存探测 通过 BIOS 中断获取内存布局 比lab1多的部分=

     .set SMAP,                  0x534d4150    # 设置变量(即4个ASCII字符“SMAP”)
     
    # 第一步: 设置一个存放内存映射地址描述符的物理地址(这里是0x8000)
    probe_memory:
        # 约定在bootloader中将内存探测结果放到0x8000地址处。
        # 在0x8000处存放struct e820map, 并清除 e820map 中的 nr_map 记录了内存块的个数
        movl $0, 0x8000 # 对0x8000处的32位单元清零,即给位于0x8000处的struct e820map的成员变量 nr_map 清零
        xorl %ebx, %ebx # 清理 %ebx, 异或,相同为0,不同为1
        
        # 0x8004 处将用于存放第一个内存映射地址描述符 
        movw $0x8004, %di # 表示设置调用INT 15h BIOS中断后,BIOS返回的映射地址描述符的起始地址
                          # 中断前需要传递的参数, es:di:告诉BIOS要把地址描述符写到这个地址。
                          
    # 第二步: 将e820作为参数传递给INT 15h中断
    start_probe:
      # 传入0xe820 作为INT 15h中断的参数 
        movl $0xE820, %eax  #  INT 15的中断调用参数 eax:子功能编号,这里我们填入0xe820
      # 内存映射地址描述符的大小 
        movl $20, %ecx      # 设置地址范围描述符的大小为20字节,其大小等于struct e820map的成员变量map的大小
                            # 存放地址范围描述符的内存大小,至少要设置为20
        movl $SMAP, %edx    # 设置edx为534D4150h (即4个ASCII字符“SMAP”),这是一个约定
      # 调用INT 15h中断 
        int $0x15 # 中断参数0xe820,要求BIOS返回一个用地址范围描述符表示的内存段信息
        
    # 通过检测 eflags 的CF位来判断探测是否结束。
    # 如果没有结束, 设置存放下一个内存映射地址描述符的物理地址,然后跳到步骤2;如果结束,则程序结束
        # 如果eflags的CF位为0,则表示还有内存段需要探测 
        jnc cont # 如果发生错误,CF位为1。那么可以尝试使用其它子功能进行探测,或者就直接关机(连内存容量都没探测肯定无法启动OS了)
        movw $12345, 0x8000 # 在ucore中表示出错,与BIOS无关
        jmp finish_probe
    cont:
      # 继续探测 设置下一个内存映射地址描述符的起始地址 
        addw $20, %di   # 设置下一个BIOS返回的映射地址描述符的起始地址
                        # 控制BIOS该将“地址描述符”写到哪里
      # e820map中的nr_map加1 
        incl 0x8000     # 递增struct e820map的成员变量nr_map # nr_map成员自增1,该变量与BIOS无关
      # 如果还有内存段需要探测则继续探测, 否则结束探测 
        cmpl $0, %ebx   # 每调用一次int $0x15,ebx会加1。当ebx为0表示所有内存块检测完毕。
        jnz start_probe
    finish_probe:
    

    程序流程图

    练习1:实现 first-fit 连续物理内存分配算法(需要编程)

    在实现first fit 内存分配算法的回收函数时,要考虑地址连续的空闲块之间的合并操作。
    提示:在建立空闲页块链表时,需要按照空闲页块起始地址来排序,形成一个有序的链表。
    可能会修改default_pmm.c 中的 default_init,default_init_memmap,default_alloc_pages, default_free_pages等相关函数。
    请仔细查看和理解default_pmm.c中的注释。
    

    软件了解硬件实际物理内存(物理内存如何分布的,哪些可用,哪些不可用)

    探测物理内存分布和大小的方法, 基本方法是通过 BIOS 中断调用 来帮助完成的。
    
    其中BIOS中断调用必须在实模式下进行,所以在bootloader进入保护模式前完成这部分工作相对比较合适。
    
    所以需要修改bootloader软件。这些部分由boot/bootasm.S 中从 probe_memory 处到 finish_probe 处的代码部分完成。
    
    通过BIOS中断获取内存可调用参数为e820h的INT 15h BIOS中断。
    
    BIOS通过 系统内存映射地址描述符(Address Range Descriptor)格式来 表示 系统物理 内存布局。
    
    其具体表示如下:
        Offset  Size    Description
        00h     8字节   base address               # 系统内存块基地址
        08h     8字节   length in bytes            # 系统内存大小
        10h     4字节   type of address range      # 内存类型
    
    看下面的 系统内存映射地址类型
        Values for System Memory Map address type:
        type = 1 01h    memory, available to OS,是可以使用的物理内存空间
        type = 2 02h    reserved, not available (e.g. system ROM, memory-mapped device)
                        保留,不能使用的物理内存空间,
                        这些地址不能映射到物理内存上, 但它们可以映射到ROM或者映射到其他设备,比如各种外设等。
        type = 3 03h    ACPI Reclaim Memory (usable by OS after reading ACPI tables)
        type = 4 04h    ACPI NVS Memory (OS is required to save this memory between NVS sessions)
                 other  not defined yet -- treat as Reserved
    
    INT15h BIOS中断的详细调用参数:
        eax:e820h:INT 15的中断调用参数;
        edx:534D4150h (即4个ASCII字符“SMAP”) ,这只是一个签名而已;
        ebx:如果是第一次调用或内存区域扫描完毕,则为0。 如果不是,则存放上次调用之后的计数值;
        ecx:保存地址范围描述符的内存大小,应该大于等于20字节;
        es:di:指向保存地址范围描述符结构的缓冲区,BIOS把信息写入这个结构的起始地址。
    
    此中断的返回值为:
        eflags 的CF位:若INT 15中断执行成功,则不置位,否则置位;
        eax:  534D4150h ('SMAP') ;
        es:di:指向保存地址范围描述符的缓冲区,此时缓冲区内的数据已由BIOS填写完毕
        ebx:  下一个地址范围描述符的计数地址
        ecx:  返回BIOS往ES:DI处写的地址范围描述符的字节大小
        ah:   失败时保存出错代码
        
    这样,我们通过调用INT 15h BIOS中断,递增di的值(20的倍数),
    让BIOS帮我们查找出一个一个的内存布局 entry,
    并放入到一个保存地址范围描述符结构的 缓冲区 中,
    供后续的ucore进一步进行物理内存管理。
    
    这个缓冲区结构定义在memlayout.h中:
    
        /* memlayout.h */
        #define E820MAX 20
        struct e820map {
        int nr_map; // 表示数组元素个数,该字段是为了方便后续OS,不是BIOS访问的
        struct {
            long long addr;
            long long size;
            long type;
        } map[E820MAX];
        };
    
    bootasm.S 需要做的修改,再使能A20之后,线进行物理内存的探测保存一些信息后,再加载全局描述符表后进入保护模式。
    
    # bootasm.S
    # 2. 物理内存探测  通过 BIOS 中断获取内存布局
    # 第一步: 设置一个存放内存映射地址描述符的物理地址(这里是0x8000)
    probe_memory:
        # 在0x8000处存放struct e820map, 并清除e820map中的nr_map 
        movl $0, 0x8000 # 对0x8000处的32位单元清零,即给位于0x8000处的struct e820map的成员变量nr_map清零
        xorl %ebx, %ebx
        # 0x8004处将用于存放第一个内存映射地址描述符 
        movw $0x8004, %di # 表示设置调用INT 15h BIOS中断后,BIOS返回的映射地址描述符的起始地址
    # 第二步: 将e820作为参数传递给INT 15h中断
    start_probe:
        # 传入0xe820作为INT 15h中断的参数 
        movl $0xE820, %eax  #  INT 15的中断调用参数
        # 内存映射地址描述符的大小 
        movl $20, %ecx   # 设置地址范围描述符的大小为20字节,其大小等于struct e820map的成员变量map的大小
        movl $SMAP, %edx # 设置edx为534D4150h (即4个ASCII字符“SMAP”),这是一个约定
        # 调用INT 15h中断 
        int $0x15 # 要求BIOS返回一个用地址范围描述符表示的内存段信息
    # 通过检测eflags的CF位来判断探测是否结束。
    # 如果没有结束, 设置存放下一个内存映射地址描述符的物理地址,然后跳到步骤2;如果结束,则程序结束
        # 如果eflags的CF位为0,则表示还有内存段需要探测 
        jnc cont
        movw $12345, 0x8000
        jmp finish_probe
    cont:
        # 继续探测 设置下一个内存映射地址描述符的起始地址 
        addw $20, %di   # 设置下一个BIOS返回的映射地址描述符的起始地址
        # e820map中的nr_map加1 
        incl 0x8000 # 递增struct e820map的成员变量nr_map
        # 如果还有内存段需要探测则继续探测, 否则结束探测 
        cmpl $0, %ebx
        jnz start_probe
    finish_probe:
    
    上述代码正常执行完毕后,
    在0x8000地址处保存了从BIOS中获得的内存分布信息,此信息按照struct e820map的设置来进行填充。
    这部分信息将在bootloader启动ucore后,
    由ucore的 page_init函数 来根据 struct e820map的memmap(定义了起始地址为0x8000)
    来完成对整个机器中的物理内存的总体管理。
    

    物理内存空间管理的初始化 mm/pmmc —> pmm_init();

    当我们在bootloader 中完成对物理内存空间的 探测后(知己知彼,百战不殆), 
    我们就可以根据得到的信息来对可用的内存空间进行管理。
    
    在ucore中, 我们将物理内存空间按照 页page 的大小(4KB,  PGSIZE =4 kb)进行管理, 
    页的信息用Page这个结构体来保存。
    对整个计算机的每一个物理页的属性用结构Page.
    Page的定义在kern/mm/memlayout.h中。以页为单位的物理内存分配管理的实现在kern/default_pmm.[ch].
    
    // 每一个物理页的属性结构===============
    struct Page { // kern/mm/memlayout.h
        int ref;        // 映射此 物理页的 虚拟页个数,表示该 物理页 被 页表 的引用记数 page frame's reference counter
                        // 一旦某页表中有一个页表项设置了虚拟页到这个Page管理的物理页的映射关系,就会把Page的ref加一。
                        // 反之,若是解除,那就减一。
        uint32_t flags; // 描述 物理页 属性的标志flags数组, array of flags that describe the status of the page frame
          // 表示此物理页的状态标记,有两个标志位,第一个表示是否被保留,如果被保留了则设为1(比如内核代码占用的空间)。
          // 第二个表示此页是否是free的。
          // 如果设置为1,表示这页是free的,可以被分配;
          // 如果设置为0,表示这页已经被分配出去了,不能被再二次分配。
          
        unsigned int property; //  the num of free block, used in first fit pm manager
                               // 用来记录某连续内存空闲块的大小,
                               // 这里需要注意的是用到此成员变量的这个Page一定是连续内存块的开始地址(第一页的地址)。
                               
        list_entry_t page_link;// 双向链接 各个Page结构的 page_link 双向链表  free list link
        // list_entry_t 是便于把多个连续内存空闲块链接在一起的 双向链表指针,
        // 连续内存空闲块利用这个页的 成员变量 page_link 来链接比它地址小和大的其他连续内存空闲块.
    };
    
    // 然后是下面这个结构。
    // 一个双向链表,负责管理所有的 连续内存 空闲块,便于分配和释放==============
    typedef struct {// kern/mm/memlayout.h
        list_entry_t free_list;         //  是一个list_entry结构的双向链表指针 the list header
        unsigned int nr_free;           //  记录当前空闲页的个数, of free pages in this free list
    } free_area_t;
    
    

    物理内存空间管理的初始化的过程 mm/pmmc —> pmm_init() —> page_init()

    物理内存空间的初始化可以分为以下4步:

    1. 根据物理内存空间探测的结果, 找到最后一个可用空间的结束地址(或者Kernel的结束地址,选一个小的),
       根据这个 结束地址 计算出整个 可用的物理内存 空间一共有多少个页 npage。
       
    2. 找到Kernel的结束地址(end),这个地址是在kernel.ld 中定义的, 
       我们从这个地址所在的 下一个页开始(pages = roundup(end/PGSIZE) )写入系统页的信息(将所有的Page写入这个地址,信息页)。
       
    3. 从pages开始,将所有页的信息的flag 都设置为reserved(不可用)。
    
    4. 找到free页的开始地址, 并初始化所有free页的信息(free页就是除了kernel 和 页信息外的可用空间,
       初始化的过程会reset flag中的 reserved位)。
       
     上面这几部中提到了很多地址空间, 下面我用一幅图来说明:   
    

    end   指的就是 BSS 的结束处;
    pages 指的是 BSS结束处 - 空闲内存空间的起始地址;
    free 页是从 空闲内存空间的起始地址 - 实际物理内存空间结束地址。
    
    有了这幅图,这些地址就很容易理解了。 
    
    物理页数 数量:
        我们首先根据 bootloader 给出的 内存布局信息 找出 最大的物理内存地址 maxpa
        (定义在 page_init() (函数中的局部变量,maxpa = end;),
        
        由于x86 的 起始物理内存地址为0,
        每个页的大小为 PGSIZE = 4KB, 
        所以可以得知需要管理的物理页个数为:
        npage = maxpa / PGSIZE
        
    每个物理也都需要一个 结构体 Page 来进行记录,
    所以内存管理结构体 Page 需要的内存数量:
        这样,我们就可以预估出 管理 页级 物理内存空间 所需的 Page结构 的 内存空间 所需的 内存大小为:
        
        sizeof(struct Page) * npage
    
    内存管理处 的 起始管理结构体page:
        由于 bootloader 加载 ucore的 结束地址(用 全局指针变量end记录)以上的空间 没有被使用,
        所以我们可以把end 按页大小为 边界取整 后,作为 管理页级物理内存空间所需的Page结构的内存空间,记为:
     
        pages = (struct Page *)ROUNDUP((void *)end, PGSIZE);//  除去操作系统后的需要管理的内存的起始页地址
        
    空闲物理空间起始地址 free:
        内存管理处的起始地址page + 内存管理所需的内存数量,
        之后为空闲地址。
        uintptr_t freemem = PADDR((uintptr_t)pages + sizeof(struct Page) * npage);
    
    标记内存管理page:
        for (i = 0; i < npage; i ++) {
             SetPageReserved(pages + i);
      // 只需把物理地址对应的Page结构中的flags标志设置为PG_reserved ,表示这些页已经被使用了,将来不能被用于分配。
        }
    
    标记空闲物理空间:
        //获得空闲空间的起始地址begin和结束地址end
        ...
        init_memmap(pa2page(begin), (end - begin) / PGSIZE);
        // 是把空闲物理页对应的Page结构中的flags和引用计数ref清零,
        // 并加到 free_area.free_list 指向的双向列表中,为将来的空闲页管理做好初始化准备工作。
        // 从pages开始保存了所有物理页的信息(严格来讲, 在pages处保存的npage个页的信息并不一定是所有的物理页信息,
        // 它还包括各种外设地址,ROM地址等。不过因为它包含了所有可用free页的信息,我们就可以使用 pages来找到任何free页的信息)。 
        // 那如何将free页的信息和free页联系起来呢?很简单, 我们用地址的物理页号(pa 的高 20bit)作为index来定位free页的信息。
        // 因为pages处保存了系统中的第一个物理页的页信息,只要我们知道某个页的物理地址, 我们就可以很容易的找到它的页号(pa >> 12)。 
        // 有了页号,我们就可以通过pages[页号]来定位其页的信息了。在本lab中, 获取页的信息是由 pa2page() 来完成的。
        // 在初始化free页的信息时, 我们只将连续多个free页中第一个页的信息连入free_list中, 
        // 并且只将这个页的property设置为连续多个free页的个数。 其他所有页的信息我们只是简单的设置为0。
    
        相应的实现在 default_pmm.c 中的 default_alloc_pages 函数和 default_free_pages() 函数,
        相关实现很简单,这里就不具体分析了,直接看源码,应该很好理解。
    物理内存页管理器框架: 
    
        在内存分配和释放方面最主要的作用是建立了一个物理内存页管理器框架,这实际上是一个函数指针列表,定义如下:
    
    struct pmm_manager {
                const char *name;                                 // 物理内存页管理器的名字
                void (*init)(void);                               // 初始化内存管理器
                void (*init_memmap)(struct Page *base, size_t n); // 初始化管理空闲内存页的数据结构
                struct Page *(*alloc_pages)(size_t n);            // 分配n个物理内存页
                void (*free_pages)(struct Page *base, size_t n);  // 释放n个物理内存页
                size_t (*nr_free_pages)(void);                    // 返回当前剩余的空闲页数
                void (*check)(void);                              // 用于检测分配/释放实现是否正确的辅助函数
    };
    
    重点是实现 init_memmap/ alloc_pages / free_pages这三个函数。
    

    内存段页式管理

    这个lab中最重要的一个知识点就是内存的段页式管理。 
    下图是段页式内存管理的示意图:
    

    我们可以看到,在这种模式下,
    逻辑地址 先通过 段机制 转化成 线性地址, 
    然后通过两种 页表(页目录和页表) 来实现 线性地址 到 物理地址的转换。 
    有一点需要注意,在 页目录 和 页表中 存放的地址都是 物理地址。
    
    下面是页目录表项:
    

    下面是页表表项:
    

    在X86系统中,页目录表 的起始物理地址存放在 cr3 寄存器中, 
    这个地址必须是一个 页对齐 的地址,也就是低 12 位必须为0。
    在ucore 用boot_cr3(mm/pmm.c)记录这个值。
    
    在ucore中,线性地址的的高10位作为页目录表的索引,之后的10位作为页表的的索引,
    所以页目录表和页表中各有1024个项,每个项占4B,所以页目录表和页表刚好可以用一个物理的页来存放。
    

    地址映射

    在这个实验中,我们在4个不同的阶段使用了四种不同的地址映射, 下面我就分别介绍这4种地址映射。

    第一阶段---- 段式映射

    这一阶段是从bootasm.S的start到entry.S的kern_entry前,这个阶段很简单,
    和lab1一样(这时的GDT中每个段的起始地址都是0x00000000并且此时kernel还没有载入)。
    
       虚拟地址 virt addr = 线性地址 linear addr = 物理地址 phy addr
    

    第二阶段---- 段式映射

    这个阶段就是从entry.S的kern_entry到pmm.c的enable_paging()。 
    
    这个阶段就比较复杂了,我们先来看bootmain.c这个文件:
    
    #define ELFHDR ((struct elfhdr *)0x10000) // scratch space
    bootmain.c中的函数被调用时候还处于第一阶段, 
    所以从上面这个宏定义我们可以知道kernel是被放在物理地址为0x10000的内存空间。
    
    我们再来看看链接文件 tools/kernel.ld 
    
    /* Load the kernel at this address: "." means the current address */
         . = 0xC0100000;
     
    连接文件将kernel(物理地址 0x10000)链接到了0xC0100000(这是Higher Half Kernel, 
    具体参考 https://wiki.osdev.org/Higher_Half_Kernel ),
    这个地址是kernel的虚拟地址。 由于此时系统还只是采用 段式映射,如果我们还是使用
    
          虚拟地址 virt addr = 线性地址 linear addr = 物理地址 phy addr
      
        的话,我们根本不能访问到正确的内存空间,
    比如要访问虚拟地址 0xC0100000, 
    其物理地址应该在 0x00100000,
    而在这种映射下, 我们却访问了0xC0100000的物理地址。
    因此, 为了让虚拟地址和物理地址能匹配,我们必须要重新设计GDT。
    
    在entry.S中,我们重新设计了GDT
    可以看到,此时段的起始地址由0变成了-KERNBASE。因此,在这个阶段, 地址映射关系为:
    
    virt addr - 0xC0000000 = linear addr = phy addr
    0xC0100000 - 0xC0000000 = 0x00100000 
    
    这里需要注意两个个地方,
    第一,lgdt载入的是线性地址,
    所以用.long REALLOC(__gdt)将GDT的虚拟地址转换成了线性地址;
    第二,因为在载入GDT前,映射关系还是
    
        virt addr = linear addr = phy addr
    
    所以通过REALLOC(__gdtdesc)来将__gdtdesc的虚拟地址转换为物理地址,
    这样,lgdt才能真正的找到GDT存储的地方。
    

    第三阶段

    这个阶段是从kmm.c的enable_paging()到kmm.c的gdt_init()。 
    这个阶段是最复杂的阶段,我们开启了页机制,
    并且在boot_map_segment()中将线性地址按照如下规则进行映射:
    
        linear addr - 0xC0000000 = phy addr
    
    这就导致此时虚拟地址,线性地址和物理地址之间的关系如下:
    
        virt addr = linear addr + 0xC0000000 = phy addr + 2 * 0xC0000000
        
    这肯定是错误的,因为我们根本不能通过虚拟地址获取正确的物理地址, 我们可以继续用之前例子。
    我们还是要访问虚拟地址0xC0100000, 
    则其线性地址就是0x00100000,
    然后通过页映射后的物理地址是0x80100000。 
    我们本来是要访问0x00100000,却访问了0x80100000, 
    因此我们需要想办法来解决这个问题,即要让映射还是:
    
        virt addr - 0xC0000000 = linear addr = phy addr
    
    这个和第一阶段到第二阶段的转变类似,都是需要调整映射关系。
    为了解决这个问题, ucore使用了一个小技巧:
    在boot_map_segment()中, 
    线性地址0xC0000000-0xC0400000(4MB)对应的物理地址是0x00000000-0x00400000(4MB)。
    如果我们还想使用虚拟地址0xC0000000来映射物理地址0x00000000,
    也就是线性地址0x00000000来映射物理地址0x00000000,
    
    因为ucore在当前lab下的大小是小于4MB的,
    因此这么做之后, 我们依然可以按照阶段二的映射方式运行ucore。
    如果ucore的大小大于了4MB, 我们只需按同样的方法设置页目录表的第1,2,3...项。
    

    第四阶段

    这一阶段开始于kmm.c的gdt_init()。gdt_init()重新设置GDT,
    
    新的GDT又将段的起始地址变为了0. 调整后, 地址的映射关系终于由
    
    	virt addr = linear addr + 0xC0000000 = phy addr + 2 * 0xC0000000
    
      变回了
      
        virt addr = linear addr = phy addr + 0xC0000000
    
    同时,我们把目录表中的第0项设置为0,这样就把之前的这种映射关系解除了。
    通过这几个步骤的转换, 我们终于在开启页映射机制后将映射关系设置正确了。
    

    实验1

    我们第一个实验需要完成的主要是default_pmm.c中的
    1. default_init()       , 空闲内存页记录初始化,nr_free=0,总的空闲内存块先初始化为0;
    2. default_init_memmap(), 初始化空闲页链表,初始化每一个空闲页,并计算空闲页的总数;
    3. default_alloc_pages(), 分配内存大小为n的内存,遍历内容空闲页链表,找到第一块内存大小大于n的块,
                               然后分配出来,把它从空闲页链表中除去,
                               然后如果有多余的内存,把分完剩下的部分再次加入会空闲页链表中即可。
    4. default_free_pages() , 释放大小为n的内存
    

    1. default_init()

    kern/init.c ---> 内核初始化kern_init() ---> pmm_init()( mm/pmm.c);
    
    pmm_init() ---> a. 内存页管理器初始化 init_pmm_manager() ----> b. 内存页初始化page_init()
    

    a. 内存页管理器初始化 init_pmm_manager()

    init_pmm_manager() ----> default_pmm_manager.init() ----> default_init() —> list_init();

    内存管理器 结构体

    // 结构体定义==============
    struct pmm_manager {
                const char *name;                                 // 物理内存页管理器的名字
                void (*init)(void);                               // 初始化内存管理器
                void (*init_memmap)(struct Page *base, size_t n); // 初始化管理空闲内存页的数据结构
                struct Page *(*alloc_pages)(size_t n);            // 分配n个物理内存页
                void (*free_pages)(struct Page *base, size_t n);  // 释放n个物理内存页
                size_t (*nr_free_pages)(void);                    // 返回当前剩余的空闲页数
                void (*check)(void);                              // 用于检测分配/释放实现是否正确的辅助函数
    };
    // 结构体实例==========
    const struct pmm_manager default_pmm_manager = {
        .name = "default_pmm_manager",       // 名字
        .init = default_init,                // 管理器初始化函数
        .init_memmap = default_init_memmap,  // 内存映射初始化
        .alloc_pages = default_alloc_pages,  // 分配内存页
        .free_pages = default_free_pages,    // 空闲内存页
        .nr_free_pages = default_nr_free_pages,// 空闲内存页数量
        .check = default_check,                // 内存页检查
    };
    

    管理器初始化函数 default_init()

    // kern/mm/default_pmm.c
    free_area_t free_area;
    #define free_list (free_area.free_list)   // 双向链表表头 header
    #define nr_free (free_area.nr_free)       // 该 空闲区域链表 free list 中存储的空闲页的数量
    static void
    default_init(void) {
        list_init(&free_list); // 双向链表节点初始化 前节点指针 和 后节点指针 初始化为指向自己
        nr_free = 0;           // 空闲也数量清零 
    } 
    
    // 空闲区域链表 结构体 kern/mm/memlayout.h
    typedef struct {
        list_entry_t free_list; //  双向链表表头 header  节点
        unsigned int nr_free;   //  该 空闲区域链表 free list 中存储的空闲页的数量
    } free_area_t;
    
    // 双向链表节点 结构体   libs/list.h
    struct list_entry {
        struct list_entry *prev, *next;// 指向 前节点 和 后节点 的 节点指针
    };
    typedef struct list_entry list_entry_t;
    
    // 双向链表节点初始化 ============
    static inline void
    list_init(list_entry_t *elm) {
        elm->prev = elm->next = elm;// 前节点指针 和 后节点指针 初始化为指向自己
    }
    
    
    

    b. 内存页初始化page_init() 来自 pmm_init()( mm/pmm.c);

    page_init() -> 获取最大物理内存地址 maxpa
                   从Kernel的结束地址(end-->取整pages)开始初始化 内存信息页
                   从 真正空闲页 freemem 开始 初始化每一个可用的内存页 init_memmap()
    init_memmap() -> pmm_manager->init_memmap() --> default_init_memmap() (kern/mm/default_pmm.c)
    
    // page_init() 
    /* pmm_init - initialize the physical memory management */
    static void
    page_init(void) {
        struct e820map *memmap = (struct e820map *)(0x8000 + KERNBASE);
        uint64_t maxpa = 0;
        
    // 1. 获取物理内存空间的最大地址 maxpa =======================
        cprintf("e820map:\n");
        int i;
        for (i = 0; i < memmap->nr_map; i ++) // 地址范围描述符 数组元素 个数 nr_map
        {
          // 这里内存块的信息来源于 bootloder中使用 BIOS INT5h中断探测到的 内存信息
            uint64_t begin = memmap->map[i].addr,   end = begin + memmap->map[i].size;// 该块内存的地址范围
            cprintf("  memory: %08llx, [%08llx, %08llx], type = %d.\n",
                    memmap->map[i].size, begin, end - 1, memmap->map[i].type);// 内存块,大小,起始地址,借宿地址,类型(是否可用)
    	       
            if (memmap->map[i].type == E820_ARM) 
    	{
                if (maxpa < end && begin < KMEMSIZE) 
    	    {
                    maxpa = end;// 所有内存块中的最大地址, 实际物理内存空间结束地址 4GB  8GB 16GB ...
                }
            }
            
        }
        if (maxpa > KMEMSIZE) 
        {
            maxpa = KMEMSIZE;
        }
        
    // 2. 计算内存信息页的起始页 地址(在系统数据之后,用来记录所有内存页信息的)=====
        extern char end[];// 全局变量 end, Kernel的结束地址(end),这个地址是在tools/kernel.ld 中定义的(ucoreBSS段结束处)
        npage = maxpa / PGSIZE;// 计算总内存空间 按页分组(4K),需要多少页
        pages = (struct Page *)ROUNDUP((void *)end, PGSIZE);// 系统结束处  空闲内存空间的起始地址,page是4k的帧数倍
        // 把end 按页大小为 边界取整 后,作为 管理页级物理内存空间所需的Page结构的内存空间
        //  除去操作系统后的需要管理的内存的起始页地址
        
    // 3. 在 内存信息页区域(内存页管理区域)写入内存页信息======
        for (i = 0; i < npage; i ++) 
        {// 从pages开始,将所有页的信息的flag 都设置为reserved(不可用)。
            SetPageReserved(pages + i);// 该内存页 首先设置为 保留,不可用
        }
        
    //4.  最终空闲物理空间起始地址 free , free页就是除了kernel 和 页信息外的可用空间
    // 内存管理处的起始地址page + 内存管理所需的内存数量 ,之后为空闲地址。
        uintptr_t freemem = PADDR((uintptr_t)pages + sizeof(struct Page) * npage);
        
    // 5. 初始化每一块内存空间中的 空闲内存页==========================
        for (i = 0; i < memmap->nr_map; i ++)
        {
            uint64_t begin = memmap->map[i].addr, end = begin + memmap->map[i].size;// 该块内存的地址范围
            if (memmap->map[i].type == E820_ARM)
    	   {
                if (begin < freemem) 
    	        {
                    begin = freemem;// 将起始地址设置为空闲页空间的起始地址 freemem
                }
                if (end > KMEMSIZE)
    	        {
                    end = KMEMSIZE;// 最大地址
                }
                if (begin < end) 
    	        {
                    begin = ROUNDUP(begin, PGSIZE);// 内存起始地址 按页大小取整,还是地址,只不过地址是 页大小的整数倍
                    end = ROUNDDOWN(end, PGSIZE);// 内存结束地址 按页大小取整
                    if (begin < end) 
    		        {
    		  // 初始化改块内存空间的 free页的信息,重置页标志 flag中的 reserved位
    		      // pa2page() 物理内存地址转换成 页id ,在转成 页id指针
    		      //  pmm.h --> mmu.h   左移12位, 相当于除以 4096(4k), 每个内存页大小4K
    		      // return &pages[PPN(pa)]
    		      // 改块内存空间包含的空闲页数量 (end - begin) / PGSIZE
                        init_memmap(pa2page(begin), (end - begin) / PGSIZE);
       // init_memmap() -> pmm_manager->init_memmap() --> default_init_memmap() (kern/mm/default_pmm.c)
                    }
                }
            }
        }
    }
    
    

    2. 初始化空闲内存页 default_init_memmap() (kern/mm/default_pmm.c)

    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);// 并设置该内存页的 引用次数为0 ,还未使用过
        }
        base->property = n;// 记录从这里开始的 连续内存页 的数量
        SetPageProperty(base);
        nr_free += n;// 总空闲页计数
        list_add_before(&free_list, &(base->page_link));// 将空闲页 链表加入到 总内存空闲页表 之前
    }
    
    

    3. 分配内存页数量为n的内存,default_alloc_pages() (kern/mm/default_pmm.c)

    遍历内存空闲页链表,找到第一块内存大小大于n的页,
        然后分配出来,把它从空闲页链表中除去,
        然后如果有多余的内存,把分完剩下的部分再次加入会空闲页链表中即可。
    
    // 分配内存页数量为n的内存,default_alloc_pages(),
    //	遍历内存空闲页链表, 找到第一块连续内存页数量大小大于n的块,
    //        然后分配出来,把它从空闲页链表中除去,
    //        然后如果有多余的内存,把分完剩下的部分再次加入会空闲页链表中即可。
    static struct Page *
    default_alloc_pages(size_t n) 
    {
        assert(n > 0);
        if (n > nr_free) // 超过总的空闲页数量
        {
            return NULL;
        }
    // 遍历内存空闲页链表, 找到第一块连续内存页数量大小大于n的页========
        struct Page *page = NULL;// 内存页结构体
        list_entry_t *le = &free_list; // 空闲页 链表表头
        // TODO: optimize (next-fit)
        while ((le = list_next(le)) != &free_list) // 遍历 内存空闲页 链表
        {// 内存页 链表  转内存页
            struct Page *p = le2page(le, page_link);// kern/mm/memlayout.h ---> libs/defs.h
            if (p->property  >= n) // 连续内存页 的数量 大于n的
    	{
                page = p;// 找到第一块连续内存页数量大于n的内存块地址页
                break;
            }
        }
    //  然后把找到的合适的内存页 分配出来,把它从空闲页链表中除去=======
        if (page != NULL) 
        {
        //    list_del(&(page->page_link));
            if (page->property > n) 
    	{
                struct Page *p = page + n;// 连续空间 尾部的内存页 地址
                p->property = page->property - n;// 连续内存页数量 去除被划出去的n个数量
                SetPageProperty(p);
                list_add_after(&(page->page_link), &(p->page_link));// 在空闲页链表中去除被划去的部分
            }
            list_del(&(page->page_link));
            nr_free -= n;// 总空闲页数量 -n
            ClearPageProperty(page);
        }
        
        return page;
    }
    
    

    4. default_free_pages() , 释放n个内存页 (kern/mm/default_pmm.c)

    // 释放n个内存页=======================
    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);// 清零 被引用次数
        }
        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);// 遍历链表,指针迭代
    	
            // TODO: optimize
    	// 更新起始内存页 的 连续内存页记录==================
            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;// 更新空闲页数量
        le = list_next(&free_list);
        while (le != &free_list)
        {
            p = le2page(le, page_link);
            if (base + base->property <= p)
    	{
                assert(base + base->property != p);
                break;
            }
            le = list_next(le);
        }
    // 加入到链表之前
        list_add_before(le, &(base->page_link));
    }
    /*
    将根据传入的Page address来释放n page大小的内存空间。
    该函数会判断Page address是否是allocated的,也会判断是否base + n会跨界(由allocated到free的内存空间)。
    如果输入的Page address合法,则会将新的Page插入到free_list中的合适位置(free_list是按照Page地址由低向高排序的)。
    
    有一点需要注意,在本first-fit连续物理内存分配算法中,对于任何allocated后的Page,
    Property flag都为0;任何free的Page,Property flag都为1。
    
    对于allocated后的Pages,第一个Page的property在这里是被清零了的,
    如果ucore要求只能用第一个Page来free Pages,那么allocate时,第一个Page的property就不应该清零。
    我们在free Page时要用property来判断Page是不是第一个Page。
    
    如果ucore规定free需要free掉整个Page块,
    那么我们还需要检测第一个Page的property是否和要free的page数相等。
    上面这几点在Lab2中并不能确定,如果之后Lab有说明,或者出现错误,我们需要重新修改这些地方。
    */
    

    实验2

    这个练习是实现寻找虚拟地址对应的页表项。

    /* pmm.c */
    
    pte_t * get_pte(pde_t *pgdir, uintptr_t la, bool create) 
    {
        pte_t *pt_addr;
        struct Page *p;
        uintptr_t *page_la; 
        if (pgdir[(PDX(la))] & PTE_P) {
            pt_addr = (pte_t *)(KADDR(pgdir[(PDX(la))] & 0XFFFFF000)); 
            return &pt_addr[(PTX(la))]; 
        }
        else {
            if (create) {
                p = alloc_page();
                if (p == NULL) {
                    cprintf("boot_alloc_page failed.\n");
                    return NULL;
                }
                p->ref = 1;
                page_la = KADDR(page2pa(p));
                memset(page_la, 0x0, PGSIZE); 
                pgdir[(PDX(la))] = ((page2pa(p)) & 0xFFFFF000) | (pgdir[(PDX(la))] & 0x00000FFF); 
                pgdir[(PDX(la))] = pgdir[(PDX(la))] | PTE_P | PTE_W | PTE_U;
                return &page_la[PTX(la)]; 
            }
            else {
                return NULL;
            }
        }
    }
    
    这个代码很简单, 但有几个地方还是需要注意下: 
    	首先,最重要的一点就是要明白页目录和页表中存储的都是物理地址。
    	      所以当我们从页目录中获取页表的物理地址后,我们需要使用KADDR()将其转换成虚拟地址。
    	      之后就可以用这个虚拟地址直接访问对应的页表了。
    
    	第二, *, &, memset() 等操作的都是虚拟地址。
    	      注意不要将物理或者线性地址用于这些操作(假设线性地址和虚拟地址不一样)。
    
    	第三,alloc_page()获取的是物理page对应的Page结构体,而不是我们需要的物理page。
    	     通过一系列变化(page2pa()),我们可以根据获取的Page结构体得到与之对应的物理page的物理地址,
    	     之后我们就能获得它的虚拟地址。
    

    实验3

    这个练习是实现释放某虚地址所在的页并取消对应二级页表项的映射。

    /* pmm.c */
    
    static inline void
    page_remove_pte(pde_t *pgdir, uintptr_t la, pte_t *ptep) {
        pte_t *pt_addr;
        struct Page *p;
        uintptr_t *page_la; 
        if ((pgdir[(PDX(la))] & PTE_P) && (*ptep & PTE_P)) {
            p = pte2page(*ptep);   
            page_ref_dec(p); 
            if (p->ref == 0) 
                free_page(p); 
            *ptep = 0;
            tlb_invalidate(pgdir, la);
        }
        else {
            cprintf("This pte is empty!\n"); 
        }
    }
    

    在这里插入图片描述

    展开全文
  • 在解释第一个问题之前,先说明一下计算机内存管理的中的四个名词:虚拟内存,虚拟内存地址,物理内存物理内存地址。 先说说为什么会有虚拟内存和物理内存的区别。正在运行的一个进程,他所需的内存是有可能大于...
  • 虚拟内存和物理内存区别

    千次阅读 2016-10-12 23:28:51
    物理内存就是实际的内存,在CPU中指的是寻址空间的大小,比如8086只有20根地址线,那么它的寻址空间就是1MB,我们就说8086能支持1MB的物理内存,及时我们安装了128M的内存条在板子上,我们也只能说8086拥有1MB的物理...
  • 虚拟内存与物理内存的关系

    千次阅读 2019-05-25 18:45:35
    操作系统有虚拟内存与物理内存的概念。在很久以前,还没有虚拟内存概念的时候,程序寻址用的都是物理地址。程序能寻址的范围是有限的,这取决于CPU的地址线条数。比如在32位平台下,寻址的范围是2^32也就是4G。并且...
  • 物理内存物理内存是真正的内存,就是真实的插在板子上的内存。看机器配置的时候,看的就是这个物理内存。虚拟内存虚拟内存是为了满足系统对超出物理内存容量的需求时在外存(如硬盘)上开辟的存储空间。 由于虚拟...
  • 物理内存映射

    千次阅读 2018-05-26 15:04:01
    kernel 通过paging_init来映射物理内存 void __init paging_init(void) { #为pgd申请内存并赋值,这时候还没有buddy system,因此在早期都是通过memblock来申请内存的 phys_addr_t pgd_phys = early_pgtable_alloc...
  • 操作系统—物理内存与虚拟内存

    千次阅读 2018-07-15 17:12:17
    概念解析物理内存,在应用中,自然是顾名思义,物理上,真实的插在板子上的内存是多大就是多大了。而在CPU中的概念,物理内存就是CPU的地址线可以直接进行寻址的内存空间大小。比如8086只有20根地址线,那么它的寻址...
  • 查看linux系统中空闲内存/物理内存使用/剩余内存 查看系统内存有很多方法,但主要的是用top命令和free 命令 当执行top命令看到结果,要怎么看呢?这里说明一下: Mem: 666666k total, 55555k used,并不是代表你的...
  • linux内核虚拟内存之物理内存

    千次阅读 2017-09-15 11:41:58
    linux虚拟内存之物理内存描述
  • 虚拟内存和物理内存

    千次阅读 2014-04-02 22:26:16
    而计算机物理内存的访问地址则称为实地址或物理地址,其对应的存储空间称为物理存储空间或主存空间。程序进行虚地址到实地址转换的过程称为程序的再定位。 2、物理内存:在应用中,真实存在的,插在主板内存槽上的...
  • **物理内存 ** 1.在应用中,自然是顾名思义,物理上,真实的插在板子上的内存条是多大就是多大了。 2.在CPU中的概念,物理内存就是CPU的地址线可以直接进行寻址的内存空间大小。比如8086只有20根地址 线,那么它...
  • 物理内存与虚拟内存之间的映射

    万次阅读 多人点赞 2017-04-13 14:49:49
    而计算机物理内存的访问地址则称为实地址或物理地址,其对应的存储空间称为物理存储空间或主存空间。 2、虚拟存储器的容量限制:主存容量+辅存(硬盘)容量。 3、物理内存:在应用中,真实存在的,插在主板...
  • winserver物理内存使用不到一半就频繁爆内存

    千次阅读 多人点赞 2020-08-21 01:59:08
    使用的物理内存和内存使用的区别?提交、专用、工作集的区别?已提交又是什么东西? 如何配好winserver内存线上一直存在一个问题,内存无法最大利用化,经常出现服务崩溃问题。 winserver上有16g的物理内存,使用不...
  • swap交换空间,当系统的物理内存不够用的时候,就需要将物理内存中的一部分空间释放出来,以供当前运行的程序使用。那些被释放的空间可能来自一些很长时间没有什么操作的程序,这些被释放的空间被临时保存到Swap空间...
  • 物理内存就是物理上,真实的内存有多少;物理地址,地址对应的是总线,所以物理地址的大小跟总线的宽度相关。比如10根地址线则代表最大物理地址是2的10次方,1K的物理地址; 正在运行的一个进程,他所需的内存是有...
  • 转自:https://blog.csdn.net/rebirthme/article/details/50402082想必在linux上写过程序的同学都有分析进程占用多少内存的经历,或者被问到这样的问题——你的程序在运行时占用了多少内存(物理内存)?通常我们可以...
  • 虚拟内存是虚拟的,是操作系统为了合理使用内存而提出的一种到物理内存的动态映射,系统访问一个内存的时候,首先根据虚拟内存地址,通过映射表转换去找到对应的真正的物理内存上的存储位置,然后读取数据,合理利用...
  • Linux下物理内存和虚拟内存交换机制

    千次阅读 2016-03-17 22:29:39
    Linux下物理内存和虚拟内存交换机制   Vmstat是Virtual Memory Statistics虚拟内存统计缩写: 物理内存是计算机内存的大小,从物理内存中读写数据比硬盘中读写数据要快很多,而内存是有限的,所以就有了物理内存和...
  • 虚拟内存页号 to 物理内存页号

    千次阅读 2014-03-14 23:13:35
    物理内存地址 = (物理内存页号,内存页内偏移) if 查快表,虚拟内存页在TLB中 then case 1: 虚拟内存地址 to 物理内存地址; else // 查快表,虚拟内存页不在TLB中 case 2: if 查内表,虚拟内存页有对应...
  • documentlinux内存机制CPU内存虚拟内存硬盘物理内存内存和虚拟内存跟 Windows 完全不同的 Linux 内存机制Swap配置对性能的影响 linux内存机制 Linux支持虚拟内存(Virtual Mmemory),虚拟内存是指使用磁盘当作RAM的...
  • 物理内存(DRAM):只占物理地址一部分,由/proc/iomem可见,“System RAM” 就是物理内存的空间: io内存: 对外部设备寄存器(有的外部设备还有其内存)的编址方式,将物理地址(RAM)的一部分划出来用作IO地址...
  • linux内核虚拟内存和物理内存的映射

    千次阅读 2018-08-24 15:06:16
    NUMA指CPU对不同内存单元的访问时间可能不一样,因而这些物理内存被划分为几个节点,每个节点里的内存访问时间一致,NUMA体系结构主要存在大型机器、alpha等,嵌入式的基本都是UMA。UMA也使用了节点概念,只是永远都...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 47,112
精华内容 18,844
关键字:

物理内存