Linux下brk、sbrk实现一个简易版本的malloc
目录
一.内存四区回顾
二.brk、sbrk
三.使用brk、sbrk模拟实现malloc和free
一.内存四区回顾
- 程序代码区:该区域在程序运行时存放程序的二进制代码。
全局数据区:该区域主要存放全局变量,静态变量和各种常量。
- 堆:堆用于在程序运行时动态分配内存,比如new一个新的对象,或者malloc一个新数组,就是在堆中分配存储空间的,一般由程序员手动控制,但也容易造成内存泄漏。
- 栈:该区域主要存放程序运行时函数的参数与局部变量等,当程序员完成某个软件的编译时,一般该软件对应内存栈的大小也就由编译器确定了,但直到程序真正运行时,操作系统才会在内存栈中为其分配空间
二.brk、sbrk
下面让我们一起来看看brk和sbrk的函数原型
#include <unistd.h> int brk(void *addr); void *sbrk(intptr_t increment);
sbrk/brk都是通过增量的方式来分配虚拟内存。首先我们先来解释一下这个sbrk函数,sbrk函数当中的increment的取值有三种情况
1.大于0增加内存空间
2.等于0获取末尾地址
3.小于0释放空间
函数返回值:返回上一调用brk/sbrk后末尾地址,失败返回-1.
sbrk其实内部维护了一个指针,指向当前堆内存的最后一个字节的下一个位置,sbrk函数根据增量参数调整该指针原来的位置如果发现页耗尽了,则会自动追加或者取消页映射
下面我们来通过一张简图来看看sbrk是如何实现分配内存的
下面我们调用sbrk申请四个字节的空间那么此时top就会往右边移动四个字节(假设地址从左往右依次递增)
如果我们此时再调用sbrk(0)那么此时返回的就是top'指向的地址。如果我们想要释放这块空间此时我们需要调用sbrk(-4)即可。此时top'指针就回到了top所指向的位置。sbrk这种分配内存空间的方式也就意味着,无法做到释放任意申请的空间比如说我们依次申请了3次4个字节的空间,此时我们无法通过sbrk释放中间这块空间。
下面我们通过代码进行演示一下,sbrk的函数的使用:
#include<iostream> #include<cstdlib> #include<unistd.h> #include<cstdio> using namespace std; int main() { char*ptr1=(char*)sbrk(1);//申请一个字节大小的空间 printf("ptr1的地址为:%p\n",ptr1); void*ptr2=sbrk(0);//获取动态内存的末尾位置 printf("ptr2的地址为:%p\n",ptr2); int *ptr3=(int*)sbrk(4); printf("ptr3的地址为:%p\n",ptr3); return 0; }
运行结果:
2.下面我们来看一下brk这个函数,同样的这个函数也是通过增量分配虚拟内存
brk函数内部维护了一个指针指向当前堆内存最后一个字节的下一个位置这一点和sbrk很相似。
brk函数会根据指针参数设置该指针的位置,如果发现页耗尽则自动追加或者取消映射
函数返回值:成功返回0,失败返回-1
由于这个非常的简单再这里就不演示代码了。
总结:
1.sbr/brk通过增量方式管理动态内存,申请内存是在动态内存后面申请,释放是从后往前释放不能够选择性的释放增量动态内存,只能从后往前释放动态内存。
2.一般来说我们使用sbrk来分配内存,使用brk来释放内存,这是因为使用sbrk来释放内存我们需要记住我们之前申请了多少大小的空间
3.sbrk/brk都有申请和释放动态内存的功能
三.使用brk、sbrk模拟实现malloc和free
在模拟实现一个简易版本的malloc和free之前,需要说明一下在这里我们只是模拟和真实的malloc的区别很大。我们在使用malloc的时候,比如说我们申请了4个字节的大小但是在底层真的只是分配了4个字节的大小吗?下面给出一张图来解释一下大致是个什么情况
我们在使用malloc进行申请内存的时候并不是你申请了多少个字节,底层给你分配多少个字节。你申请空间的时候会多分配一个控制块空间的大小用来管理。malloc的底层采用的是双向链表的方式来进行管理。在这里我们使用单链表来进行模拟
下面我们来看一下这个控制块是什么样子的
//malloc底层维护的是一个双向链表,再这里我们用单向链表来模拟 //注意我们只是模拟和真实的malloc有很大的区别 struct mem_control_block { size_t size_;//动态内存块的大小、 bool free_;//该内存块是否被使用,为true则代表可以分配给用户使用 mem_control_block*prev;//前一个控制块的指针 };
下面我们来看看这个malloc的模拟实现:
struct mem_control_block { size_t size_; //动态内存块的大小、 bool free_; //该内存块是否被使用,为true则代表可以分配给用户使用 mem_control_block *prev; //前一个控制块的指针 }; typedef mem_control_block MCB; MCB *g_top = nullptr; //维护的那个指针 #define MCBSIZE sizeof(MCB) void *my_malloc(size_t size) { //注意第一步需要从后往前找用没有合适的内存可以分配,没有开辟新的内存,真正的malloc是从前往后找 MCB *mcb = g_top; while (mcb) { if (mcb->size_ >= size) { break; } mcb = mcb->prev; } if (mcb == nullptr) //之前的空间当中没有合适的控制块 { mcb = (MCB *)sbrk(size + MCBSIZE); if (mcb == (void *)-1) { return nullptr; } mcb->size_ = size; mcb->prev = g_top; g_top = mcb; } mcb->free_ = false; //已经分配给用户了 return (void *)(mcb + 1); //返回可用的地址 }
注意:我们申请空间的时候不是直接调用sbrk函数直接来申请,而是需要遍历一下有没有合适的空间可以分配给用户,如果没有我们才调用sbrk函数进行分配空间,注意在分配空间的时候我们需要把控制块的空间也算上。在这里我们是通过单链表进行链接。
下面我们来看看free的模拟实现是怎么样的
void my_free(void *ptr) { if (ptr == nullptr) return; MCB *mcb = (MCB *)ptr - 1; mcb->free_ = true; //如果很多连续的动态内存释放了此时我们需要还给系统 mcb = g_top; while (mcb->prev && mcb->free_) { //从最后往前找最后一块被使用的内存 mcb = mcb->prev; } if (mcb->free_) { //释放内存 g_top = mcb->prev; brk(mcb); } else //后面的可以释放但是mcb这块内存是不能够释放的 { g_top = mcb; brk((char *)mcb + mcb->size_ + MCBSIZE); } }
注意我们在释放内存的时候不是直接将它的控制块当中的free_置为true,我们需要从后往前遍历一下看一下有没有一骗连续的内存空间可以释放。如果有我们则将这一块连续的空间通过brk函数归还给OS。
对应总代码:
#include <iostream> using namespace std; #include <unistd.h> #include <cstdlib> // malloc底层维护的是一个双向链表,再这里我们用单向链表来模拟 //注意我们只是模拟和真实的malloc有很大的区别 struct mem_control_block { size_t size_; //动态内存块的大小、 bool free_; //该内存块是否被使用,为true则代表可以分配给用户使用 mem_control_block *prev; //前一个控制块的指针 }; typedef mem_control_block MCB; MCB *g_top = nullptr; //维护的那个指针 #define MCBSIZE sizeof(MCB) void *my_malloc(size_t size) { //注意第一步需要从后往前找用没有合适的内存可以分配,没有开辟新的内存,真正的malloc是从前往后找 MCB *mcb = g_top; while (mcb) { if (mcb->size_ >= size) { break; } mcb = mcb->prev; } if (mcb == nullptr) //之前的空间当中没有合适的控制块 { mcb = (MCB *)sbrk(size + MCBSIZE); if (mcb == (void *)-1) { return nullptr; } mcb->size_ = size; mcb->prev = g_top; g_top = mcb; } mcb->free_ = false; //已经分配给用户了 return (void *)(mcb + 1); //返回可用的地址 } void my_free(void *ptr) { if (ptr == nullptr) return; MCB *mcb = (MCB *)ptr - 1; mcb->free_ = true; //如果很多连续的动态内存释放了此时我们需要还给系统 mcb = g_top; while (mcb->prev && mcb->free_) { //从最后往前找最后一块被使用的内存 mcb = mcb->prev; } if (mcb->free_) { //释放内存 g_top = mcb->prev; brk(mcb); } else //后面的可以释放但是mcb这块内存是不能够释放的 { g_top = mcb; brk((char *)mcb + mcb->size_ + MCBSIZE); } } int main() { int *p1 = (int *)my_malloc(4); int *p2 = (int *)my_malloc(4); int *p3 = (int *)my_malloc(4); cout << p2 << endl; my_free(p2); int *p4 = (int *)my_malloc(4); cout << p4 << endl; return 0; }