lab2 物理内存管理

Posted Nullan

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了lab2 物理内存管理相关的知识,希望对你有一定的参考价值。

物理内存探测

物理内存分布和大小是用bios进行中断调用进行的,而中断调用需要用到bios,bios怎么运用呢?需要在实模式下,也就是bootloader还未加载前使用,BIOS中断获取内存可调用参数为e820h的INT 15h BIOS中断。返回值用di来增长,找到一个个内存的entry,然后用下面结构的缓冲区来保存

struct e820map {
                  int nr_map;
                  struct {
                                    long long addr;
                                    long long size;
                                    long type;
                  } map[E820MAX];
};

中断返回值

eflags的CF位:若INT 15中断执行成功,则不置位,否则置位;

eax:534D4150h ('SMAP') ;

es:di:指向保存地址范围描述符的缓冲区,此时缓冲区内的数据已由BIOS填写完毕

ebx:下一个地址范围描述符的计数地址

ecx    :返回BIOS往ES:DI处写的地址范围描述符的字节大小

ah:失败时保存出错代码

实现物理内存的探测
先是做准备:

probe_memory:
//对0x8000处的32位单元清零,即给位于0x8000处的
//struct e820map的成员变量nr_map清零
                  movl $0, 0x8000
                  xorl %ebx, %ebx
//表示设置调用INT 15h BIOS中断后,BIOS返回的映射地址描述符的起始地址
                  movw $0x8004, %di

cont:
//设置下一个BIOS返回的映射地址描述符的起始地址
                  addw $20, %di
//递增struct e820map的成员变量nr_map
                  incl 0x8000
//如果INT0x15返回的ebx为零,表示探测结束,否则继续探测
                  cmpl $0, %ebx
                  jnz start_probe
finish_probe:

然后设置中断参数

start_probe:
                  movl $0xE820, %eax // INT 15的中断调用参数
//设置地址范围描述符的大小为20字节,其大小等于struct e820map的成员变量map的大小
                  movl $20, %ecx
//设置edx为534D4150h (即4个ASCII字符“SMAP”),这是一个约定
                  movl $SMAP, %edx
//调用int 0x15中断,要求BIOS返回一个用地址范围描述符表示的内存段信息
                  int $0x15
//如果eflags的CF位为0,则表示还有内存段需要探测
                  jnc cont
//探测有问题,结束探测
                  movw $12345, 0x8000
                  jmp finish_probe
cont:
//设置下一个BIOS返回的映射地址描述符的起始地址
                  addw $20, %di
//递增struct e820map的成员变量nr_map
                  incl 0x8000
//如果INT0x15返回的ebx为零,表示探测结束,否则继续探测
                  cmpl $0, %ebx
                  jnz start_probe
finish_probe:

上面的代码执行完成后,将内存分布情况存储在0x8000,以结构体e820map的方式存储,在bootloader启动ucore后,由page_init函数来完成对机器的总体管理,且依据e820map的mmap进行管理,为0x8000

ucore的各个部分由一些.a和.o文件组成。而ld脚本把这些文件链接起来,这些各个组成部分在文件中的地址和ld脚本有关。而链接脚本主要规定如何把这些组成部分的section给到kernel文件,并且让它输出各个部分的空间地址的布局

/* Simple linker script for the ucore kernel.
   See the GNU ld 'info' manual ("info ld") to learn the syntax. */

OUTPUT_FORMAT("elf32-i386", "elf32-i386", "elf32-i386")
OUTPUT_ARCH(i386)
ENTRY(kern_entry)
//初始化内核入口
SECTIONS {
    /* Load the kernel at this address: "." means the current address */
    . = 0xC0100000;
//在这个地址加载内核
    .text : {
        *(.text .stub .text.* .gnu.linkonce.t.*)
    }

    PROVIDE(etext = .); /* Define the 'etext' symbol to this value */

    .rodata : {
        *(.rodata .rodata.* .gnu.linkonce.r.*)
    }

    /* Include debugging information in kernel memory */
    .stab : {
        PROVIDE(__STAB_BEGIN__ = .);
        *(.stab);
        PROVIDE(__STAB_END__ = .);
        BYTE(0)     /* Force the linker to allocate space
                   for this section */
    }

    .stabstr : {
        PROVIDE(__STABSTR_BEGIN__ = .);
        *(.stabstr);
        PROVIDE(__STABSTR_END__ = .);
        BYTE(0)     /* Force the linker to allocate space
                   for this section */
    }

    /* Adjust the address for the data segment to the next page */
    . = ALIGN(0x1000);

    /* The data segment */
    .data : {
        *(.data)
    }

    PROVIDE(edata = .);

    .bss : {
        *(.bss)
    }

    PROVIDE(end = .);

    /DISCARD/ : {
        *(.eh_frame .note.GNU-stack)
    }
}

最开始就是链接器的信息,然后告诉各个组成部分得section怎么组合,以什么顺序排列,这个脚本同时定义了一些符号比如.bss,.test,.data等。这些符号再组成一张符号表。每个符号包含了符号名字,符号所引用的内存地址,以及其他一些属性信息。

虚拟地址图:
/* *
 * Virtual memory map:                                          Permissions
 *                                                              kernel/user
 *
 *     4G ------------------> +---------------------------------+
 *                            |                                 |
 *                            |         Empty Memory (*)        |
 *                            |                                 |
 *                            +---------------------------------+ 0xFB000000
 *                            |   Cur. Page Table (Kern, RW)    | RW/-- PTSIZE
 *     VPT -----------------> +---------------------------------+ 0xFAC00000
 *                            |        Invalid Memory (*)       | --/--
 *     KERNTOP -------------> +---------------------------------+ 0xF8000000
 *                            |                                 |
 *                            |    Remapped Physical Memory     | RW/-- KMEMSIZE
 *                            |                                 |
 *     KERNBASE ------------> +---------------------------------+ 0xC0000000
 *                            |                                 |
 *                            |                                 |
 *                            |                                 |
 *                            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 * (*) Note: The kernel ensures that "Invalid Memory" is *never* mapped.
 *     "Empty Memory" is normally unmapped, but user programs may map pages
 *     there if desired.
 *
 * */

比如在上面ld中地址就是虚地址,而虚地址和物理地址得对应有这样一个公式:物理+0xc0000000 = 虚拟地址
而bootloader把kernel加载到内存中使用的是加载地址,因为这个时候只是分段,还没有进行分页,所以是段寻址,此时的关系在源码中有:linear addr = phy addr = virtual addr
在bootmain.c里面有:
readseg(ph->p_va & 0xFFFFFF, ph->p_memsz, ph->p_offset);
这里的ph->p_va=0xC0XXXXXX,就是ld工具根据kernel.ld设置的链接地址,且链接地址等于虚地址。考虑到ph->p_va & 0xFFFFFF == 0x0XXXXXX,所以bootloader加载ucore kernel的加载地址是0x0XXXXXX, 这实际上是ucore内核所在的物理地址。
也就是说加载kernel是物理地址,链接用虚地址

BSS段(bss segment):指用来存放程序中未初始化的全局变量的内存区域。BSS是英文Block Started by Symbol的简称。BSS段属于静态内存分配。
数据段(data segment):指用来存放程序中已初始化的全局变量的一块内存区域。数据段属于静态内存分配。
代码段(code segment/text segment):指用来存放程序执行代码的一块内存区域。这部分区域的大小在程序运行前就已经确定,并且内存区域通常属于只读, 某些架构也允许代码段为可写,即允许修改程序。在代码段中,也有可能包含一些只读的常数变量,例如字符串常量等。

一般在OS里用页机制来管理,也就是说一个页就为物理内存分配的最小单位管理过程中就要设定其状态,比如free和used等,接着建立页表,启动分页机制,根据页表项完成虚拟页与物理页帧的对应关系
内存管理相关的总体控制函数是pmm_init函数,它完成的主要工作包括:

初始化物理内存页管理器框架pmm_manager;
建立空闲的page链表,这样就可以分配以页(4KB)为单位的空闲内存了;
检查物理内存页分配算法;
为确保切换到分页机制后,代码能够正常执行,先建立一个临时二级页表;
建立一一映射关系的二级页表;
使能分页机制;
从新设置全局段描述符表;
取消临时二级页表;
检查页表建立是否正确;
通过自映射机制完成页表的打印输出(这部分是扩展知识)

探测完物理内存就要使用物理页,用page数据结构来表示物理页,用4KB来对齐,大小也是4KB.一个物理页需要占用一个Page结构的空间,Page结构在设计时须尽可能小,以减少对内存的占用。

struct Page {
    int ref;        // page frame's reference counter
    uint32_t flags; // array of flags that describe the status of the page frame
    unsigned int property;// the num of free block, used in first fit pm manager
    list_entry_t page_link;// free list link
};

ref表示被引用数,如果被引用了,那么就+1
flags表示此物理页的状态标记,用两个bit来表示,bit0表示这个页是否被保留,如果是1,且那就被保留,不能参与动态分配与释放,且不能放入空闲页链表中。bit1是1的话那就是free的,可以被分配,0是不能再被分配,因为已经free过了
property:是head page用来表示连续内存空闲块的大小,即地址连续的空闲页的个数。head page表示此连续物理内存页中地址最小的一块
page_link是把多个连续内存页互相链接在一起的双向链表指针,是这个连续内存页块最小的页head page用来链接其他连续内存页的
一开始有是很大的一块内存块,但随着分配与释放,会分成很多小的块,所有的连续内存空闲块可用一个双向链表管理起来,便于分配和释放,为此定义了一个管理空闲表的free_area_t数据结构。

/* free_area_t - maintains a doubly linked list to record free (unused) pages */
typedef struct {
            list_entry_t free_list;                                // the list header
            unsigned int nr_free;                                 // # of free pages in this free list
} free_area_t;
struct list_entry {
    struct list_entry *prev, *next;
};

typedef struct list_entry list_entry_t;

free_list双向链表指针
nr_free:空闲物理页的个数
我们通过bootloader探测的内存找到物理地址最大的一块为maxpa(定义在page_init函数中的局部变量),物理地址起始为0,那么物理页个数为npage = maxpa / PGSIZE
内存空间 sizeof(struct Page) * npage

uintptr_t freemem = PADDR((uintptr_t)pages + sizeof(struct Page) * npage);

由于bootloader加载ucore的结束地址(用全局指针变量end记录)以上的空间没有被使用,所以我们可以把end按页大小为边界取整后,作为管理页级物理内存空间所需的Page结构的内存空间,记为:

pages = (struct Page *)ROUNDUP((void *)end, PGSIZE);
//end为前面加载ucore已经用掉的内存,将end向PGSIZE大小取整,舍入的位数

为了简化起见,从地址0到地址pages+ sizeof(struct Page) * npage)结束的物理内存空间设定为已占用物理内存空间(起始0~640KB的空间是空闲的),地址pages+ sizeof(struct Page) * npage)以上的空间为空闲物理内存空间,这时的空闲空间起始地址为

uintptr_t freemem = PADDR((uintptr_t)pages + sizeof(struct Page) * npage);

要把这两部分空间分别标记,所以要实现占用标记

for (i = 0; i < npage; i ++) {
SetPageReserved(pages + i);
}

然后实现空闲标记

//获得空闲空间的起始地址begin和结束地址end
...
init_memmap(pa2page(begin), (end - begin) / PGSIZE);

SetPageReserved函数:把物理地址对应的Page结构中的flags标志设置为PG_reserved ,表示这些页已经被使用,将来不能被用于动态内存分配。
init_memmap函数:把空闲物理页对应的Page结构中的flags和ref清零,并加到free_area.free_list指向的双向列表中,为将来的空闲页管理做好初始化准备工作。
内存分配函数:

static struct Page *
default_alloc_pages(size_t n) {
    assert(n > 0);
    if (n > nr_free) {
        return NULL;
    }
//n不能大于空闲物理页的个数
    list_entry_t *le, *len;
    le = &free_list;
//定义两个双向链表结构le和len
//le指向了free list这个双向链表指针

    while((le=list_next(le)) != &free_list) {
    //只要获取表管理的地址不是free_list
    
      struct Page *p = le2page(le, page_link);
       将le节点转换为关联的Page结构,得到所属的page结构
      if(p->property >= n){//如果是headpage判断连续内存页的个数
        int i;
        for(i=0;i<n;i++){
        
          len = list_next(le);
          struct Page *pp = le2page(le, page_link);
          
          SetPageReserved(pp);
          ClearPageProperty(pp);
          list_del(le);
          le = len;
        }
        if(p->property>n){
         // 发现一个满足要求的,空闲页数大于等于N的空闲块
          (le2page(le,page_link))->property = p->property - n;
        }
        ClearPageProperty(p);
        SetPageReserved(p);
        nr_free -= n;
        return p;
      }
    }
    return NULL;
}
//如果page != null代表找到了,分配成功。反之则分配物理内存失败

其实也就是遍历free_list在多个连续内存页中找到符合N大小的内存块

first fit算法

libs/list.h定义了可挂接任意元素的通用双向链表结构和对应的操作,所以需要了解如何使用这个文件提供的各种函数,从而可以完成对双向链表的初始化/插入/删除等。
之前free_area_t的结构如下:

  list_entry_t free_list;         // the list header   空闲块双向链表的头
  unsigned int nr_free;           // # of free pages in this free list  空闲块的总数(以页为单位)

就用这个结构完成对空闲块的管理,在文件default_pmm.c中的free_area_t,
free_area变量的, first_fit分配算法可直接重用default_init函数的实现
default_init_memmap函数将根据每个物理页建立空闲页链表,且空闲页块应该是根据地址高低形成一个有序链表。

default_init_memmap(struct Page *base, size_t n) {
    struct Page *p = base;
    for (; p != base + n; p ++) {
        p->flags = p->property = 0;
        set_page_ref(p, 0);
    }
    base->property = n;
    SetPageProperty(base);
    nr_free += n;
    list_add(&free_list, &(base->page_link));
}

default_free_pages函数的实现其实是default_alloc_pages的逆过程
通过list_next找到下一个空闲块元素,通过le2page宏可以由链表元素获得对应的Page指针p。通过p->property可以了解此空闲块的大小。

段页机制

有逻辑地址和物理地址
逻辑地址,就是我们访问的地址
物理地址就是实际上存储在物理固件上的地址
逻辑地址通过段式管理得到的地址映射为线性地址,线性地址通过分页映射得到的是物理地址

段式管理只起到了一个过渡作用,它将逻辑地址不加转换直接映射成线性地址,所以下面线性地址 = 逻辑地址

地址映射阶段

第一阶段:开启保护模式,创建段表的时候,虚拟地址,线性地址,逻辑地址还没进行所谓的区分。

 virt addr = linear addr = phy addr

第二阶段:创建页表,开启分页。从kern_entry函数开始,到pmm_init函数被执行之前。将一个自带的页目录表和页表设置好,然后将0~4M的线性地址映射到物理地址。
movl %eax, %cr3:把页目录表的起始地址存入CR3寄存器中
movl %eax, %cr0:把设置cr0中的CR0_PG标志位

  virt addr = linear addr = phy addr # 线性地址在0~4MB之内三者的映射关系
  virt addr = linear addr = phy addr + 0xC0000000 # 线性地址在0xC0000000~0xC0000000+4MB之内三者的映射关系

此时范围还是0~4MB的低虚拟地址,并且,这个地址是要给用户程序用的,所以需要用一个绝对跳转到高地址,跳转完后,把表项清零取消临时的映射关系,此时

virt addr = linear addr = phy addr + 0xC0000000 # 线性地址在0~4MB之内三者的映射关系

第三段:从pmm_init函数被调用开始。pmm_init函数将页目录表项补充完成然后,更新了段映射机制,使用了一个新的段表。这个新段表包括内核态的代码段和数据段描述符,用户态的代码段,数据段描述符,TSS(段)的描述符。

virt addr = linear addr = phy addr + 0xC0000000

二级页表

在二级页表结构中,页目录表占4KB空间,可通过alloc_page函数获得一个空闲物理页作为页目录表
要把0~KERNSIZE的物理地址映射到页表项和页目录项上的步骤:

boot_pgdir:存储指向页目录表的指针
填充好0~4MB的首页表
调用boot_map_segment函数进一步建立映射关系:
   linear addr = phy addr + 0xC0000000

设la是线性地址,la高10位中存在(PTE_P)为0,表示缺少对应的页表空间,alloc_page获得一个空闲物理页给页表,页表起始物理地址是按4096字节对齐的,这样填写页目录项的内容为

页目录项内容 = (页表起始物理地址 & ~0x0FFF) | PTE_U | PTE_W | PTE_P
页表项内容 = (pa & ~0x0FFF) | PTE_P | PTE_W

PTE_U:位3,表示用户态的软件可以读取对应地址的物理内存页内容

PTE_W:位2,表示物理内存页内容可写
PTE_P:位1,表示物理内存页存在
查页表时:给定一个虚拟地址,找出这个虚拟地址在二级页表中对应的项。通过更改此项可以方便地将虚拟地址映射到其他页上。可完成此功能的这个函数是get_pte函数。它的原型:

pte_t *get_pte(pde_t *pgdir, uintptr_t la, bool create)

只有当一级二级页表的项都设置了写权限后,才能对对应的物理地址进行读写。二级页表再对权限进行限制
当这个页需要在一个地址上解除映射时,操作系统不能直接把这个页回收,而是要先看看它还有没有映射到别的虚拟地址上。通过物理页的Page数据结构的成员变量ref实现。如果ref = 0,就没有虚拟页到物理页的映射关系,就可以把这个物理页给回收了,这个物理页是free的,可以再被分配。
建立好一一映射的二级页表结构后,由于分页机制在前一节所述的前两个阶段已经开启,分页机制到此初始化完毕。当执行完毕gdt_init函数后,新的段页式映射已经建立好了。
如图为最后的虚拟地址图

以上是关于lab2 物理内存管理的主要内容,如果未能解决你的问题,请参考以下文章

lab2 物理内存管理

MIT-6.828 Lab 2实验报告

ucore实验

物理内存管理:连续内存分配

物理内存管理:连续内存分配

Linux 内核 内存管理物理分配页 ⑨ ( __alloc_pages_slowpath 慢速路径调用函数源码分析 | retry 标号代码分析 )