C语言接口与实现之又谈内存管理

前言

这一篇,我们继续讲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字段指向了大内存块中的空闲开始位置,也就是说从availlimit的内存是可用的

链表结构

假如我们需要分配 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 bunion 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结构体所有成员一一对应赋值给ptrstruct Arena_T结构体的成员,再对我们原来的arena进行赋值,这样做的意义就是将新的内存块插入到链表中,如下图所示

*ptr = *arena;
arena->avail = (char *)((union header *)ptr + 1);
arena->limit = limit;
arena->prev  = ptr;
插入后

特别的,下面这一句就是将找到新内存块的空闲内存开始位置并赋值给arenaavail成员

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自增表明空闲链表多了一块内存,此时arenalimit成员还是指向一号内存块的内存结束位置,将其赋给freechunkslimit成员,最后将tmp结构体的成员赋值给arena,最终变成以下结构

第四步

到此,我们就完成了链表的操作了

else部分比较简单,直接释放掉内存块。所以这里不做赘述。

后记

这一篇的内存管理跟上一章相比,结构更加简单,因为少了哈希表,而且使用了两章链表来维护,跟我在上一章结尾所言一样,将使用空闲这两章表分开管理。他们各自有自己的优缺点,笔者更加喜欢第二种。简洁明了,在功能上来说,两种方法达到的目的是一样的,我们今天讲的这一种会消耗更多一点的内存,但它同时也带来了便利性,我们可以一次性释放我们所有使用过的内存,保证我们不会内存泄露。

通过对这两章的学习,相信各位和我都可以知道内存管理不止是简单的mallocfree,我们可以使用数据结构来维护我们的内存,从而减少程序员在编程过程中的疏忽而造成的损失。当然还有很多种内存管理的方法,我们依旧需要在技术上好好学习才是

你可能感兴趣的:(C语言接口与实现之又谈内存管理)