前情提要
前面我们实现了 0x80
中断,并实现了两个中断调用,getpid
和 write
,其中 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);
}
结束语
今天实现了系统调用 malloc
和 free
,将堆内存管理实现,下一节将实现硬盘驱动,在硬盘驱动的基础上实现文件系统。
老规矩,本节的代码地址:https://github.com/lyajpunov/os