Ucore lab2

news2024/11/8 21:00:50

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

根据实验指导书中的实验执行流程概述,先了解分析ucore如何对物理内存进行管理,再完成实验练习。

在对物理内存进行管理之前,需要先进行物理内存布局的探测,探测得到的内存映射存放在e820map中。物理内存以页的形式进行管理,页的信息保存在Page结构中,而Page以链式结构保存在链表free_area_t中。对于物理内存的管理,ucore是通过定义一个pmm_manager实现的,其中包含了初始化需要管理的物理内存空间,初始化链表,内存空间分配释放等功能的函数。在内核启动后,会初始化pmm_manager,调用page_init,使用init_memmap将e820map中的物理内存纳入物理内存页管理(将空闲页初始化并加入链表),接下来就可以使用pmm_manager中的空间分配和释放等函数进行内存管理了。

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

对物理内存空间进行管理,首先要对物理内存布局进行探测。本实验中是在bootloader进入保护模式之前通过BIOS中断获取。基于INT 15h,通过e820h中断探测物理内存信息。并将探测到的物理内存对应的映射存放在物理地址0x8000处,定义e820map结构保存映射。在bootasm.S中有内存探测部分的代码,e820map的结构定义则在memlayout.h中。

//memlayout.h
struct e820map {
    int nr_map;
    struct {
        uint64_t addr;
        uint64_t size;
        uint32_t type;
    } __attribute__((packed)) map[E820MAX];
};
//bootasm.S
probe_memory:
    movl $0, 0x8000            #存放内存映射的位置
    xorl %ebx, %ebx
    movw $0x8004, %di          #0x8004开始存放map
start_probe:
    movl $0xE820, %eax         #设置int 15h的中断参数   
    movl $20, %ecx             #内存映射地址描述符的大小
    movl $SMAP, %edx
    int $0x15
    jnc cont                   #CF为0探测成功
    movw $12345, 0x8000
    jmp finish_probe
cont:
    addw $20, %di              #下一个内存映射地址描述符的位置
    incl 0x8000                #nr_map+1
    cmpl $0, %ebx              #ebx存放上次中断调用的计数值,判断是否继续进行探测
    jnz start_probe

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

物理内存是以页为单位进行管理的。探测可用物理内存空间的情况后,就可以建立相应的数据结构来管理物理内存页了。每个内存页使用一个Page结构来表示,Page的定义在memlayout.h中,如下:

//memlayout.h中的Page定义
struct Page {
    int ref;                        // 引用计数
    uint32_t flags;                 // 状态标记
    unsigned int property;          // 在first-fit中表示地址连续的空闲页的个数,空闲块头部才有该属性
    list_entry_t page_link;         // 双向链表
};
//状态
#define PG_reserved                 0       //表示是否被保留
#define PG_property                 1       //表示是否是空闲块第一页

初始情况下,空闲物理页可能都是连续的,随着物理页的分配与释放,大的连续内存空闲块会被分割为地址不连续的多个小连续内存空闲块。为了管理这些小连续内存空闲块,使用一个双向链表进行管理,定义free_area_t数据结构,包含一个list_entry结构的双向链表指针,记录当前空闲页个数的无符号整型:

typedef struct {
    list_entry_t free_list;         // 链表
    unsigned int nr_free;           // 空闲页个数
} free_area_t;

对以页为单位进行内存管理,还有两个问题需要解决。一个是找到管理页级物理内存空间所需的Page结构的内存空间的位置,另一个是要找到空闲空间开始的位置。

可以根据内存布局信息找到最大物理内存地址maxpa计算出页的个数,并计算出管理页的Page结构需要的空间。ucore的结束地址(即.bss段的结束地址,用全局变量end表示)以上的空间空闲,从这个位置开始存放Pages结构,而存放Pages结构的结束的位置以上就是空闲物理内存空间。将空闲的物理内存空间使用init_memmap()函数纳入物理内存管理器,部分代码如下:

//pmm.c中的page_init的部分代码
    npage = maxpa / PGSIZE;                                    //页数
    pages = (struct Page *)ROUNDUP((void *)end, PGSIZE);       //Page结构的位置
    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.物理内存空间管理的初始化

ucore中建立了一个物理内存页管理框架对内存进行管理,定义如下:

//pmm.h中的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);  // 释放自Base起的连续n个物理页
    size_t (*nr_free_pages)(void);                    // 返回剩余空闲页数
    void (*check)(void);                              // 用于检测分配释放是否正确
};

其中,init_memmap用于对页内存管理的Page的初始化,在上面提到的page_init负责确定探查到的物理内存块与对应的struct Page之间的映射关系,Page的初始化工作由内存管理器的init_memmap()来完成。而init则用于初始化已定义好的free_area_t结构的free_area。

在内核初始化的kern_init中会调用pmm_init(),pmm_init()中会调用init_pmm_manager()进行初始化,使用默认的物理内存页管理函数,即使用default_pmm.c中定义的default_init等函数进行内存管理,本实验中需要实现的分配算法可以直接通过修改这些函数进行实现。

//pmm.c中的init_pmm_manager()定义,在pmm_init()中调用,在kernel_init()中初始化
static void
init_pmm_manager(void) {
    pmm_manager = &default_pmm_manager;             //pmm_manager指向default_pmm_manager
    cprintf("memory management: %s\n", pmm_manager->name);
    pmm_manager->init();
} 
//default_pmm.c中的default_init()
static void
default_init(void) {
    list_init(&free_list);                          //初始化链表
    nr_free = 0;
}
//init_memmap
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));                    //检查是否为保留页
        //property设置为0,只有空闲块的第一页使用该变量,标志位清0
        p->flags = p->property = 0;                 
        set_page_ref(p, 0);                         //引用计数为0
    }
    base->property = n;                             //设置第一页的property
    SetPageProperty(base);
    nr_free += n;                                   //空闲页+n
    list_add(&free_list, &(base->page_link));       //将空闲块加入列表
}

4.first-fit算法的实现

在pmm_manager完成链表的初始化,page_init完成页的初始化后,就可以使用pmm_manager的函数进行内存空间管理了。该练习中使用默认的pmm_manager进行管理,其中的default_init和default_init_memmap可以直接使用,只需要修改default_alloc_pages,default_free_pages,实现first-fit算法以及内存空间释放。

使用的函数及宏定义

使用的链表相关操作和宏定义如下。ucore对空闲内存空间管理使用的链表与常见的链表不同,链表只包含了前后节点的信息,而链表包含在Page结构中,这样做是为了实现链表通用性(c语言中并不支持泛型功能,因此采用这种方式)。而通过链表节点获取节点数据是通过宏定义le2page实现的。链表定义和部分相关函数如下:

//双向链表的定义
struct list_entry {
    struct list_entry *prev, *next;
};
typedef struct list_entry list_entry_t;
 //返回下一个节点
static inline list_entry_t *
list_next(list_entry_t *listelm) {
    return listelm->next;
}
//删除当前节点
static inline void
list_del(list_entry_t *listelm) { 
    __list_del(listelm->prev, listelm->next);
}
//前插节点,还有类似的list_add_after
static inline void
list_add_before(list_entry_t *listelm, list_entry_t *elm) {
    __list_add(elm, listelm->prev, listelm);
}

le2page通过page_link地址减去其相对于Page结构的偏移,实现从链表节点找到对应的数据。

#define le2page(le, member)                 
    to_struct((le), struct Page, member
#define to_struct(ptr, type, member)                               
    ((type *)((char *)(ptr) - offsetof(type, member))))

还有一些其他函数需要使用,以设置Page的信息。

//pmm.h中定义的设置引用计数的函数
static inline void
set_page_ref(struct Page *page, int val) {
    page->ref = val;
}
//memlayou.h中的关于页标志位设置的宏定义
#define SetPageReserved(page)       set_bit(PG_reserved, &((page)->flags))
#define ClearPageReserved(page)     clear_bit(PG_reserved, &((page)->flags))
#define PageReserved(page)          test_bit(PG_reserved, &((page)->flags))
#define SetPageProperty(page)       set_bit(PG_property, &((page)->flags))
#define ClearPageProperty(page)     clear_bit(PG_property, &((page)->flags))
#define PageProperty(page)          test_bit(PG_property, &((page)->flags))

使用first-fit实现default_alloc_pages

使用first-fit实现空闲空间分配,只需要遍历空闲链表,找到合适的块大小,重新设置标志位并从链表中删除该页,如果找到的块大于需要的大小,则需要进行分割,最后更新空闲页数,返回分配好的页块的地址。

static struct Page *
default_alloc_pages(size_t n) {
    assert(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);
        if (p->property >= n) {
            page = p;
            break;
        }
    }
    if (page != NULL) {
        ClearPageProperty(page);               //page已被分配
        //如果空闲块过大则进行分割
        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;    
    }
    return page;
}

default_free_pages

释放空间,首先确定需要释放的n都页是未被保留且非空闲的页,然后将这些页的标志位和引用计数清零,并将释放空间的第一页的property设置为n,即空闲块共有n页。接下来完成空闲块的合并,对于相邻的空间,将高地址的块合并到低地址的空闲块中,删除被合并的块,并重新设置空闲块的第一页的property,最后由于空闲链表按地址空间由低到高排列空闲块,还需要找到插入的位置。

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);               //引用计数清0
    }
    SetPageProperty(base);
    base->property = n;                   //空闲块第一页,property=n
    //空闲块的合并
    list_entry_t *le = list_next(&free_list);
    while (le != &free_list) {
        p = le2page(le, page_link);
        le = list_next(le);
        if (base + base->property == p) {
            base->property += p->property;
            p->property=0;                //不再是空闲块第一页,property清0
            SetPageProperty(base);        //设置为空闲块第一页
            list_del(&(p->page_link));
        }
        else if (p + p->property == base) {
            p->property += base->property;
            base->property=0;
            SetPageProperty(p);            //设置为空闲块第一页
            list_del(&(base->page_link));
            base=p;                        //更新空闲块第一页
        }
    }
    le = list_next(&free_list);
    //找到插入位置
     while (le != &free_list) 
    {
        p = le2page(le, page_link);
        if (base + base->property <= p)
        {
            break;
        }
        le = list_next(le);
    }
    //将base插入到正确位置
    list_add_before(le, &(base->page_link));
    nr_free += n;
}

改进空间

每次查找链表都需要进行遍历,时间复杂度较高,且在空闲链表开头会产生许多小的空闲块,仍然有优化空间。

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

1.段页式管理

在保护模式中,内存地址分成三种:逻辑地址、线性地址和物理地址。逻辑地址即是程序指令中使用的地址,物理地址是实际访问内存的地址。段式管理的映射将逻辑地址转换为线性地址,页式管理的映射将线性地址转换为物理地址。

ucore中段式管理仅为一个过渡,逻辑地址与线性地址相同。而页式管理是通过二级页表实现的,地址的高10位为页目录索引,中间10位为页表索引,低12位为偏移(页对齐,低12位为0)。一级页表的起始物理地址存放在 boot_cr3中。

2.页目录项和页表项的组成

问题一:请描述页目录项(Pag Director Entry)和页表(Page Table Entry)中每个组成部分的含义和以及对ucore而言的潜在用处

页目录项的组成

  • 前20位表示该PDE对应的页表起始位置
  • 第9-11位保留给OS使用
  • 第8位可忽略
  • 第7位用于设置Page大小,0表示4KB
  • 第6位为0
  • 第5位表示该页是否被写过
  • 第4位表示是否需要进行缓存
  • 第3位表示CPU是否可直接写回内存
  • 第2位表示该页是否可被任何特权级访问
  • 第1位表示是否允许读写
  • 第0位为该PDE的存在位

页表项的组成

  • 前20位表示该PTE指向的物理页的物理地址
  • 第9-11位保留给OS使用
  • 第8位表示在 CR3 寄存器更新时无需刷新 TLB 中关于该页的地址
  • 第7位恒为0
  • 第6位表示该页是否被写过
  • 第5位表示是否可被访问
  • 第4位表示是否需要进行缓存
  • 第0-3位与页目录项的0-3位相同

3.页访问异常的处理

问题二:如果ucore执行过程中访问内存,出现了页访问异常,请问硬件要做哪些事情?
当发生页访问异常时,硬件需要将发生错误的地址存放在cr2寄存器中,向栈中压入EFLAGS,CS,EIP等,如果异常发生在用户态,还需要进行特权级切换,最后根据中断描述符找到中断服务例程,接下来由中断服务例程处理该异常。

4.实现get_pte寻找页表项

练习二要求实现get_pte函数,使该函数能够找到传入的线性地址对应的页表项,返回页表项的地址。为了完成该函数,需要了解ucore中页式管理的相关函数及定义。

//PDX(la):la为线性地址,该函数取出线性地址中的页目录项索引
#define PDX(la) ((((uintptr_t)(la)) >> PDXSHIFT) & 0x3FF)
//取出线性地址中的页表项索引
#define PTX(la) ((((uintptr_t)(la)) >> PTXSHIFT) & 0x3FF)
//KADDR(pa):返回物理地址pa对应的虚拟地址
#define KADDR(pa) {...}
//set_page_ref(page,1):设置该页引用次数为VAL
static inline void
set_page_ref(struct Page *page, int val) {
    page->ref = val;
}
//page2pa:找到page结构对应的页的物理地址
static inline uintptr_t
page2pa(struct Page *page) {
    return page2ppn(page) << PGSHIFT;
}
//alloc_page():分配一页
#define alloc_page() alloc_pages(1)
//页目录项、页表项的标志位
#define PTE_P           0x001                   // 存在位
#define PTE_W           0x002                   // 是否可写
#define PTE_U           0x004                   // 用户是否可访问
//页目录项和页表项类型
typedef uintptr_t pte_t;
typedef uintptr_t pde_t;
//以下两个函数用于取出页目录项中的页表地址,取出页表项中的页地址
#define PDE_ADDR(pde)   PTE_ADDR(pde)
#define PTE_ADDR(pte)   ((uintptr_t)(pte) & ~0xFFF)

需要完成的get_pte函数原型如下,其中pgdir是一级页目录的起始地址,la为线性地址,creat表示是否可以为页表分配新的页。

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

取出线性地址对应的页表项,首先在页目录中找到对应的页目录项,并判断是否有效(二级页目录是否存在)。如果不存在则根据create判断是否需要创建新的一页存放页表,如果需要则调用alloc_page分配新的一页,将这一页的地址结合标志位设置为页目录项。最后返回页表项的线性地址,使用PDE_ADDR取出页目录项中保存的页表地址,再加上使用PTX从线性地址取出的页表索引,就找到了页表项的位置,使用KADDR转换为线性地址返回(先将页表地址转换为线性地址再加索引同样可行),注意类型转换。

pte_t *get_pte(pde_t *pgdir, uintptr_t la, bool create) {
    pde_t *pdep = &pgdir[PDX(la)];              //找到页目录项
    if (!*pdep&PTE_P) {                         //判断二级页表是否存在
        if(!create) return NULL;                //create=0则直接返回
        else{                         
            struct Page* page=alloc_page();     //分配一页用于存放二级页表
            if(page==NULL) return NULL;
            set_page_ref(page,1);               //引用计数设为1
            uintptr_t pte_pa=page2pa(page);     //分配的页物理地址
            memset(KADDR(pte_pa),0,PGSIZE);     //清除页面内容
            *pdep=pte_pa | PTE_P | PTE_W | PTE_U;   
        }
    }
    return ((pte_t*)KADDR(PDE_ADDR(*pdep)))+PTX(la);     // 返回页表项的地址
}

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

1.Page与页目录项和页表项的关系

问题一:数据结构Page的全局变量(其实是一个数组)的每一项与页表中的页目录项和页表项有无对应关系?如果有,其对应关系是啥?

当页目录项与页表项均有效时,有对应关系。每个页目录项记录一个页表的位置,每个页表项则记录一个物理页的位置,而Page变量保存的就是物理页的信息,因此每个有效的页目录项和页表项,都对应了一个page结构,即一个物理页的信息。

2.实现虚拟地址与物理地址相等

问题二:如果希望虚拟地址与物理地址相等,则需要如何修改lab2,完成此事?

由附录可知,在lab1中,虚拟地址=线性地址=物理地址,ucore的起始虚拟地址(也即物理地址)从0x100000开始。而在lab2中建立了从虚拟地址到物理地址的映射,ucore的物理地址仍为0x100000,但虚拟地址变为了0xC0100000,即最终建立的映射为:virt addr = linear addr = phy addr + 0xC0000000。

 //memlayout.h给出了直观的映射关系
 *     4G ------------------> +---------------------------------+
 *                            |                                 |
 *                            |         Empty Memory (*)        |
 *                            |                                 |
 *                            +---------------------------------+ 0xFB000000
 *                            |   Cur. Page Table (Kern, RW)    | RW/-- PTSIZE
 *     VPT -----------------> +---------------------------------+ 0xFAC00000
 *                            |        Invalid Memory (*)       | --/--
 *     KERNTOP -------------> +---------------------------------+ 0xF8000000
 *                            |                                 |
 *                            |    Remapped Physical Memory     | RW/-- KMEMSIZE
 *                            |                                 |
 *     KERNBASE ------------> +---------------------------------+ 0xC0000000
 *                            |                                 |
 *                            |                                 |
 *                            |                                 |
 *                            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

只要取消这个映射,就可以实现虚拟地址和物理地址相等,将ld工具形成的ucore的虚拟地址修改为0x100000,就可以取消映射。

ENTRY(kern_entry)
SECTIONS {
            /* Load the kernel at this address: "." means the current address */
            . = 0xC0100000;        //修改为0x100000就可以实现虚拟地址=物理地址
            .text : {
                        *(.text .stub .text.* .gnu.linkonce.t.*)
            }

还需要在memlayout.h中将KERNBASE即虚拟地址基址设置为0,并关闭entry.S中对页表机制的开启。

//memlayout.h中定义的KERNBASE
#define KERNBASE 0x0
//页表机制开启
    # 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        //将这句注释掉

pmm_init中的check_pgdir和check_boot_pgdir都假设kernbase不为0,对线性地址为0的地址进行页表查询等,因此会产生各种错误,可以将这两个函数注释掉。

3.实现page_remove_pte释放虚拟页并取消二级映射

本练习中需要完成的是page_remove_pte函数,该函数将传入的虚拟页释放,取消二级页表的映射,并且需清除TLB中对应的项。原型如下:

page_remove_pte(pde_t *pgdir, uintptr_t la, pte_t *ptep) 

使用的相关函数定义如下:

//pte2page:找到页表项对应的页
struct Page *page pte2page(*ptep)
//free_page:释放页
free_page(page)
//page_ref_dec:引用计数-1
page_ref_dec(page)
//tlb_invalidate:清除TLB
tlb_invalidate(pde_t *pgdir, uintptr_t la) 

对于传入的页表项,首先要判断其是否有效,如果有效,将引用计数-1,当引用计数为0时释放该页,再清0二级页表映射,使用tlb_invalidate清除对应的TLB项。最终实现如下:

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);                         //引用计数-1
        if(page->ref==0) free_page(page);           //引用计数为0释放该页
        *ptep=0;                                    //清除二级页表映射
        tlb_invalidate(pgdir,la);                   //修改TLB
        return;
    }
    else return;
}

Challenge:buddy system(伙伴系统)分配算法

challenge没有自己完成ucore中的实现,主要是读懂和理解参考资料中的伙伴系统。

1.伙伴系统分配算法

伙伴系统是一种采用二分的方式分割和合并空闲空间的内存分配算法。空闲空间被视为2^n的空间,当有内存空间分配请求时,将空闲空间一分为二,直到刚好可以满足请求大小。在空间释放时,分配程序会检查该块同大小的伙伴块是否空闲,如果是则可以进行合并,并继续上溯,直到完成全部可能的合并。

分配器使用伙伴系统,是通过数组形式的二叉树实现的。二叉树的节点标记相应的内存块是否使用,通过这些标记进行块的分离与合并。二叉树的情况如下图(总大小为16),其中节点数字为在数组中的索引:
在这里插入图片描述

2.伙伴系统的实现

面是对实验指导书中给出的伙伴系统的分析。

首先是数据结构和一些宏定义,主要是伙伴系统定义,计算节点等:

struct buddy2 {
  unsigned size;//表明物理内存的总单元数
  unsigned longest[1]; //二叉树的节点标记,表明对应内存块的空闲单位
};
#define LEFT_LEAF(index) ((index) * 2 + 1)//左子树节点的值
#define RIGHT_LEAF(index) ((index) * 2 + 2)//右子树节点的值
#define PARENT(index) ( ((index) + 1) / 2 - 1)//父节点的值
#define IS_POWER_OF_2(x) (!((x)&((x)-1)))//x是不是2的幂
#define MAX(a, b) ((a) > (b) ? (a) : (b))//判断a,b大小
#define ALLOC malloc//申请内存
#define FREE free//释放内存

在分配时,分配的空间大小必须满足需要的大小,且为2的幂次方,以下函数用于找到合适的大小:

static unsigned fixsize(unsigned size) {//找到大于等于所需内存的2的倍数
  size |= size >> 1;
  size |= size >> 2;
  size |= size >> 4;
  size |= size >> 8;
  size |= size >> 16;
  return size+1;
}

接下来是分配器的初始化及销毁。初始化传入的参数是需要管理的内存空间大小,且这个大小应该是2的幂次方。在函数中node_size用于计算节点的大小,每次除2,初始化每一个节点。

struct buddy2* buddy2_new( int size ) {//初始化分配器
  struct buddy2* self;
  unsigned node_size;//节点所拥有的内存大小
  int i;

  if (size < 1 || !IS_POWER_OF_2(size))
    return NULL;

  self = (struct buddy2*)ALLOC( 2 * size * sizeof(unsigned));
  self->size = size;
  node_size = size * 2;

  for (i = 0; i < 2 * size - 1; ++i) {
    if (IS_POWER_OF_2(i+1))
      node_size /= 2;
    self->longest[i] = node_size;
  }
  return self;
}

void buddy2_destroy( struct buddy2* self) {
  FREE(self);
}

内存分配的实现如下,传入分配器,需要分配的空间大小,首先判断是否可以进行分配,并将空间大小调整为2的幂次方,然后进行分配。分配的过程为遍历寻找合适大小的节点,将找到的节点大小清0表示以被占用,并且需要更新父节点的值,最后返回的值为所分配空间相对于起始位置的偏移。

int buddy2_alloc(struct buddy2* self, int size) {
  unsigned index = 0;        //节点在数组的索引
  unsigned node_size;
  unsigned offset = 0;

  if (self==NULL)            //无法分配
    return -1;
  if (size <= 0)             
    size = 1;
  else if (!IS_POWER_OF_2(size))//调整大小为2的幂次方
    size = fixsize(size);
  if (self->longest[index] < size)//可分配内存不足
    return -1;
  //从根节点开始向下寻找合适的节点
  for(node_size = self->size; node_size != size; node_size /= 2 ) {
    if (self->longest[LEFT_LEAF(index)] >= size)
      index = LEFT_LEAF(index);
    else
      index = RIGHT_LEAF(index);//左子树不满足时选择右子树
  }
  self->longest[index] = 0;     //将节点标记为已使用
  offset = (index + 1) * node_size - self->size;//计算偏移量
    //更新父节点
  while (index) {
    index = PARENT(index);
    self->longest[index] = 
      MAX(self->longest[LEFT_LEAF(index)], self->longest[RIGHT_LEAF(index)]);
  }
  return offset;
}

内存释放时,先自底向上寻找已被分配的空间,将这块空间的大小恢复,接下来就可以匹配其大小相同的空闲块,如果块都为空闲则进行合并。

void buddy2_free(struct buddy2* self, int offset) {
  unsigned node_size, index = 0;
  unsigned left_longest, right_longest;
  //判断请求是否出错
  assert(self && offset >= 0 && offset < self->size);
  node_size = 1;
  index = offset + self->size - 1;
  //寻找分配过的节点
  for (; self->longest[index] ; index = PARENT(index)) {
    node_size *= 2;
    if (index == 0)                //如果节点不存在
      return;
  }
  self->longest[index] = node_size;//释放空间
  //合并
  while (index) {
    index = PARENT(index);
    node_size *= 2;

    left_longest = self->longest[LEFT_LEAF(index)];
    right_longest = self->longest[RIGHT_LEAF(index)];

    if (left_longest + right_longest == node_size) //如果可以则合并
      self->longest[index] = node_size;
    else
      self->longest[index] = MAX(left_longest, right_longest);
  }
}

以上就是参考资料中给出的伙伴系统的实现(另外还有两个函数分别用于返回当前节点大小和打印内存状态),在ucore中实现伙伴系统的原理相同,但需要对具体的页进行处理分配以及释放,完成对应的buddy.h头文件和buddy.c文件后,修改pmm.c中的init_pmm_manager,将默认使用的分配器修改为伙伴系统分配器就可以在ucore中实现伙伴系统了。

实验总结

重要知识点

  • 分页机制
  • 空闲链表
  • 内存分配算法(first-fit)
  • 多级页表
  • 虚拟地址的转换
  • 页表项与页目录项的组成

lab2主要是关于物理内存管理机制的实现,是OS原理中内存空间管理的具体实现,细节上更加复杂,且ucore通过创建一个物理内存管理框架进行内存管理,在OS原理知识中未涉及到。通过lab2还学习了物理内存空间探测,段页机制的配合等知识,在物理内存空间管理方面比OS原理的知识更加完整深入。关于内存空间分配算法,lab2中只实现了首次匹配算法,没有涉及下次匹配,最优匹配等其他分配策略。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/420830.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

[LeetCode解题报告] 1157. 子数组中占绝大多数的元素

[LeetCode解题报告] 1157. 子数组中占绝大多数的元素 一、 题目1. 题目描述2. 原题链接二、 解题报告1. 思路分析2. 复杂度分析3. 代码实现三、 本题小结四、 参考链接一、 题目 1. 题目描述 2. 原题链接 链接: 1157. 子数组中占绝大多数的元素 二、 解题报告 1. 思路分析 …

python+requests的接口自动化测试框架实例详解教程

目录 前言 一、环境准备 二、设计框架结构 三、实现框架功能 四、执行测试用例 五、总结 前言 Python是一种简单易学、功能强大的编程语言&#xff0c;广泛应用于各种软件开发和测试场景中。requests是Python中流行的HTTP库&#xff0c;支持发送HTTP请求和处理HTTP响应&a…

【c语言】多维数组原理

创作不易&#xff0c;本篇文章如果帮助到了你&#xff0c;还请点赞支持一下♡>&#x16966;<)!! 主页专栏有更多知识&#xff0c;如有疑问欢迎大家指正讨论&#xff0c;共同进步&#xff01; 给大家跳段街舞感谢支持&#xff01;ጿ ኈ ቼ ዽ ጿ ኈ ቼ ዽ ጿ ኈ ቼ ዽ ጿ…

话说~~ HTTP协议请求的工作流程 (Web服务请求过程)最细详解

目录 文章导入 &#xff1a; 概述 &#xff1a; 详解过程 &#xff1a; DNS域名解析 &#xff1a; DNS 域名解析过程分析 &#xff1a; TCP连接搭建 &#xff1a; 等待TCP队列 建立TCP连接 发起 HTTP 请求 &#xff1a; # 哪是如何进行 HTTP 请求的呢 &#…

实验架构的部署

目录 实验要求 实验环境 1、部署静态页面 2、部署负载均衡 3、搭建动态网页 4、 nginx反向代理 5、部署NFS 7、mysql 安装mysql 安装mha 准备主从复制 开启mha 架构的部署 实验要求 完成用户访问时虚拟IP由LVS负责高可用&#xff0c;静态交给nginx处理&#xff0c;…

【工具篇】Spring Boot 整合阿里云短信-SMS

短信服务-SMS 短信服务&#xff08;Short Message Service&#xff09;是广大企业客户快速触达手机用户所优选使用的通信能力&#xff0c;分为国内短信服务和国际/港澳台短信服务。通过 API/SDK、控制台调用短信发送能力&#xff0c;将指定信息发送至国内或境外手机号码。 应…

<数据结构>NO1.算法的时空复杂度

文章目录&#x1f6a9;算法效率算法复杂度&#x1fa85;时间复杂度大O的渐进表示法常见的时间复杂度举例&#x1fa85;空间复杂度大O的渐进表示法常见的空间复杂度举例&#x1f5ef;️常见复杂度对比&#x1f5ef;️&#x1f6a9;算法效率 算法是一个被设计好的&#xff0c;计…

python 读写txt方法

​​​​​​​ 1. Python支持在程序中读写 txt文件。这里有两种方式&#xff1a; 方式一&#xff1a;使用 python内置函数&#xff0c;该函数将一个字符串的长度转换为与这个字符串长度相关的值。 例如&#xff1a;" readme"&#xff08;"r&#xff09;。 prin…

数据结构---递归转化为非递归

递归转化为非递归前言快速排序非递归归并排序的非递归前言 为什么要学习非递归写法呢&#xff1f; 当我们在用递归实现一个程序的时候&#xff0c;要考虑一个问题&#xff0c;这个程序用递归去实现&#xff0c;当数据量庞大的时候&#xff0c;会不会造成栈溢出(STACK OVERFLOW…

学习风`宇blog的websocket模块

文章目录后端代码引入依赖WebSocketConfigWebSocketServiceImpl分析tb_chat_record表WebSocketServiceImplChatConfigurator聊天消息ChatTypeEnumsWebsocketMessageDTO后端 代码 引入依赖 仅需引入以下依赖 <!-- websocket依赖 --> <dependency><groupId>…

ACM8629 立体声50W/100W单声道I2S数字输入D类音频功放IC

概述 ACM8629 一款高度集成、高效率的双通道数字输入功放。供电电压范围在4.5V-26.4V,数字接口电源支持3.3V 。在4 欧负载&#xff0c;BTL模式下输出功率可以到250W1%THDN&#xff0c;在2欧负载&#xff0c;PBTL模式下单通道可以输出1100W 1%THDN. ACM8629采用新型PWM脉宽调制架…

全国青少年软件编程(Scratch)等级考试二级考试真题2023年3月——持续更新.....

一、单选题(共25题,共50分) 1. 小猫的程序如图所示,积木块的颜色与球的颜色一致。点击绿旗执行程序后,下列说法正确的是?( ) A.小猫一直在左右移动,嘴里一直说着“抓到了”。 B.小猫会碰到球,然后停止。 C.小猫一直在左右移动,嘴里一直说着“别跑” D.小猫会碰到球,…

2023MatherCup杯三人小队手搓!(C 题 电商物流网络包裹应急调运与结构优化问题)

一个不知名大学生&#xff0c;江湖人称菜狗original author: Jacky LiEmail : 3435673055qq.com Time of completion&#xff1a;2023.4.16 Last edited: 2023.4.16 实际完成时间&#xff1a;2023/4/17 0:52 Mathematical modeling Author: HandSome Wang、BigTall Hu、Jacky L…

一个Email简单高效处理.Net开源库

推荐一个可处理电子邮件消息开源库&#xff0c;可用于消息解析、消息创建、消息修改和消息发送等功能。 项目简介 这是一个基于C#开发的&#xff0c;针对MIME&#xff08;多用途邮件扩展&#xff09;消息创建与解析&#xff0c;该项目简单易用、可用于消息解析、消息创建、消…

【Pytorch】神经网络搭建

在之前我们学习了如何用Pytorch去导入我们的数据和数据集&#xff0c;并且对数据进行预处理。接下来我们就需要学习如何利用Pytorch去构建我们的神经网络了。 目录 基本网络框架Module搭建 卷积层 从conv2d方法了解原理 从Conv2d方法了解使用 池化层 填充层 非线性层 …

Node实现 Socket 通信

socket 通信流程 Socket通信&#xff0c;首先要知道 Socket 是什么&#xff0c;就是网络上的两个程序通过一个双向的通信连接实现数据的交换&#xff0c;这个连接的一端被称为 socket &#xff0c;举一个简单的例子就是两个人在线上进行聊天&#xff0c;即线上通信&#xff0c…

充电桩检测设备TK4860E交流充电桩检定装置

产品特点 充电桩检测设备内置5.28 kW单相交流负载&#xff0c;无需携带额外负载进行测试。 宽动态范围测量技术&#xff0c;避免充电桩输出波动引起的测量风险。 ms级电能刷新速度&#xff0c;减少充电桩与标准仪器在非同步累积电能过程中引入的误差&#xff0c;提高累积电能…

【C++11那些事儿(二)】

文章目录一、新的类功能1.1 默认成员函数1.2 强制生成默认函数的关键字default1.3 禁止生成默认函数的关键字delete二、lambda表达式2.1 语法2.2 捕捉列表说明2.3 函数对象与lambda表达式一、新的类功能 1.1 默认成员函数 原来C类中&#xff0c;有6个默认成员函数&#xff1a…

性能测试总结-根据工作经验总结还比较全面

性能测试总结性能测试理论性能测试的策略基准测试负载测试稳定性测试压力测试并发测试性能测试的指标响应时间并发数吞吐量资源指标性能测试流程性能测试工具JMeter基本使用元件构成线程组jmeter的分布式使用jmeter测试报告常用插件性能测试的计算1.根据请求数明细数据计算满足…

【MySQL】多表查询

文章目录&#x1f389;多表查询&#x1f388;3.1 内连接查询&#x1f388;3.2 外连接查询&#x1f388;3.3 子查询最后说一句&#x1f389;多表查询 &#x1f388;3.1 内连接查询 语法 -- 隐式内连接 SELECT 字段列表 FROM 表1,表2… WHERE 条件;-- 显示内连接 SELECT 字段列…