ucore lab2(无Challenge)

205 阅读9分钟

lab2

探测物理内存分布和大小的方法

  • 操作系统需要知道了解整个计算机系统中的物理内存如何分布的,哪些被可用,哪些不可用。这是因为我们印象中的内存是一个从0开始的大数组,但是实际上内存是由很多ram,rom构成的,所以os要知道什么地址开始是ram(具体可以去查实模式内存布局)。其基本方法是通过BIOS中断调用来帮助完成的。bootasm.S中新增了一段代码,使用BIOS中断检测物理内存总大小。

    对比lab1中的bootasm.S和lab2(用的meld)

    其实就是多了probe_memory处到finish_probe处的代码部分

    image-20221112175504137

  • 这段代码就是用了INT 15h BIOS中断获取内存的。15号中断大概是向es:di:(指向保存地址范围描述符结构的缓冲区)写入信息。(因为15中断是以系统内存映射地址描述符的格式写入的,所以缓冲区必需是以系统内存映射地址描述符所描述的)

  • BIOS通过系统内存映射地址描述符(Address Range Descriptor)格式来表示系统物理内存布局,其具体表示如下:

    Offset  Size    Description
    00h    8字节   base address               #系统内存块基地址
    08h    8字节   length in bytes            #系统内存大小
    10h    4字节   type of address range     #内存类型
    
  • ucore 也构造了一个结构体来映射系统内存映射地址描述符

    struct e820map {
                      int nr_map;               //map的数量
                      struct {
                                        long long addr;   //对应base address
                                        long long size;   //对应length in bytes 
                                        long type;        //表示内存状态 1 memory, available to OS 2reserved, not available (e.g. system ROM, memory-mapped device)
                      } map[E820MAX];
    };
    

    到此为止,我们就可以大概知道probe_memory处到finish_probe处的代码功能:向es:di指向的缓冲区(820map)写入内存信息

    INT15h BIOS中断的详细调用参数:

    eax:e820h:INT 15的中断调用参数;
    edx:534D4150h (即4个ASCII字符“SMAP”) ,这只是一个签名而已;
    ebx:如果是第一次调用或内存区域扫描完毕,则为0。 如果不是,则存放上次调用之后的计数值;
    ecx:保存地址范围描述符的内存大小,应该大于等于20字节;
    es:di:指向保存地址范围描述符结构的缓冲区,BIOS把信息写入这个结构的起始地址。
    
    probe_memory:
        movl $0, 0x8000   # e820map,就是缓冲区的开始 就是nr_map 初始化为0
        xorl %ebx, %ebx   #  将ebx清0,这是位操作,ebx作为中断的参数,表示第一次
        movw $0x8004, %di #  di指向map数组首地址
    start_probe:
        movl $0xE820, %eax  #  INT 15的中断调用参数;
        movl $20, %ecx    #  保存地址范围描述符的内存大小,应该大于等于20字节;
        movl $SMAP, %edx  #   534D4150h (即4个ASCII字符“SMAP”) ,这只是一个签名而已;
        int $0x15     #  调用bios中断
        jnc cont      #  如果该中断执行失败,则CF标志位会置1,此时要通知UCore出错
        movw $12345, 0x8000 # 向结构e820map中的成员nr_map中写入特殊信息,报告当前错误
        jmp finish_probe    # 结束
    cont:
        addw $20, %di   #  一个map数组的大小
        incl 0x8000     # nr_map++
        cmpl $0, %ebx   # 是否是内存区域扫描完毕
        jnz start_probe #没扫描完,继续扫描
    finish_probe:
    

    刚开始时我对这个0x8000感到十分困惑,到处找他是个什么东西。后来看网上说0x8000是缓冲区也就是e820map的地址

    我就在想为什么?后来我发现是我把因果倒置了,因为在这个boot时期哪来的的e820map结构,汇编语言只是把数据按内存映射地址描述符的格式放在内存0x8000上,把0x8000作为缓冲区仅此而已。什么e820map是我们后面要用到时才出现的结构,用来看物理内存。所以是先由0x8000后有e820map(这仅是我个人的理解而已)

  • 在0x8000地址处保存了从BIOS中获得的内存分布信息,此信息按照structe820map的设置来进行填充。这部分信息将在bootloader启动ucore后,由ucore的page_init函数来根
  • 据structe820map的memmap(定义了起始地址为0x8000)来完成对整个机器中的物理内存的总体管理。

image-20221115202344506

ucore的地址空间布局

  • 在刚开始上电时,寄存器被初始化,指向了bios( 0x000F0000),之后bios把bootloader放在0x00007C00 开始执行

  • 在uCore中,CPU先在bootasm.S(实模式)中通过调用BIOS中断,将物理内存的相关描述符写入特定位置0x8000,然后读入kernel的elf文件至物理地址0x10000、虚拟地址0xC0000000

    这个图是我看到别人的博客很好借来的,如果侵删

    image-20221115210416677

博客地址:

[]: kiprey.github.io/2020/08/uCo…

  • 顺便说一下,为什么elf header与它的section为什么在当前的地址,

  • elf header地址是在bootmain中通过readseg读到0x10000

  • 而data section是通过programmer header table 加载到这里的(因为已经打开了a20,可以寻到4gb了)

    image-20221115214040161

    image-20221115213946710

实验执行流程概述

  • 首先在bootloader中已经完成了物理内存的探测,之后再通过kernel_init中调用的pmm_init函数完成基于bootloader探测出的物理内存情况进行物理内存管理初始化工作。之后bootloader不像lab1那样,直接调用kern_init函数,而是先调用位于lab2/kern/init/entry.S中的kern_entry函数。

  • 这个时候我就很好奇,系统是怎么实现的?后来在kernel.ld中看到显示的entry是kern_entry说明现在控制权由bootloader到了ucore中的kern_entry手里了

  • 可是kern_entry作为ucore的代码,被kernel.ld文件放在了高地址,可是我们现在还是平坦寻址,会出错,所以构建了一个临时映射

    image-20221115211242575

  • # labcodes/lab2/kern/init/entry.S
    kern_entry:
        # load pa of boot pgdir
        movl $REALLOC(__boot_pgdir), %eax
        movl %eax, %cr3    #将页目录表的起始地址存入CR3寄存器中;
        # enable paging
        movl %cr0, %eax
        orl $(CR0_PE | CR0_PG | CR0_AM | CR0_WP | CR0_NE | CR0_TS | CR0_EM | CR0_MP), %eax
        andl $~(CR0_TS | CR0_EM), %eax
        movl %eax, %cr0  #把cr0中的CR0_PG标志位设置上,开启分页
    ​
        # update eip
        # now, eip = 0x1xxxxx
        leal next, %eax
        # set eip = KERNBASE + 0x1xxxxx
        jmp *%eax    #将eip的地址修改为虚拟地址
    next:
     # unmap va 0 ~ 4M, it is temporary mapping
        xorl %eax, %eax
        # 将__boot_pgdir的第一个页目录项清零,取消0~4M虚地址的映射
        movl %eax, __boot_pgdir
    ​
        # 设置C的内核栈
        # set ebp, esp
        movl $0x0, %ebp
        # the kernel stack region is from bootstack -- bootstacktop,
        # the kernel stack size is KSTACKSIZE (8KB)defined in memlayout.h
        movl $bootstacktop, %esp
        # now kernel stack is ready , call the first C function
        # 调用init.c中的kern_init总控函数
        call kern_init
      # .....省略剩余代码# kernel builtin pgdir
    # an initial page directory (Page Directory Table, PDT)
    # These page directory table and page table can be reused!
    .section .data.pgdir
    .align PGSIZE
    __boot_pgdir:
    .globl __boot_pgdir
        # map va 0 ~ 4M to pa 0 ~ 4M (temporary)
        .long REALLOC(__boot_pt1) + (PTE_P | PTE_U | PTE_W)
        .space (KERNBASE >> PGSHIFT >> 10 << 2) - (. - __boot_pgdir) # pad to PDE of KERNBASE
        # map va KERNBASE + (0 ~ 4M) to pa 0 ~ 4M
        .long REALLOC(__boot_pt1) + (PTE_P | PTE_U | PTE_W)
        .space PGSIZE - (. - __boot_pgdir) # pad to PGSIZE
         #将虚拟地址0~4M和虚拟地址kernelbase~kernel+4M映射到物理0~4M
    ​
    .set i, 0 # __boot_pt1是一个存在1024个32位long数据的数组,当将其作为页表时其中每一项都代表着一个物理地址映射项
    __boot_pt1:
    .rept 1024
        .long i * PGSIZE + (PTE_P | PTE_W)
        .set i, i + 1
    .endr
    

image-20221115231738881

采用了一个将kernel base开始的4M虚拟地址一一映射到物理内存的临时映射。(这也解释为什么要把虚拟0~4M和kernel base ~kernel+4M映射到同一个地方?因为在开始时,虚拟地址和物理地址就是一一映射的)

最终,离开这个阶段时,虚拟地址、线性地址以及物理地址之间的映射关系为:

 lab2 stage 2: virt addr = linear addr = phy addr + 0xC0000000 # 线性地址在0~4MB之内三者的映射关系
  • 到这一步,来梳理一下我们干了什么:探测了物理内存并开启了分页机制。那么理所当然的我们现在开始要对物理内存进行分页了。于是便引出我们物理页的数据结构page了

    struct Page {
      int ref;                // 当前页被引用的次数,与内存共享有关
      uint32_t flags;         // 标志位的集合,与eflags寄存器类似
      unsigned int property;  // 空闲的连续page数量。这个成员只会用在连续空闲page中的第一个page
      list_entry_t page_link; // 两个分别指向上一个和下一个非连续空闲页的指针。
    };
    ​
    
  • 所有page都存放在ucore section的后面也就是上文内存分布中的vpt页表。那么为了有效地管理这些小连续内存空闲块。所有的连续内存空闲块可用一个双向链表管理起来,便于分配和释放,为此定义了一个free_area_t数据结构,包含了一个list_entry结构的双向链表指针和记录当前空闲页的个数的无符号整型变量nr_free。其中的链表指针指向了空闲的物理页。

    typedef struct {
                list_entry_t free_list;                                // the list header
                unsigned int nr_free;                                 // # of free pages in this free list
    } free_area_t;
    

    image-20221116202841403

  • 为了这些页进行管理,我们又定义了一个pmm_manager数据结构

    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); //用于检测分配/释放实现是否正确的辅助函数
    };
    

    经过default_alloc_page(3)之后的内存(对应上文我画的图)

    static struct Page *
    default_alloc_pages(size_t n) {
        assert(n > 0);  //断言判断 n > 0
        if (n > nr_free) {
            return NULL;  //要求页数大于空闲页
        }
        struct Page *page = NULL;
        list_entry_t *le = &free_list;
        while ((le = list_next(le)) != &free_list) {
            struct Page *p = le2page(le, page_link); //通过le2page宏实现从free_list指针变成page(块)指针
            if (p->property >= n) {
                page = p;
                break;
            }
        }
        if (page != NULL) {
            list_del(&(page->page_link)); //在链表中删除原来指针
            if (page->property > n) {
                struct Page *p = page + n;
                p->property = page->property - n;
                list_add(&free_list, &(p->page_link)); //如果要求数小于分给它的页的数量,就做一次切割,并修改新生成的页(块)的property数并把它加入链表
        }
            nr_free -= n;
            ClearPageProperty(page); 
        }
        return page; //返回页指针
    }
    

    image-20221116202936328

  • struct Page *p = le2page(le, page_link);
    ​
    #define le2page(le, member)                 \
    to_struct((le), struct Page, member)
    ​
    #define offsetof(type, member)                                      \
    ((size_t)(&((type *)0)->member))
    ​
    #define to_struct(ptr, type, member)                               \
    ((type *)((char *)(ptr) - offsetof(type, member)))
    
  • offsetof这个宏就是求member(就是page_link)在page中的offset

因为&type *a->member就是求member的地址,可是如果a的地址是0,那么member的地址就变成member在page中的offset(开动你聪明的小脑筋想一想)

offset =& member-&a

如果&a = 0

offset = &member

这里采用了一个利用gcc编译器技术的技巧,即先求得数据结构的成员变量在本宿主数据结构中的偏移量,然后根据成员变量的地址反过来得出属主数据结构的变量的地址。

因为我们手里只有le(它就是page_link)

于是page_link的地址减page_link的offset就得到了page的首地址,在进行地址转换,不就得到page指针了

练习1:实现 first-fit 连续物理内存分配算法

  • 到这里是不是感觉都清楚了。要修改物理内存分配算法说到底就是修改pmm_manager

而first-fit都知道

1.空闲分区由地址从小到大排序

2.找到第一个比它大的,分配给它,如果有剩余,就做一次切割

3.归还时,注意合并

  • 就是按照这几点修改pmm_manager

1.发现default_init_memmap中用了list_add(&free_list, &(base->page_link));而list_add中调用list_add_after函数,会把新加的页放在free_list后面就是倒置了(地址从大到小)

所以要改为list_add_before(&free_list, &(base->page_link));

2.在default_alloc_pages做切割时也就是 list_add(&free_list, &(p->page_link)); 这一行时,并没有新的指针放在原来的指针的后面,而是放在free_list后面

所以要改为

if (page != NULL) {
        if (page->property > n) {
            struct Page *p = page + n;
            p->property = page->property - n;
            SetPageProperty(p);  //刚开始我就是完了这件事情
            list_add_after(&(page->page_link), &(p->page_link));
    }
        list_del(&(page->page_link));
        nr_free -= n;
        ClearPageProperty(page);
    }
    return page;

3.default_free_pages中默认会在函数末尾处,将待释放的页头插入至链表的第一个节点。

list_add(&free_list, &(base->page_link));

所以我们要该把他放回到原来位置。可是我们怎么找到原来位置,毕竟原来的指针已经被删除了。

但我们可以在链表中找地址大于base + base->property的第一个指针,把它放在指针前面。(因为地址是从小到大排的)

image-20221116223313327

图是承接上文的

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

练习2:实现寻找虚拟地址对应的页表项

到现在为止完成了物理地址的探测,实现了对物理地址的分页,并在ucore中开启了分页,也利用pmm_manager和page数据结构实现了对物理内存的管理。并且构建了在kernel base ~ kernel base +4M到0~4M的临时映射。

接下来就是实现虚拟页和物理页帧的地址映射,就是填充页目录表和页表

其大致流程如下:

  1. 先通过alloc_page获得一个空闲物理页,用于页目录表;

  2. 调用boot_map_segment函数建立一一映射关系,具体处理过程以页为单位进行设置,

    boot_pgdir[PDX(VPT)] = PADDR(boot_pgdir) | PTE_P | PTE_W; //页目录表
     
    boot_map_segment(boot_pgdir, KERNBASE, KMEMSIZE, 0, PTE_W); //以页为单位建立映射关系
    
    static void
    boot_map_segment(pde_t *pgdir, uintptr_t la, size_t size, uintptr_t pa, uint32_t perm) {
        assert(PGOFF(la) == PGOFF(pa));
        size_t n = ROUNDUP(size + PGOFF(la), PGSIZE) / PGSIZE;
        la = ROUNDDOWN(la, PGSIZE);
        pa = ROUNDDOWN(pa, PGSIZE);
        for (; n > 0; n --, la += PGSIZE, pa += PGSIZE) {
            pte_t *ptep = get_pte(pgdir, la, 1);
            assert(ptep != NULL);
            *ptep = pa | PTE_P | perm;
        }
    }
    

建立映射关系说到底就是根据la在页表中找到页表项pte,再把pa映射到该页上。get_pte(pgdir, la, 1);就是在pgdir页表(二级)上根据la找到页表项。(其实刚开始我也有点晕,后来我认为不管怎样我们只要给页就行了,因为有关页目录表是我们自己在get_pte的事情,映射还是关于虚拟页和物理页帧的映射)

有没有感觉很奇怪为什么*ptep = pa | PTE_P 就可以设置页表项。 是因为页表项 前面是地址 后面是状态位。而上面一行代码采用了bitset 就是前面是地址,后面(PTE_P)是状态位

boot_map_segment就实现了kernel的映射

get_pte的规则:

  1. 计算la1对应的 PDE 地址。
  2. 若该 PDE 不存在(PTE 所在的页表不存在)且create为 不为 0 ,创建页表并设置 PTE。
  3. 若该 PDE 不存在且create为 0 ,返回NULL
  4. 若该 PDE 存在,直接返回 PTE 虚拟地址。
    pde_t *pte = &pgdir[PDX(la)];
    
    if(!(*pte &PTE_P)){
        struct Page * page;
        if(!create || (page = alloc_page())== NULL)
            return NULL;
        set_page_ref(page,1);
        uintptr_t pa = page2pa(page);
        memset(KADDR(pa),0,PGSIZE); 
        *pte = pa | PTE_U | PTE_W | PTE_P;  
    }
​
        return &((pte_t *)KADDR(PDE_ADDR(*pte)))[PTX(la)];

有一个要点就是pde_t 指向的pde 它同时是一个pte_t,所以它的[PTX(la)]代表着页

la的布局

image-20221117232918466

但其实就算到了现在我们也只是完成了虚拟地址和pa的映射,但是虚拟地址只有映射上了物理页才可以正常的读写,所以还要介绍两个函数page_insert函数将物理页映射在了页表上,取消映射由page_remove来做,

image-20221118165955148

image-20221118170039236

这个图是通过uderstand生成的

练习3:释放某虚地址所在的页并取消对应二级页表项的映射

其实就是完成page_remove_pte函数

1.当释放一个包含某虚地址的物理内存页时,需要让对应此物理内存页的管理数据结构Page的ref减1;

2.如何物理页空闲,就要释放物理页

3.还需把表示虚地址与物理地址对应关系的二级页表项清除。

4.刷新TLB内的数据

if(*ptep & PTE_P){
    struct Page *page = pte2page(*ptep);
    if(page_ref_dec(page) == 0)
        free_page(page);
    *ptep = 0;
    tlb_invalidate(pgdir,la); 
}

自映射

当页目录与页表建立完成后,如果需要按虚拟地址的地址顺序显示整个页目录表和页表的内容,则要查找页目录表的页目录表项内容,并根据页目录表项内容找到页表的物理地址,再转换成对应的虚地址,然后访问页表的虚地址,搜索整个页表的每个页目录项。这样的过程比较繁琐,而自映射可以改善这个过程

自映射的关键就是

  • 把所有的页表(4KB * 1024个)放到连续的4MB 虚拟地址 空间中,并且要求这段空间4MB对齐,这样,就会有一张虚拟页的内容与页目录的内容完全相同
  • 页目录表中存在一个页目录条目,该条目内含的物理地址就是页目录表本身的物理地址。

代码:

boot_pgdir[PDX(VPT)] = PADDR(boot_pgdir) | PTE_P | PTE_W;

这个图是我看到别人的博客很好借来的,如果侵删

image-20221118163137972

博客地址:

[]: kiprey.github.io/2020/08/uCo…

结果:

make grade:

image-20221119143245705

make qemu:

image-20221119143559491

至于Challenge

就是buddy system和slub算法,等我有时间再写吧,主要是想推一下进度