首页 > 系统相关 >手写简易操作系统(二十)--实现堆内存管理

手写简易操作系统(二十)--实现堆内存管理

时间:2024-04-02 18:31:04浏览次数:12  
标签:arena block -- void 内存 手写 pool desc

前情提要

前面我们实现了 0x80 中断,并实现了两个中断调用,getpidwrite,其中 write 还由于没有实现文件系统,是个残血版,这一节我们实现堆内存管理。

一、arena

在计算机科学中,“arena” 内存管理通常指的是一种内存分配和管理技术,它通常用于动态内存分配和释放。在这种管理方式下,程序会从系统中获取一大块内存,然后按需分割和管理这块内存,以满足程序中对内存的动态需求。

“arena” 内存管理的优点在于减少了频繁向操作系统请求内存的开销。相比之下,每次调用 malloc 或 free 都需要涉及到系统调用,而 “arena” 内存管理可以减少这种开销,因为它在一开始就获取了一大块内存,然后由程序自己来管理这块内存的分配和释放。

一般来说,“arena” 内存管理会维护一个或多个内存池(memory pool),从中分配内存给程序使用。内存池可以分割成多个固定大小的块,或者按需分配不同大小的内存块。内存分配器会跟踪这些内存块的使用情况,并确保在程序需要时能够高效地分配和释放内存。

许多现代的内存分配器都采用了 “arena” 内存管理的思想,以提高内存分配和释放的效率。这种技术在实际应用中有助于减少内存碎片化、提高性能,并且减少了向操作系统请求内存的次数。

换个说法就是,我准备好很多小块的内存,你需要哪种规格,我给你哪种规格。大于1KB,直接给你通过页框给你分配,小于16B,直接给16B。加入你需要53B,但是53不是一种规格,那么给你64B,64是一种规格,也刚刚好够你使用

1.1、arena数据结构

/* 内存块 */
struct mem_block {
    struct list_elem free_elem;
};

/* 内存块描述符 */
struct mem_block_desc {
    uint32_t block_size;		 // 内存块大小
    uint32_t blocks_per_arena;	 // 本arena中可容纳此mem_block的数量.
    struct list free_list;    	 // 目前可用的mem_block链表
};

/* 内存仓库arena元信息 */
struct arena {
    struct mem_block_desc* desc; // 此arena关联的mem_block_desc
    uint32_t cnt;                // large为ture时,cnt表示的是页框数。否则cnt表示空闲mem_block数量
    bool large;
};

struct mem_block_desc k_block_descs[DESC_CNT];	// 内核内存块描述符数组,其中规格,最小16Byte

首先是内存块描述符,内存块描述符描述的是我们有多少种规格的内存大小分类,这里我们设计的是七种。所以最后我们也生成了一个全局的内存块描述符数组。

内存块描述符中包含三个属性,一个是内存块的大小(16B,32B,64B…),另一个是arena中可以容纳的内存块数量,这个值是一个定值,比如我们的内存块为16B,arena结构体占一定的空间,剩下的空间全部被分为16B的内存块。最后是空闲的内存块的链表,这个链表链接的就是arena仓库中的内存块

arena也包含三个信息,arena关联的内存块描述符的指针,这个是为了标识这个arena的存储的类型,还有就是cnt数量,cnt数量在large为true时表示的页框数,这个很好理解,如果大于1024B的话,那么直接分配页框了,就将large标识为true。

image-20240329195055740

1.2、arena初始化

/* 内核内存块描述符数组初始化 */
void block_desc_init(struct mem_block_desc* desc_array) {
    uint32_t block_size = 16;
    // 初始化每个mem_block_desc描述符
    for (uint32_t desc_idx = 0; desc_idx < DESC_CNT; desc_idx++) {
        desc_array[desc_idx].block_size = block_size;
        // 初始化arena中的内存块数量
        desc_array[desc_idx].blocks_per_arena = (PG_SIZE - sizeof(struct arena)) / block_size;
        // 初始化每个描述符的空闲块链表
        list_init(&(desc_array[desc_idx].free_list));
        // 更新为下一个规格内存块
        block_size *= 2;
    }
}

/* 返回arena中第idx个内存块的地址 */
static struct mem_block* arena2block(struct arena* a, uint32_t idx) {
    return (struct mem_block*)((uint32_t)a + sizeof(struct arena) + idx * a->desc->block_size);
}

/* 返回内存块b所在的arena地址 */
static struct arena* block2arena(struct mem_block* b) {
    return (struct arena*)((uint32_t)b & 0xfffff000);
}

内核内存块描述符数组是在内核中是准备好了的,这里我们只是初始化。

二、malloc

/* 在堆中申请size字节内存 */
void* sys_malloc(uint32_t size) {
    enum pool_flags PF;        // 线程标识
    struct pool* mem_pool;     // 内核内存池或者用户内存池
    uint32_t pool_size;        // 内存池大小
    struct mem_block_desc* descs; // 内存块描述符
    struct task_struct* cur_thread = running_thread();

    if (cur_thread->pgdir == NULL) {     // 若为内核线程
        PF = PF_KERNEL;
        pool_size = kernel_pool.pool_size;
        mem_pool = &kernel_pool;
        descs = k_block_descs;
    }
    else {				                 // 若为用户线程
        PF = PF_USER;
        pool_size = user_pool.pool_size;
        mem_pool = &user_pool;
        descs = cur_thread->u_block_desc;
    }

    if (!(size > 0 && size < pool_size)) { // 若申请的内存不在内存池容量范围内则直接返回NULL
        return NULL;
    }

    struct arena* a;         // 内存仓库元信息
    struct mem_block* b;     // 内存块
    lock_acquire(&mem_pool->lock);

    if (size > 1024) {
        // 超过最大内存块1024, 就分配页框,需要的页框数为申请内存大小+内存块元信息
        uint32_t page_cnt = DIV_ROUND_UP(size + sizeof(struct arena), PG_SIZE);

        a = malloc_page(PF, page_cnt);

        if (a != NULL) {
            memset(a, 0, page_cnt * PG_SIZE);	 // 将分配的内存清0  

            /* 对于分配的大块页框,将desc置为NULL, cnt置为页框数,large置为true */
            a->desc = NULL;
            a->cnt = page_cnt;
            a->large = true;
            lock_release(&mem_pool->lock);
            return (void*)(a + 1);		 // 跨过arena大小,把剩下的内存返回
        }
        else {
            lock_release(&mem_pool->lock);
            return NULL;
        }
    }
    else {
        // 若申请的内存小于等于1024,可在各种规格的mem_block_desc中去适配
        uint8_t desc_idx;

        // 从内存块描述符中匹配合适的内存块规格
        for (desc_idx = 0; desc_idx < DESC_CNT; desc_idx++) {
            if (size <= descs[desc_idx].block_size) {  // 从小往大后,找到后退出
                break;
            }
        }

        // 若mem_block_desc的free_list中已经没有可用的mem_block, 就创建新的arena提供mem_block
        if (list_empty(&descs[desc_idx].free_list)) {
            // 分配1页框做为arena
            a = malloc_page(PF, 1);
            if (a == NULL) {
                lock_release(&mem_pool->lock);
                return NULL;
            }
            memset(a, 0, PG_SIZE);

            // 对于分配的小块内存,将desc置为相应内存块描述符,cnt置为此arena可用的内存块数,large置为false
            a->desc = &descs[desc_idx];
            a->large = false;
            a->cnt = descs[desc_idx].blocks_per_arena;
            uint32_t block_idx;

            enum intr_status old_status = intr_disable();

            // 开始将arena拆分成内存块,并添加到内存块描述符的free_list中
            for (block_idx = 0; block_idx < descs[desc_idx].blocks_per_arena; block_idx++) {
                b = arena2block(a, block_idx);
                ASSERT(!elem_find(&a->desc->free_list, &b->free_elem));
                list_append(&a->desc->free_list, &b->free_elem);
            }
            intr_set_status(old_status);
        }

        /* 开始分配内存块 */
        b = elem2entry(struct mem_block, free_elem, list_pop(&(descs[desc_idx].free_list)));
        memset(b, 0, descs[desc_idx].block_size);

        a = block2arena(b);  // 获取内存块b所在的arena
        a->cnt--;		     // 将此arena中的空闲内存块数减1
        lock_release(&mem_pool->lock);
        return (void*)b;
    }
}

malloc函数较为复杂,首先是判断当前是内核线程还是用户进程,这是不一样的,因为内核线程有内核线程的物理内存池,用户进程有用户进程的物理内存池。每次分配内存要在不同的内存池内分配。

申请的内存以byte为单位,如果大于1024,那么直接分配连续的连续的页。计算出需要多少页,然后通过 malloc_page 函数申请,返回其虚拟地址。此时其实已经为销毁这部分占用内存埋下了伏笔。让我们看一下现在我们分配的内存张啥样

image-20240329205115097

可以看到这里分配的页数是2,找到虚拟地址连续的两页,然后cnt=2,large=true,表示我们直接分配的页框,如果药销毁的话,传入的就是现在的地址,根据现在的地址,我们就可以修改相应的线程或者进程的虚拟地址池和物理地址池,达到销毁的目的。

如果申请的是小于1024的内存呢?比如64Byte。

image-20240329210024260

那就申请一个页,然后创建成这样的arena仓库,此时的cnt为当前可用的内存块数量,desc指向了内存描述符,内存描述符中有一个链表,链表把未分配的地址链接了起来,这是由于未分配的地址最前面放置了一个链表节点结构体,并在初始化这个arena仓库时将所有的空闲块都链接了起来,在释放时,也需要将释放地址前端再初始化为链表节点,加入空闲队列。

三、sys_free

理解了mallloc,再看free就简单很多了,他俩就是两个相互对应的过程,内存怎么分配的就怎么释放,

/* 将物理地址pg_phy_addr回收到物理内存池,这里的回收以页为单位 */
void pfree(uint32_t pg_phy_addr) {
    struct pool* mem_pool;
    uint32_t bit_idx = 0;
    if (pg_phy_addr >= user_pool.phy_addr_start) {         // 用户物理内存池
        mem_pool = &user_pool;
        bit_idx = (pg_phy_addr - user_pool.phy_addr_start) / PG_SIZE;
    }
    else {	                                               // 内核物理内存池
        mem_pool = &kernel_pool;
        bit_idx = (pg_phy_addr - kernel_pool.phy_addr_start) / PG_SIZE;
    }
    bitmap_set(&mem_pool->pool_bitmap, bit_idx, 0);	 // 将位图中该位清0
}

/* 去掉页表中虚拟地址vaddr的映射,只去掉vaddr对应的pte */
static void page_table_pte_remove(uint32_t vaddr) {
    uint32_t* pte = pte_ptr(vaddr);
    *pte &= ~PG_P_1;	                                   // 将页表项pte的P位置0,不需要删除pde
    asm volatile ("invlpg %0"::"m" (vaddr) : "memory");    // 更新tlb
}

/* 在虚拟地址池中释放以_vaddr起始的连续pg_cnt个虚拟页地址 */
static void vaddr_remove(enum pool_flags pf, void* _vaddr, uint32_t pg_cnt) {
    uint32_t bit_idx_start = 0;
    uint32_t vaddr = (uint32_t)_vaddr;
    uint32_t cnt = 0;

    if (pf == PF_KERNEL) {
        // 内核虚拟内存池
        bit_idx_start = (vaddr - kernel_vaddr.vaddr_start) / PG_SIZE;
        while (cnt < pg_cnt) {
            bitmap_set(&kernel_vaddr.vaddr_bitmap, bit_idx_start + cnt++, 0);
        }
    }
    else if (pf == PF_USER) {
        // 用户虚拟内存池
        struct task_struct* cur_thread = running_thread();
        bit_idx_start = (vaddr - cur_thread->userprog_vaddr.vaddr_start) / PG_SIZE;
        while (cnt < pg_cnt) {
            bitmap_set(&cur_thread->userprog_vaddr.vaddr_bitmap, bit_idx_start + cnt++, 0);
        }
    }
    else {
        PANIC("vaddr_remove error!\n");
    }
}

/* 释放以虚拟地址vaddr为起始的cnt个页框,vaddr必须是页框起始地址 */
void mfree_page(enum pool_flags pf, void* _vaddr, uint32_t pg_cnt) {
    uint32_t vaddr = (int32_t)_vaddr;
    uint32_t page_cnt = 0;
    // 确保虚拟地址是页框的起始
    ASSERT(pg_cnt >= 1 && vaddr % PG_SIZE == 0);
    // 获取虚拟地址vaddr对应的物理地址
    uint32_t pg_phy_addr = addr_v2p(vaddr);
    // 确保物理地址也是页框的起始
    ASSERT((pg_phy_addr % PG_SIZE) == 0);
    // 确保待释放的物理内存在低端1M+1k大小的页目录+1k大小的页表地址范围外
    ASSERT(pg_phy_addr >= 0x102000);

    if (pg_phy_addr >= user_pool.phy_addr_start) {
        // 位于user_pool内存池,要释放的是用户内存
        for (page_cnt = 0; page_cnt < pg_cnt; page_cnt++) {
            vaddr = (int)_vaddr + PG_SIZE * page_cnt;
            pg_phy_addr = addr_v2p(vaddr);
            // 确保物理地址属于用户物理内存池 
            ASSERT((pg_phy_addr % PG_SIZE) == 0);
            ASSERT(pg_phy_addr >= user_pool.phy_addr_start);
            // 先将对应的物理页框归还到内存池
            pfree(pg_phy_addr);
            // 再从页表中清除此虚拟地址所在的页表项pte
            page_table_pte_remove(vaddr);
        }
    }
    else {
        // 位于kernel_pool内存池,要释放的是内核内存
        for (page_cnt = 0; page_cnt < pg_cnt; page_cnt++) {
            vaddr = (int)_vaddr + PG_SIZE * page_cnt;
            // 获得物理地址
            pg_phy_addr = addr_v2p(vaddr);
            // 确保待释放的物理内存只属于内核物理内存池
            ASSERT((pg_phy_addr % PG_SIZE) == 0);
            ASSERT(pg_phy_addr >= kernel_pool.phy_addr_start);
            ASSERT(pg_phy_addr < user_pool.phy_addr_start);
            // 先将对应的物理页框归还到内存池
            pfree(pg_phy_addr);
            // 再从页表中清除此虚拟地址所在的页表项pte
            page_table_pte_remove(vaddr);
        }
    }
    // 清空虚拟地址的位图中的相应位
    vaddr_remove(pf, _vaddr, pg_cnt);
}

/* 回收堆内存 */
void sys_free(void* ptr) {
    ASSERT(ptr != NULL);
    if (ptr == NULL) return;

    enum pool_flags PF;        // 回收的是内核还是用户的内存
    struct pool* mem_pool;     // 内核用户池或者用户内存池

    /* 判断是线程还是进程 */
    if (running_thread()->pgdir == NULL) {
        ASSERT((uint32_t)ptr >= K_HEAP_START);
        PF = PF_KERNEL;
        mem_pool = &kernel_pool;
    }
    else {
        PF = PF_USER;
        mem_pool = &user_pool;
    }

    lock_acquire(&mem_pool->lock);
    struct mem_block* b = ptr;
    struct arena* a = block2arena(b);	       // 把mem_block转换成arena,获取元信息,元信息在每个块的头部
    if (a->desc == NULL && a->large == true) { // 大于1024的内存
        mfree_page(PF, a, a->cnt);
    }
    else {
        // 小于等于1024的内存块,先将内存块回收到描述符的空闲列表
        list_append(&a->desc->free_list, &b->free_elem);
        // 将内存块元信息中的块数量加1
        a->cnt++;
        // 再判断此arena中的内存块是否都是空闲,如果是就释放这个arena块
        if (a->cnt == a->desc->blocks_per_arena) {
            // 先从空闲列表中逐个删除块
            for (uint32_t block_idx = 0; block_idx < a->desc->blocks_per_arena; block_idx++) {
                struct mem_block* b = arena2block(a, block_idx);
                ASSERT(elem_find(&a->desc->free_list, &b->free_elem));
                list_remove(&b->free_elem);
            }
            // 在删除整个页
            mfree_page(PF, a, 1);
        }
    }
    lock_release(&mem_pool->lock);
}

这里需要注意的一点是,如果一个arena仓库全为空,那么就释放这个仓库所占页面。就是要一点,给一点,内存资源宝贵,只能这样抠抠搜搜。

四、用户调用

/* 初始化系统调用,也就是将syscall_table数组中绑定好确定的函数 */
void syscall_init(void) {
    put_str("syscall_init begin!\n");
    syscall_table[SYS_GETPID] = sys_getpid;
    syscall_table[SYS_WRITE] = sys_write;
    syscall_table[SYS_MALLOC] = sys_malloc;
    syscall_table[SYS_FREE] = sys_free;
    put_str("syscall_init done!\n");
}
/* 申请size字节大小的内存,并返回结果 */
void* malloc(uint32_t size) {
   return (void*)_syscall1(SYS_MALLOC, size);
}

/* 释放ptr指向的内存 */
void free(void* ptr) {
   _syscall1(SYS_FREE, ptr);
}

用户调用的代码和之前的保持一致。

4.1、仿真

仿真的main如下

// os/src/kernel/main.c
#include "print.h"
#include "init.h"
#include "thread.h"
#include "interrupt.h"
#include "console.h"
#include "process.h"
#include "syscall_init.h"
#include "syscall.h"
#include "stdio.h"
#include "memory.h"

void k_thread_a(void*);
void k_thread_b(void*);
void u_prog_a(void);
void u_prog_b(void);

int main(void) {
    put_str("I am kernel\n");
    init_all();
    intr_enable();
    process_execute(u_prog_a, "u_prog_a");
    process_execute(u_prog_b, "u_prog_b");
    thread_start("k_thread_a", k_thread_a, "I am thread_a");
    thread_start("k_thread_b", k_thread_b, "I am thread_b");
    while (1);
    return 0;
}

/* 在线程中运行的函数 */
void k_thread_a(void* arg) {
    void* addr1 = sys_malloc(256);
    void* addr2 = sys_malloc(255);
    void* addr3 = sys_malloc(254);
    printk(" k_thread_a malloc addr:0x%x, 0x%x, 0x%x\n", (int)addr1, (int)addr2, (int)addr3);

    int cpu_delay = 100000;
    while (cpu_delay-- > 0);
    sys_free(addr1);
    sys_free(addr2);
    sys_free(addr3);
    while (1);
}

/* 在线程中运行的函数 */
void k_thread_b(void* arg) {
    void* addr1 = sys_malloc(256);
    void* addr2 = sys_malloc(255);
    void* addr3 = sys_malloc(254);
    printk(" k_thread_b malloc addr:0x%x, 0x%x, 0x%x\n", (int)addr1, (int)addr2, (int)addr3);

    int cpu_delay = 100000;
    while (cpu_delay-- > 0);
    sys_free(addr1);
    sys_free(addr2);
    sys_free(addr3);
    while (1);
}

/* 测试用户进程 */
void u_prog_a(void) {
    void* addr1 = malloc(256);
    void* addr2 = malloc(255);
    void* addr3 = malloc(254);
    printf(" prog_a malloc addr:0x%x, 0x%x, 0x%x\n", (int)addr1, (int)addr2, (int)addr3);

    int cpu_delay = 100000;
    while (cpu_delay-- > 0);
    free(addr1);
    free(addr2);
    free(addr3);
    while (1);
}

/* 测试用户进程 */
void u_prog_b(void) {
    void* addr1 = malloc(256);
    void* addr2 = malloc(255);
    void* addr3 = malloc(254);
    printf(" prog_b malloc addr:0x%x, 0x%x, 0x%x\n", (int)addr1, (int)addr2, (int)addr3);

    int cpu_delay = 100000;
    while (cpu_delay-- > 0);
    free(addr1);
    free(addr2);
    free(addr3);
    while (1);
}

image-20240329211057932

结束语

今天实现了系统调用 mallocfree,将堆内存管理实现,下一节将实现硬盘驱动,在硬盘驱动的基础上实现文件系统。

老规矩,本节的代码地址:https://github.com/lyajpunov/os

标签:arena,block,--,void,内存,手写,pool,desc
From: https://blog.csdn.net/weixin_43903639/article/details/137240974

相关文章

  • HWOD:记录正负数
    一、知识点1、scanf()的返回值scanf()返回值类型为int,返回转换成功的个数有代码int temp;  scanf("%d",&temp);在屏幕输入一个数字,比如5,回车,scanf()返回1在屏幕输入一个字符或字符串,比如h或helloworld,回车,scanf()返回0,表示转换失败2、结束scanf()的不定数量读取基......
  • 一文彻底搞懂常见IO模型
    文章目录1.常见的IO模型2.阻塞IO(BIO)3.非阻塞IO(NIO)4.IO多路复用5.信号驱动6.异步IO(AIO)7.BIO,NIO,AIO的区别1.常见的IO模型在UNIX操作系统中,常见的I/O模型有以下五种:1.阻塞I/O模型(BlockingI/O)在阻塞I/O模型中,应用程序发起一个I/O请求后会一直阻......
  • int的最大值加1会变成int的最小值
    一、概要int是4个字节,总共是32个bit位,所以总共能表示2^32个数int的最大值是2^31-1,也就是2147483647,大约21亿多减的那个1表示自然数0的位置int的最小值是-2^31,也就是-2147483648,大约负的21亿多int的最大值加1会变成int的最小值二、int的最大值加1会变成int的最小值1、自......
  • 类的函数成员(三):拷贝构造函数
    一.什么是拷贝构造函数?1.1概念        同一个类的对象在内存中有完全相同的结构,如果作为一个整体进行复制或称拷贝是完全可行的。这个拷贝过程只需要拷贝数据成员,而函数成员是共用的(只有一份拷贝)。        在建立对象时可用同一类的另一个对象来初始化该对......
  • 26版SPSS操作教程(初级第十四章)
    前言#由于导师最近布置了学习SPSS这款软件的任务,因此想来平台和大家一起交流下学习经验,这期推送内容接上一次第十三章的学习笔记,希望能得到一些指正和帮助~粉丝及官方意见说明#针对官方爸爸的意见说的推送缺乏操作过程的数据案例文件澄清如下:1、操作演示的数据全部由我本人......
  • 26版SPSS操作教程(初级第十五章)
    前言#由于导师最近布置了学习SPSS这款软件的任务,因此想来平台和大家一起交流下学习经验,这期推送内容接上一次第十四章的学习笔记,希望能得到一些指正和帮助~粉丝及官方意见说明#针对官方爸爸的意见说的推送缺乏操作过程的数据案例文件澄清如下:1、操作演示的数据全部由我本人......
  • 初识CSS
    目录前言:    CSS的介绍:    CSS的发展:    1)CSS1.0:    2)CSS2.0:    3)CSS2.1:    4)CSS3:CSS特点:    1)丰富的样式定义:    2)易于设置和修改:    3)可以多页面应用:    4)层叠:5)页面压缩:......
  • 01-​JVM学习记录-类加载器
     一、类加载器子系统1.作用-运输工具(快递员)负责从文件系统或者网络中加载Class文件(DNA元数据模板),Class文件开头有特定标识,魔术,咖啡杯壁(class文件存于本地硬盘,JVM根据class实例化)DNA元数据模板Classloader只负责class文件的加载,至于是否可运行,则由执行引擎决定加载的......
  • 一文彻底搞懂MySQL中事务的五种分类
    文章目录1.什么是事务2.事务的分类3.事务的详解1.什么是事务事务是指作为单个逻辑工作单元执行的一系列操作,这些操作要么全部成功完成,要么全部失败回滚,从而保证数据库操作一致性和完整性的重要机制,它确保了数据库在并发环境下的正确性和可靠性。在数据库中,事务......
  • 微调工程师岗位可能并不存在,但使用 AI 编码工具已经成为刚需
    智能编码工具的快速普及是否会带来全新的编程模式?“大力出奇迹”的规律还将继续适用吗?本文节选自QCon北京特别策划圆桌节目,内容摘自阿里云通义灵码产品技术负责人陈鑫在圆桌对话里的精彩回答。全文见:Sora很难跟进?微调就不是一个岗位?大力出奇迹将继续适用?大模型将对软件生态带来......