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

前情提要

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

一、arena

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

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

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

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

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

1.1、arena数据结构

c 复制代码
/* 内存块 */
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。

1.2、arena初始化

c 复制代码
/* 内核内存块描述符数组初始化 */
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

c 复制代码
/* 在堆中申请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 函数申请,返回其虚拟地址。此时其实已经为销毁这部分占用内存埋下了伏笔。让我们看一下现在我们分配的内存张啥样

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

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

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

三、sys_free

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

c 复制代码
/* 将物理地址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仓库全为空,那么就释放这个仓库所占页面。就是要一点,给一点,内存资源宝贵,只能这样抠抠搜搜。

四、用户调用

c 复制代码
/* 初始化系统调用,也就是将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");
}
c 复制代码
/* 申请size字节大小的内存,并返回结果 */
void* malloc(uint32_t size) {
   return (void*)_syscall1(SYS_MALLOC, size);
}

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

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

4.1、仿真

仿真的main如下

c 复制代码
// 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);
}

结束语

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

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

相关推荐
watermelonoops44 分钟前
Deepin和Windows传文件(Xftp,WinSCP)
linux·ssh·deepin·winscp·xftp
疯狂飙车的蜗牛2 小时前
从零玩转CanMV-K230(4)-小核Linux驱动开发参考
linux·运维·驱动开发
远游客07134 小时前
centos stream 8下载安装遇到的坑
linux·服务器·centos
马甲是掉不了一点的<.<4 小时前
本地电脑使用命令行上传文件至远程服务器
linux·scp·cmd·远程文件上传
jingyu飞鸟4 小时前
centos-stream9系统安装docker
linux·docker·centos
超爱吃士力架4 小时前
邀请逻辑
java·linux·后端
cominglately7 小时前
centos单机部署seata
linux·运维·centos
魏 无羡7 小时前
linux CentOS系统上卸载docker
linux·kubernetes·centos
CircleMouse7 小时前
Centos7, 使用yum工具,出现 Could not resolve host: mirrorlist.centos.org
linux·运维·服务器·centos
木子Linux8 小时前
【Linux打怪升级记 | 问题01】安装Linux系统忘记设置时区怎么办?3个方法教你回到东八区
linux·运维·服务器·centos·云计算