前言
使用c语言实现malloc,加深对内存分配的理解。主要实现两个内存分配相关的函数,malloc与free
前置知识
内存布局相关
malloc分配的内存是虚拟内存,首先了解虚拟地址空间。虚拟地址空间内部分为内核空间与用户空间。32位系统的内存布局如下图所示
其中堆区和文件映射区的内存是动态分配的,分别对应系统调用sbrk()、brk()
以及mmap()
。malloc()
在堆区还是文件映射区分配内存取决于要分配的内存大小,本文主要使用系统调用sbrk()
C语言相关
sbrk()
brk()
与sbrk()
都是扩展堆区的上界break
,从而获得新的内存空间,本实验主要使用sbrk()
函数原型:void *sbrk(sizt_t size);
说明:使用该函数将堆区空间增加size字节,分配成功返回堆区上界break
struct
使用结构体定义双向链表
typedef struct _Node {
size_t size; //可使用大小
struct _Node *prev;
struct _node *next;
} Node;
结构体变量初始化:Node *node = NULL;
,初始化为NULL后,并没有为node分配空间,不指向内存中的任何地址
访问结构体变量的两种方式:①node->size
②(*node).size
取结构体的地址进行运算:(char *)
将地址转化为字节长度进行运算,使用(void *)
编译器可能报错
内存对齐
CPU访问内存不是逐个字节访问,而是以字长(word size)为单位。
举个例子:32位CPU,一个int变量占4字节,假设存放的地址为0x 00000001 ~ 0x 00000005。为了取到该变量,CPU要进行两次寻址,0 ~ 3与4 ~ 7,然后再拼接处理,影响了访问的性能。若内存对齐,则能够减少访问次数。
结构体内存对齐规则:
- 第一个成员在结构体地址偏移量为0的地方
- 其它成员变量的偏移量 = 该变量对齐数的整倍数(对齐数取编译器默认值(#pragma pack())与该变量所占内存大小中的较小值)
- 结构体的大小 = 所有成员最大对齐数的整倍数
- 数组以数组本身的类型计算,如 int a[5] 以 int 类型大小计算对齐
举个简单的例子:假设 #pragma pack(4),表示编译器默认对齐数为4字节
struct Test {
char a;
char b;
int c;
};
此时变量的偏移量分别为0、1、4,结构体总大小为8字节,若定义顺序改变,如下
struct Test {
char a;
int b;
char c;
};
此时变量的偏移量分别为0、4、8,结构体总大小为12字节。所以在设计数据结构时,要合理的定义变量。
具体实现
数据结构
隐式空闲链表
特点:结构体成员中包含free标志,用于判断该内存块是否是空闲可用的。
缺点:分配内存时,需要沿着链表一直遍历到合适的内存块,无差别遍历,会遍历已分配出去的内存块,性能低。
显示空闲链表
链表中只包含空闲的内存块,调用malloc时只遍历空闲的内存块,效率高。当执行free(*ptr)
操作时,将释放的内存块加入到链表当中,并且看能不能和相邻的空闲块合并,组成更大的内存块。因为要访问相邻的链表,故考虑使用双向链表。
部分接口实现
void *my_malloc(size_t size, int type) {
Node *cur = freeHead;
cur = type == 1 ? getFirstFitAdd(cur, size) : getBestFitAdd(cur, size);
if (cur != NULL){
split(cur, size);
return (char *)cur + nodeSize;
} else{
Node *node = (Node *)sbrk(size + nodeSize); //用户可用区+元数据区
node->prev = NULL;
node->next = NULL;
node->size = size;
return (char *)node + nodeSize;
}
}
说明:size为分配的内存大小(可使用内存,不包括元数据区);type为分配的策略
sbrk()
申请的大小应为size + sizeof(Node)
(用户可使用内存+元数据所占内存)
两种分配策略:
first fit:返回第一个满足所需大小的内存块
best fit:将链表遍历完,返回分配后残留空间最小的内存块
void *getFirstFitAdd(Node *cur, size_t size) {
Node *node = cur;
while (node != NULL) {
if (node->size >= size) {
return (char *)node + nodeSize;
}
node = node->next;
}
return NULL;
}
说明:从cur开始搜索,返回第一个大小大于size的内存块的地址
同理可得getBestFitAdd()
,这里省略不写了
void split(Node *cur, size_t size) {
//enough
if (cur->size >= size + nodeSize){
Node *split_block = (Node *)((char *)cur + size + nodeSize); //怎么表示结构体地址??
split_block->size = cur->size - size - nodeSize;
split_block->next = NULL;
split_block->prev = NULL;
addIntoLinkedList(split_block);
cur->size = size;
removeFromLinkedList(cur);
}else {
removeFromLinkedList(cur);
}
}
说明:申请内存成功后,进行内存分割,将所申请的内存块中多余的内存分割出来。如果当前传入的块内存空间大于size + sizeof(node)
,则证明可以进行分割;若不能,直接移除该结点。
void ff_free(void *ptr) {
Node *cur = (Node *)((char *)ptr - nodeSize);
addIntoLinkedList(cur);
mergeBackIfPossible(cur);
mergeFrontIfPossible(cur);
}
说明:释放内存,将释放的内存加入到链表当中来,加入后记得合并相邻空间(如果可以的话)。
void mergeFrontIfPossible(Node *cur) {
if (cur->prev != NULL && ((char *)cur->prev + cur->prev->size + nodeSize) == (char *)cur) {
cur->prev->size += cur->size + nodeSize;
removeFromLinkedList(cur);
}
}
说明:若该内存块能与前一个内存块合并,则合并,并删除原有块
mergeBackIfPossible()
与之类似,不写了
结尾
新手,一些细节可能写错,望指正