前情提要
前面我们实现了 0x80
中断,并实现了两个中断调用,getpid
和 write
,其中 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_descuint32_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初始化
/* 内核内存块描述符数组初始化 */
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)) { // 若申请的内存不在内存池容量范围内则直接返回NULLreturn 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_blockif (list_empty(&descs[desc_idx].free_list)) {// 分配1页框做为arenaa = malloc_page(PF, 1);if (a == NULL) {lock_release(&mem_pool->lock);return NULL;}memset(a, 0, PG_SIZE);// 对于分配的小块内存,将desc置为相应内存块描述符,cnt置为此arena可用的内存块数,large置为falsea->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所在的arenaa->cnt--; // 将此arena中的空闲内存块数减1lock_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就简单很多了,他俩就是两个相互对应的过程,内存怎么分配的就怎么释放,
/* 将物理地址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,不需要删除pdeasm 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);// 再从页表中清除此虚拟地址所在的页表项ptepage_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);// 再从页表中清除此虚拟地址所在的页表项ptepage_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);// 将内存块元信息中的块数量加1a->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);
}
结束语
今天实现了系统调用 malloc
和 free
,将堆内存管理实现,下一节将实现硬盘驱动,在硬盘驱动的基础上实现文件系统。
老规矩,本节的代码地址:https://github.com/lyajpunov/os