美文网首页
C语言接口与实现之又谈内存管理

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

作者: wipping的技术小栈 | 来源:发表于2019-05-03 14:15 被阅读0次

    前言

    这一篇,我们继续讲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语言接口与实现之又谈内存管理

          本文链接:https://www.haomeiwen.com/subject/epaanqtx.html