ucore lab2笔记

 

 

 

1.探测系统物理内存布局

当 ucore 被启动之后,最重要的事情就是知道还有多少内存可用,其基本方法是通过BIOS中断调用来帮助完成的。其中BIOS中断调用必须在实模式下进行,所以在bootloader进入保护模式前完成这部分工作相对比较合适。这些部分由boot/bootasm.S中从probe_memory处到finish_probe处的代码部分完成。

通过BIOS中断获取内存可调用参数为e820h的INT 15h BIOS中断。并且把 e820 映 射结构保存在物理地址0x8000处。具体实现详见boot/bootasm.S

https://chyyuu.gitbooks.io/ucore_os_docs/content/lab2/lab2_3_3_2_search_phymem_layout.html

知识结构及对应联系如下:

详细请看:(https://chyyuu.gitbooks.io/ucore_os_docs/content/lab2/lab2_3_5_probe_phymem_methods.html)

bootasm.S新增的物理内存探测部分

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

                  movl $20, %ecx    // 设置地址描述符的大小为20字节,其大小等于系统内存映射地址描述符的大小

                  movl $SMAP, %edx // 设置edx为534D4150h (即4个ASCII字符“SMAP”),这是一个约定
//调用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:

2.以页为单位管理物理内存

在获得可用物理内存范围后,系统需要建立相应的数据结构来管理以物理页(按4KB对齐,且大小为4KB的物理内存单元)为最小单位的整个物理内存,以配合后续涉及的分页管理机制。每个物理页可以用一个 Page数据结构来表示。由于一个物理页需要占用一个Page结构的空间,Page结构在设计时须尽可能小,以减少对内存的占用。Page的定义在kern/mm/memlayout.h中。以页为单位的物理内存分配管理的实现在kern/default_pmm.[ch]。

https://chyyuu.gitbooks.io/ucore_os_docs/content/lab2/lab2_3_3_3_phymem_pagelevel.html

既然我们要以页作为最小单位来管理物理内存,首先我们要为每个页分配一个数据结构来记录相关信息,便于后续的管理。我们也把这些信息存放在内存的一块特定区域。

Page结构

Page结构体的定义(kern/mm/memlayout.h):

/* *
 * struct Page - Page descriptor structures. Each Page describes one
 * physical page. In kern/mm/pmm.h, you can find lots of useful functions
 * that convert Page to other data types, such as phyical address.
 * */
struct Page {
    // 页帧的 引用计数
    int ref;

    // 页帧的状态
    // bit 0: 此页是否被保留(reserved)
    // bit 1: 此页是否可以被分配(free?)
    uint32_t flags;

    // 记录连续空闲页的数量 只在第一页进行设置
    unsigned int property;

    // 用于将所有的页帧串在一个双向链表中 这个地方很有趣 直接将 Page 这个结构体加入链表中会有点浪费空间 因此在 Page 中设置一个链表的结点 将其结点加入到链表中 还原的方法是将 链表中的 page_link 的地址 减去它所在的结构体中的偏移 就得到了 Page 的起始地址
    list_entry_t page_link;
};

有关page_link,我们到后面再详细分析

现在我们有每个页的结构,为了分配内存,我们需要知道现在有哪些页是空闲(free)的,所以需要一个数据结构来记录那些没被使用的页(能被分配的页)。也就是free_area_t.

free_area_t结构

free_area-t结构体的定义(kern/mm/memlayout.h):

/* 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;

 结构体中有两个成员变量:

  free_list: 一个双向循环链表的指针,这个双向链表记录了没被使用的页。

  nr_free: 现在链表中有多少个页(也就是现在能被分配的页)

list_entry_t的定义(lib/list.h):

struct list_entry {
    struct list_entry *prev, *next;
};

typedef struct list_entry list_entry_t;

其实就是一个简单的双向链表节点

这里需要注意一点,其实链表节点不是直接指向Page结构,而是指向Page结构中的page_link成员变量。

为什么要这样做,请参考(https://chyyuu.gitbooks.io/ucore_os_docs/content/lab0/lab0_2_6_2_1_linked_list.html).

 

这样以free_area_t结构的数据为双向循环链表的链表头指针,以Page结构的数据为双向循环链表的链表节点,就可以形成一个完整的双向循环链表,如下图所示:

空闲块双向循环链表

 

结构体建立完之后,通过page_init()来遍历之前探测到的物理内存,把已经占用的内存设为reserved,把没能被分配的内存放到free page list 里。

page_init()函数:

​​/* page_init - 
detect physical memory space, reserve already used memory,
then use pmm->init_memmap to create free page list
*/
static void
page_init(void) {
    struct e820map *memmap = (struct e820map *)(0x8000 + KERNBASE);
    uint64_t maxpa = 0;

    cprintf("e820map:\n");
    int i;
    for (i = 0; i < memmap->nr_map; i ++) {
        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;
            }
        }
    }
    if (maxpa > KMEMSIZE) {
        maxpa = KMEMSIZE;
    }

    extern char end[];

    npage = maxpa / PGSIZE;
    pages = (struct Page *)ROUNDUP((void *)end, PGSIZE);

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

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

    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;
            }
            if (end > KMEMSIZE) {
                end = KMEMSIZE;
            }
            if (begin < end) {
                begin = ROUNDUP(begin, PGSIZE);
                end = ROUNDDOWN(end, PGSIZE);
                if (begin < end) {
                    init_memmap(pa2page(begin), (end - begin) / PGSIZE);
                }
            }
        }
    }
}

最后,我们需要建立一个物理内存页管理器框架。

3.物理内存页分配算法实现

简单来说,当上层需要请求物理内存时,我们需要有个管理器能够分配物理内存页给上层。为此,管理器需要维护一个数据结构,which 能实时保存当前可供分配的物理内存页。这数据结构就是上面的free_area 双向循环链表。最开始,所有的物理页对应的Page结构都在这链表中。

pmm_manager结构

物理内存页管理器框架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); //用于检测分配/释放实现是否正确的辅助函数
};

接下来,就是manager结构体下面成员函数的实现

default_init_memap()

/*对一个连续的、可被分配的内存块(包含n页)进行初始化:
  1.对这一块内存的每一页:
    设p->flags:
        PG_reserved位(bit 0):设为0 未被保留
        PG_property位(bit 1):设为1 还未被分配
    设p->property为0
    设p->ref为0
  2.将这一页插入free_list 
    
  3.nr_free加上新加入的n
  
  4.把第一块的property设为n 
*/
  
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 = 0;
        SetPageProperty(p);
        p->property = 0;
        set_page_ref(p, 0);

        list_add_before(&free_list, &(p->page_link));
    }
    nr_free += n;
    //first page
    base->property = n;
}

default_alloc_pages()

/*为空间请求分配指定大小(n)的空间
  1.遍历free_list找出第一个大于等于n的内存块

  2.找到对应内存块后(其实是找到free_list中第一个property >= n 的页p), 遍历free_list,对这n个页进行设置:
        PG_reserved位(bit 0):设为1 已被占用
        PG_property位(bit 1): 设为0 已被分配

  3.将这n个页从free_list中删除
  
  4.将free_list的原第n+1个页(我们刚刚分配的块的下一页)的property置为p->property - n, 设置为新块

  5.nr_free减去分配的n

  6.返回分配的内存块的头页p

*/

static struct Page *
default_alloc_pages(size_t n) {
    assert(n > 0);
    if (n > nr_free) {
        return NULL;
    }
    list_entry_t *le, *len;
    le = &free_list;

    while((le=list_next(le)) != &free_list) {
      struct Page *p = le2page(le, page_link);
      if(p->property >= n){
        ClearPageProperty(p);
        SetPageReserved(p);
        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){
          (le2page(le,page_link))->property = p->property - n;
        }
        
        nr_free -= n;
        return p;
      }
    }
    return NULL;
}

 

default_free_pages()

/*将指定页开始,连续n页的内存块释放,重新插入free_list
  1.遍历free_list,找到插入点。(第一个地址大于base的页p的位置)

  2.将从base开始的n页逐个插入

  3.对这一块内存的每一页:
    设p->flags:
        PG_reserved位(bit 0):设为0 未被保留
        PG_property位(bit 1):设为1 还未被分配
    设p->property为0
    设p->ref为0
    
  4.判断是否能合并
    A.若能向前合并(base + n == p)
        将base->property += p->property
        p->property =0
    B.若能向后合并(base - 1 == )
      向前遍历找到上一块的头页,合并
   
  5.nr_free加上n
*/

static void
default_free_pages(struct Page *base, size_t n) {
    assert(n > 0);
    assert(PageReserved(base));

    list_entry_t *le = &free_list;
    struct Page * p;
    while((le=list_next(le)) != &free_list) {
      p = le2page(le, page_link);
      if(p>base){
        break;
      }
    }
    //list_add_before(le, base->page_link);
    for(p=base;p<base+n;p++){
      list_add_before(le, &(p->page_link));
    }
    base->flags = 0;
    set_page_ref(base, 0);
    ClearPageProperty(base);
    SetPageProperty(base);
    base->property = n;
    
    p = le2page(le,page_link) ;
    if( base+n == p ){
      base->property += p->property;
      p->property = 0;
    }
    le = list_prev(&(base->page_link));
    p = le2page(le, page_link);
    if(le!=&free_list && p==base-1){
      while(le!=&free_list){
        if(p->property){
          p->property += base->property;
          base->property = 0;
          break;
        }
        le = list_prev(le);
        p = le2page(le,page_link);
      }
    }

    nr_free += n;
    return ;
}

4.实现分页机制

get_pte()

//get_pte - get pte and return the kernel virtual address of this pte for la
//        - if the PT contians this pte didn't exist, alloc a page for PT
pte_t *
get_pte(pde_t *pgdir, uintptr_t la, bool create) {
        int i = PDX(la); // la的对应PDE项在PD中的索引
        uintptr_t pa;
        pde_t cpde = *(pgdir + i); // la的对应PDE
        if (!(cpde & PTE_P)){ // 当cpde not present
                struct Page *page;
                if (!create || (page = alloc_page()) == NULL) { 
                        return NULL;
        }
        set_page_ref(page, 1);
        pa = page2pa(page); // 得到分配页面(新建的PT所在页)对应的物理地址
        memset(KDDR(pa), 0, PGSIZE);
        cpde = pa |PTE_U | PTE_W | PTE_A; // 用新建PT的物理地址更新pde的内容

        // PDE_ADDR(cpde): 通过pde获得对应的PT的位置
        return (pte_t*)(KDDR(PDE_ADDR(cpde)) + PTX(la));
}

page_remove_pte()

//page_remove_pte - free an Page sturct which is related linear address la
//                - and clean(invalidate) pte which is related linear address la
//note: PT is changed, so the TLB need to be invalidate 
//  pgdir:  the kernel virtual base address of PDT
static inline void
page_remove_pte(pde_t *pgdir, uintptr_t la, pte_t *ptep) {
        if ((*ptep)&PTE_P){
                 struct Page *page = pte2page(*ptep);
                 page_ref_dec(page);
                 if (!(page->ref)){
                        free_page(page);
                }
                ptep = 0;
                tlb_invalidate(pgdir, la);
        }
}

​

pmm.c

void
pmm_init(void) {
    //We need to alloc/free the physical memory (granularity is 4KB or other size). 
    //So a framework of physical memory manager (struct pmm_manager)is defined in pmm.h
    //First we should init a physical memory manager(pmm) based on the framework.
    //Then pmm can alloc/free the physical memory. 
    //Now the first_fit/best_fit/worst_fit/buddy_system pmm are available.
    init_pmm_manager();

    // detect physical memory space, reserve already used memory,
    // then use pmm->init_memmap to create free page list
    page_init();

    //use pmm->check to verify the correctness of the alloc/free function in a pmm
    check_alloc_page();

    // create boot_pgdir, an initial page directory(Page Directory Table, PDT)
    boot_pgdir = boot_alloc_page();
    memset(boot_pgdir, 0, PGSIZE);
    boot_cr3 = PADDR(boot_pgdir);

    check_pgdir();

    static_assert(KERNBASE % PTSIZE == 0 && KERNTOP % PTSIZE == 0);

    // recursively insert boot_pgdir in itself
    // to form a virtual page table at virtual address VPT
    boot_pgdir[PDX(VPT)] = PADDR(boot_pgdir) | PTE_P | PTE_W;

    // map all physical memory to linear memory with base linear addr KERNBASE
    //linear_addr KERNBASE~KERNBASE+KMEMSIZE = phy_addr 0~KMEMSIZE
    //But shouldn't use this map until enable_paging() & gdt_init() finished.
    boot_map_segment(boot_pgdir, KERNBASE, KMEMSIZE, 0, PTE_W);

    //temporary map: 
    //virtual_addr 3G~3G+4M = linear_addr 0~4M = linear_addr 3G~3G+4M = phy_addr 0~4M     
    boot_pgdir[0] = boot_pgdir[PDX(KERNBASE)];

    enable_paging();

    //reload gdt(third time,the last time) to map all physical memory
    //virtual_addr 0~4G=liear_addr 0~4G
    //then set kernel stack(ss:esp) in TSS, setup TSS in gdt, load TSS
    gdt_init();

    //disable the map of virtual_addr 0~4M
    boot_pgdir[0] = 0;

    //now the basic virtual memory map(see memalyout.h) is established.
    //check the correctness of the basic virtual memory map.
    check_boot_pgdir();

    print_pgdir();

}

至今为止ucore的物理内存的使用情况分析

1.加电之前:

0XFFFF 0000 ~ 0XFFFF FFFF: ROM占用

我们知道,80386把ROM给编址在了物理内存最高端的64KB区域。Intel又通过某种映射机制,将存在ROM里的BIOS代码映射至物理内存1MB的最高端64KB区域,即0XF0000~0X100000这块区域。至于为什么要映射至这里,其实是为了向8086CPU兼容,详细请看(https://mp.csdn.net/editor/html/114376926)。

0X000F 0000 ~ 0X0010 0000: 从ROM映射的BIOS代码

 

2.装载bootloader:

BIOS做完计算机硬件自检和初始化后,会选择一个启动设备(例如软盘、硬盘、光盘等),并且读取该设备的第一扇区(即主引导扇区或启动扇区)到内存一个特定的地址0x7c00处,然后CPU控制权会转移到那个地址继续执行。至此BIOS的初始化工作做完了,进一步的工作交给了ucore的bootloader。

我们不用关心BIOS是怎么将bootloader加载进内存中的,我们只需要知道bootloader被BIOS加载至物理内存的0x7c00处。那bootloader是在哪个位置结束的呢?注意,BIOS是读取了一个扇区(512 bytes)的内容进入内存的,而这块内容就是bootloader,所以bootloader在内存中占据了512 bytes的大小,即2^9 B的大小,所以我们可以认为这块内存的结束位置是0x7c00 + 0x100 = 0x7d00。

0x0000 7c00 ~ 0x0000 7d00: bootloader占用

bootloader包括bootasm.S与bootmain.c两部分,为了让bootmain.c运行,bootasm.S需要初始化一块堆栈供bootmain.c使用。我们可以看到bootasm.S有这样一段代码

# Set up the stack pointer and call into C. The stack region is from 0--start(0x7c00)
    movl $0x0, %ebp
    movl $start, %esp
    call bootmain

通过将栈底寄存器ebp置0,栈顶寄存器esp置为start代码段的初位置(bootasm.S的首位置),即0x7c00,我们开辟了一块从物理内存最低位置0x0到0x7c00的堆栈区。

而这块堆栈区同时也作为之后装载的ucore的可用堆栈区。

0x0000 0000 ~ 0x0000 7c00: 分配给ucore的堆栈

3.装载ucore

当bootmain.c接手后,它就开始将ucore装载进物理内存中。

我们来看bootmain.c的装载过程

#define SECTSIZE        512
#define ELFHDR          ((struct elfhdr *)0x10000)

/* bootmain - the entry of bootloader */
void
bootmain(void) {
    // read the 1st page off disk
    readseg((uintptr_t)ELFHDR, SECTSIZE * 8, 0);

    // is this a valid ELF?
    if (ELFHDR->e_magic != ELF_MAGIC) {
        goto bad;
    }

    struct proghdr *ph, *eph;

    // load each program segment (ignores ph flags)
    ph = (struct proghdr *)((uintptr_t)ELFHDR + ELFHDR->e_phoff);
    eph = ph + ELFHDR->e_phnum;
    for (; ph < eph; ph ++) {
        readseg(ph->p_va & 0xFFFFFF, ph->p_memsz, ph->p_offset);
    }

    // call the entry point from the ELF header
    // note: does not return
    ((void (*)(void))(ELFHDR->e_entry & 0xFFFFFF))();

程序首先将一页大小的内容从硬盘读出,放入物理内存0x10000处(此时逻辑地址等于物理地址)。而这一页的内容,就是ucore(KERNEL)的ELF header。

所以从0x0001 0000处开始的、一页大小的物理内存空间存放的是ucore内核的ELF header。

0x0001 0000 ~ 0x0001 1000: KERNEL的ELF header

有了ELF header,就能通过e_phoff定位到硬盘文件中Program header的位置,就可以通过Program header提供的信息将KERNEL的各个段载入至指定的物理内存地址(再次强调,现在的 逻辑地址 = 线性地址 = 物理地址,其实program header里存储的还是逻辑(虚拟)地址。

注意,readseg的第一个参数值为(ph->p_va & 0x00FF FFFF)。这表明,对program header里指定的每一段的载入地址(为虚拟地址),只取他们的前24位的值作为物理地址载入内存。

我们用readelf看看kernel的program header

可以看到kernel被分为三段,分别对应TEXT, DATA和BSS段。

经过前面对VirtAddr的处理,代码段被载入至物理内存的0x0010 0000处,数据段被载入至0x0011 5000处。

由此可看出,这里ucore内核的虚拟地址(或可直接称为线性地址),与物理地址存在着这样一个映射关系:

Virtul Address  = Linear Address = Logical Address + 0xC000 0000 

bootmain.c将kernel全部载入了内存后,就把控制权交给了kernel。而kernel的大小我们也能用readelf得出

0x0010 0000 ~ end: ucore kernel

其中,end为kernel.ld文件中定义的bss段结束地址。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值