操作系统实现:malloc 与 堆 实现
本文参考书:操作系统真像还原
什么是malloc?
malloc 是用户态申请内存时使用的函数。
malloc在哪里申请?
堆中。
什么是堆?
程序运行过程中需要申请额外的内存都会在堆中分配,堆中的内存分为几个规格类型的块用链表保存,程序需要内存就分配一个大于等于所需内存大小的块。如果一个规格的块用完了就像系统申请页,再将页切分成规格块的大小一个一个用链表链接起来。
如何找到堆?
一般堆在进程pcb处有指针,内核的堆可以是一个全局变量。
由以上几个问题我们可以知道,只要先搞定堆后,malloc的作用就是在堆中拿个内存块就好啦。
堆的结构图
堆的核心结构是个数组u_block_desc 代表用户堆,放在进程pcb中。数组的每个元素mem_block_desc 代表不同类型的内存块大小,每个内存块用链表链接。
几个重要数据结构,下面会进行解释
/* 内存块 */ 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
/* large为ture时,cnt表示的是页框数。
* 否则cnt表示空闲mem_block数量 */
uint32_t cnt;
bool large;
};
mem_block_desc 就是内存块的信息,记录着内存块大小和链表
arena就是分配的页,arena中内存块切割大小由desc指定,如果所需内存块过大,就不进行切割直接分配页框,也就是large的作用。
内存分配过程如下
① 向堆申请可用内存时,没有在所要用到的mem_block_desc下面找到可用内存块。向操作系统申请4K的页,每个页就是一个arena。
②对arena进行切分,分为所需mem_block_desc的大小,并且一个一个加入到mem_block_desc的链表中
③从链表上拿到一个内存块并返回内存地址
1 void* sys_malloc(uint32_t size) { 2 enum pool_flags PF; 3 struct pool* mem_pool; 4 uint32_t pool_size; 5 struct mem_block_desc* descs; 6 struct task_struct* cur_thread = running_thread(); 7 8 /* 判断用哪个内存池,内核也要从内核内存池申请页*/ 9 if (cur_thread->pgdir == NULL) { // 若为内核线程 10 PF = PF_KERNEL; 11 pool_size = kernel_pool.pool_size; 12 mem_pool = &kernel_pool; 13 descs = k_block_descs; 14 } else { // 用户进程pcb中的pgdir会在为其分配页表时创建 15 PF = PF_USER; 16 pool_size = user_pool.pool_size; 17 mem_pool = &user_pool; 18 descs = cur_thread->u_block_desc; 19 } 20 21 /* 若申请的内存不在内存池容量范围内则直接返回NULL */ 22 if (!(size > 0 && size < pool_size)) { 23 return NULL; 24 } 25 struct arena* a; 26 struct mem_block* b; 27 lock_acquire(&mem_pool->lock); 28 29 /* 超过最大内存块1024, 就分配页框 */ 30 if (size > 1024) { 31 uint32_t page_cnt = DIV_ROUND_UP(size + sizeof(struct arena), PG_SIZE); // 向上取整需要的页框数 32 33 a = malloc_page(PF, page_cnt); 34 35 if (a != NULL) { 36 memset(a, 0, page_cnt * PG_SIZE); // 将分配的内存清0 37 38 /* 对于分配的大块页框,将desc置为NULL, cnt置为页框数,large置为true */ 39 a->desc = NULL; 40 a->cnt = page_cnt; 41 a->large = true; 42 lock_release(&mem_pool->lock); 43 return (void*)(a + 1); // 跨过arena大小,把剩下的内存返回 44 } else { 45 lock_release(&mem_pool->lock); 46 return NULL; 47 } 48 } else { // 若申请的内存小于等于1024,可在各种规格的mem_block_desc中去适配 49 uint8_t desc_idx; 50 51 /* 从内存块描述符中匹配合适的内存块规格 */ 52 for (desc_idx = 0; desc_idx < DESC_CNT; desc_idx++) { 53 if (size <= descs[desc_idx].block_size) { // 从小往大后,找到后退出 54 break; 55 } 56 } 57 58 /* 若mem_block_desc的free_list中已经没有可用的mem_block, 59 * 就创建新的arena提供mem_block */ 60 if (list_empty(&descs[desc_idx].free_list)) { 61 a = malloc_page(PF, 1); // 分配1页框做为arena 62 if (a == NULL) { 63 lock_release(&mem_pool->lock); 64 return NULL; 65 } 66 memset(a, 0, PG_SIZE); //清空页数据 67 68 /* 对于分配的小块内存,将desc置为相应内存块描述符, 69 * cnt置为此arena可用的内存块数,large置为false */ 70 a->desc = &descs[desc_idx]; 71 a->large = false; 72 a->cnt = descs[desc_idx].blocks_per_arena; 73 uint32_t block_idx; 74 75 enum intr_status old_status = intr_disable(); 76 77 /* 开始将arena拆分成内存块,并添加到内存块描述符的free_list中 */ 78 for (block_idx = 0; block_idx < descs[desc_idx].blocks_per_arena; block_idx++) { 79 b = arena2block(a, block_idx); 80 ASSERT(!elem_find(&a->desc->free_list, &b->free_elem)); 81 list_append(&a->desc->free_list, &b->free_elem); 82 } 83 intr_set_status(old_status); 84 } 85 86 /* 开始分配内存块 */ 87 b = elem2entry(struct mem_block, free_elem, list_pop(&(descs[desc_idx].free_list))); 88 memset(b, 0, descs[desc_idx].block_size); 89 90 a = block2arena(b); // 获取内存块b所在的arena 91 a->cnt--; // 将此arena中的空闲内存块数减1 92 lock_release(&mem_pool->lock); 93 return (void*)b; 94 } 95 }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· winform 绘制太阳,地球,月球 运作规律
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理