前言
这一篇,我们继续讲C语言实现内存管理,前面一章我们讲了最先适配算法的内存管理,其原理就是维护2张链表并使用一个结构体——内存描述符来描述内存块。在这两张链表中,一张是正在使用的内存链表,一张是空闲内存的链表,并且我们优先从空闲内存链表中提取出内存,当释放内存时我们是将内存块挂在了空闲内存链表上。让我们开始这一章吧
代码综述
这一章的主要思想就是当我们使用内存的时候,从内存池中提取内存,而当我们不再需要使用这一片内存的时候,我们将整个内存池释放掉,这样就可以避免我们的内存泄露了。但它也有缺点,比如需要更多的内存,也有可能造成悬挂指针
主要有以下几个函数组成
struct Arena_T {
struct Arena_T* prev;
char *avail;
char *limit;
};//描述内存池的结构体
extern T Arena_new (void);//开辟内存池
extern void Arena_dispose(struct Arena_T* *ap);//关闭内存池
extern void *Arena_alloc (struct Arena_T* arena, long nbytes,const char *file, int line);//从内存池中内配内存
extern void *Arena_calloc(struct Arena_T* arena, long count,long nbytes, const char *file, int line);//分配并清空内存
extern void Arena_free (struct Arena_T* arena);//释放内存
Arena_T
中的 limit
字段指向了大内存块的结束处,而avail
字段指向了大内存块中的空闲开始位置,也就是说从avail
到limit
的内存是可用的
假如我们需要分配
N
个字节的内存,但是 avail - limit < N
,那么我们就需要重新开辟一个内存块,并且将该内存块继续挂在链表上,使用prev
字段来构成链表
为了让读者更容易理解,我在这里先将其余的一些结构体和数据类型声明贴出来,后面会用到
struct Arena_T {
struct Arena_T* prev;
char *avail;
char *limit;
};
union align {
int i;
long l;
long *lp;
void *p;
void (*fp)(void);
float f;
double d;
long double ld;
};
union header {
struct Arena_T b;
union align a;
};
static struct Arena_T* freechunks;
static int nfree;
Arena_new
代码如下
struct Arena_T* Arena_new(void) {
struct Arena_T* arena = malloc(sizeof (*arena));
if (arena == NULL)
RAISE(Arena_NewFailed);
arena->prev = NULL;
arena->limit = arena->avail = NULL;
return arena;
}
非常简单,开辟一个内存块结构体,初始化之后返回
void *Arena_alloc
代码如下
void *Arena_alloc(struct Arena_T* arena, long nbytes,const char *file, int line)
{
assert(arena);
assert(nbytes > 0);
nbytes = ((nbytes + sizeof (union align) - 1)/
(sizeof (union align)))*(sizeof (union align));
while (nbytes > arena->limit - arena->avail) {
struct Arena_T* ptr;
char *limit;
if ((ptr = freechunks) != NULL) {
freechunks = freechunks->prev;
nfree--;
limit = ptr->limit;
} else {
long m = sizeof (union header) + nbytes + 10*1024;
ptr = malloc(m);
if (ptr == NULL)
{
if (file == NULL)
RAISE(Arena_Failed);
else
Except_raise(&Arena_Failed, file, line);
}
limit = (char *)ptr + m;
}
*ptr = *arena;
arena->avail = (char *)((union header *)ptr + 1);
arena->limit = limit;
arena->prev = ptr;
}
arena->avail += nbytes;
return arena->avail - nbytes;
}
首先对参数进行常规的检查,下面这句代码很简单,我们之前也讲过,就是对nbytes
进行对齐,补足为16的倍数
nbytes = ((nbytes + sizeof (union align) - 1)/
(sizeof (union align)))*(sizeof (union align));
接着是一个while
循环,这个循环的判断条件就是当前内存块的空闲内存是否满足需求,进入while
时结构如下
在这个循环里面有一个if
判断,这个判断就是查看freechunks
是否为空,freechunks
是一个struct Arena_T
类型的指针,它是空闲内存块的表头,当我们释放内存的时候并不会真的被free
掉,而是被挂在了freechunks
指向的链表。
我们先看 else
部分,也就是不满足判断的情况下
else
{
long m = sizeof (union header) + nbytes + 10*1024;
ptr = malloc(m);
if (ptr == NULL)
{
if (file == NULL)
RAISE(Arena_Failed);
else
Except_raise(&Arena_Failed, file, line);
}
limit = (char *)ptr + m;
}
union header
联合体中有 struct Arena_T b
和 union align a
这两者,我们可以默认使用的是struct Arena_T b
这个结构体
那么在这个else
中一开始做的就是定义一个大小m
,这个大小是 需要分配的内存nbytes
、 结构体struct Arena_T b
的和再加上10kb
,结合我们上面的图就知道,分配出来的内存块一开始是结构体struct Arena_T
,在这之后的才是我们的真正的内存,其中10kb
应该是裕量。
接着就是找到内存块的结束位置并赋值给limit
if
部分如下
if ((ptr = freechunks) != NULL) {
freechunks = freechunks->prev;
nfree--;
limit = ptr->limit;
}
如果空闲内存块链表中有可使用的内存,那么我们就将freechunks
指针下移,指向下一个空闲内存块,因为freechunk
第一个空闲内存块的指针已经赋值给了ptr
,而limit = ptr->limit
则是将该第一个空闲内存块的结束位置赋值给limit
,后面我们会讲到ptr->limit
为什么是空闲内存块的结束位置
继续,我们看下面的代码
while (nbytes > arena->limit - arena->avail) {
struct Arena_T* ptr;
char *limit;
ptr = {get new memory}
*ptr = *arena;
arena->avail = (char *)((union header *)ptr + 1);
arena->limit = limit;
arena->prev = ptr;
}
下面这一句就是我们在上面讲的if-else
,功能就是让ptr
指向一个内存块
ptr ={ get new memory}
ptr
指向的内存块有2部分,前半部分是struct Arena_T
结构体,后半部分是内存。
接着将arena
结构体所有成员一一对应赋值给ptr
的struct Arena_T
结构体的成员,再对我们原来的arena
进行赋值,这样做的意义就是将新的内存块插入到链表中,如下图所示
*ptr = *arena;
arena->avail = (char *)((union header *)ptr + 1);
arena->limit = limit;
arena->prev = ptr;
特别的,下面这一句就是将找到新内存块的空闲内存开始位置
并赋值给arena
的avail
成员
arena->avail = (char *)((union header *)ptr + 1);
最后就是改变内存块的avail
成员,让其指向分配内存后的有效地址并可用内存的指针
arena->avail += nbytes;
return arena->avail - nbytes;
Arena_alloc
代码如下
void *Arena_calloc(struct Arena_T* arena, long count, long nbytes,
const char *file, int line) {
void *ptr;
assert(count > 0);
ptr = Arena_alloc(arena, count*nbytes, file, line);
memset(ptr, '\0', count*nbytes);
return ptr;
}
非常简单,调用Arena_alloc
并清空内存
Arena_free
代码如下
void Arena_free(struct Arena_T* arena) {
assert(arena);
while (arena->prev) {
struct Arena_T tmp = *arena->prev;
if (nfree < THRESHOLD) {
arena->prev->prev = freechunks;
freechunks = arena->prev;
nfree++;
freechunks->limit = arena->limit;
} else
free(arena->prev);
*arena = tmp;
}
assert(arena->limit == NULL);
assert(arena->avail == NULL);
}
作用就是将arena
指向的内存池中的所有内存块挂在freechunks
上,如果内存块的数量超出上限THRESHOLD
,剩余的内存就都释放掉,这里并不是关闭内存池。
在参数检查之后是一个while
循环,这个循环的判断条件就是arena->prev
是否为空,换句话说就是当前内存池arena
是否还有内存块,我们看局部的代码
struct Arena_T tmp = *arena->prev;
if(...)
{
arena->prev->prev = freechunks;
freechunks = arena->prev;
nfree++;
freechunks->limit = arena->limit;
}
else
{
...
}
*arena = tmp;
再没进入while
如下图
进入while
后定义结构体一个临时的结构体tmp
用于存放当链表中的一号内存块结构体的所有成员的值,所以会有如下图的结构
接着,我们假设是第一次free
,那么此时freechunks
为空,所以此时二号内存块的prev
相当于指向空的地方,所以如下图一样断开连接了,接着,再把一号内存块的地址赋给freechunks
,所以此时freechunks
指向了一号内存
最后,nfree
自增表明空闲链表多了一块内存,此时arena
的limit
成员还是指向一号内存块的内存结束位置,将其赋给freechunks
的limit
成员,最后将tmp
结构体的成员赋值给arena
,最终变成以下结构
到此,我们就完成了链表的操作了
而else
部分比较简单,直接释放掉内存块。所以这里不做赘述。
后记
这一篇的内存管理跟上一章相比,结构更加简单,因为少了哈希表,而且使用了两章链表来维护,跟我在上一章结尾所言一样,将使用
和空闲
这两章表分开管理。他们各自有自己的优缺点,笔者更加喜欢第二种。简洁明了,在功能上来说,两种方法达到的目的是一样的,我们今天讲的这一种会消耗更多一点的内存,但它同时也带来了便利性,我们可以一次性释放我们所有使用过的内存,保证我们不会内存泄露。
通过对这两章的学习,相信各位和我都可以知道内存管理不止是简单的malloc
和free
,我们可以使用数据结构来维护我们的内存,从而减少程序员在编程过程中的疏忽而造成的损失。当然还有很多种内存管理的方法,我们依旧需要在技术上好好学习才是