c语言接口与实现--内存管理章节理解,含实例

《c语言接口与实现–创建可重用软件技术》人邮版第5章–内存管理,本章节涉及到c语言的内存分配与回收,内存管理在c语言中尤为重要,如果处理不当,会造成内存泄漏甚至系统崩溃的严重问题。本章介绍了一种内存管理方法的实现。比较难懂,需要结合图形来理解。
书中给出了两个源码mem.c和memchk.c, mem.c为简单的实现,是一般的使用方式;memchk.c是本章的精髓,还是先上完整的代码,然后逐步阐述我对代码的理解,如有错误请指正。

mem.h,我的except.h放在公共的include里面,而且做成了libexcept.so,后续的章节基本都会用到这个异常库。

#ifndef MEM_INCLUDED
#define MEM_INCLUDED

#include "../../include/except.h"

extern const Except_T Mem_Failed;
extern void *Mem_alloc(long nbytes, const char *file, \
            int line);
extern void *Mem_calloc(long count, long nbytes,     \
            const char * file, int line);

extern void Mem_free(void *ptr, const char *file, int line);

extern void *Mem_resize(void *ptr, long nbytes, const char *file, int line);



#define ALLOC(nbytes)  \
        Mem_alloc((nbytes), __FILE__, __LINE__)

#define CALLOC(count, nbytes)   \
        Mem_calloc((count), (nbytes), __FILE__, __LINE__)

#define NEW(p)  ((p) = ALLOC((long)sizeof*(p)))

#define NEW0(p)  ((p) = CALLOC(1, (long)sizeof*(p)))

#define FREE(ptr)   ((void) (Mem_free((ptr),  \
        __FILE__, __LINE__), (ptr) = 0))

#define RESIZE(ptr, nbytes) ((ptr) = Mem_resize((ptr),  \
        (nbytes), __FILE__, __LINE__))



#endif

memchk.c

#include 
#include 
#include "assert.h"
#include "except.h"
#include "men.h"

// size 12 in 32bit system
union align 
{
    int i;
    long l;
    long *lp;
    void *p;
    void (*fp)(void);
    float f;
    double d;
    long double ld;
};

// get hash value [0,sizeof(t)/sizeof((t)[0]) - 1]
// if t = htab value [0,2046]
#define hash(p, t)  (((unsigned long) (p) >> 3) & \
        (sizeof (t)/sizeof((t)[0])-1))

// except info
const struct Except_T Mem_Failed = {"Allocation Failed"};

// mem list
static struct descriptor 
{
    struct descriptor *free;
    struct descriptor *link;
    const void *ptr;
    long size;
    const char *file;
    int line;
} *htab[2048];

// descriptor unit
static struct descriptor freelist = {&freelist};

static struct descriptor *find(const void *ptr)
{
    struct descriptor *bp = htab[hash(ptr, htab)];

    while( bp && bp->ptr != ptr )
    {
        bp = bp->link;
    }
    return bp;
}

void Mem_free(void *ptr, const char *file, int line) 
{
    if (ptr)
    {
        struct descriptor *bp;

        if (((unsigned long )ptr)%(sizeof(union align))!=0 \
                || (bp = find(ptr)) == NULL || bp->free)
        {
            Except_raise(&Assert_Failed, file, line);
        }

        bp->free = freelist.free;
        freelist.free = bp;
    }
}

void * Mem_resize(void *ptr, long nbytes, const char *file, int line)
{
    struct descriptor *bp;
    void * newptr;

    assert(ptr);
    assert(nbytes > 0);
    if(((unsigned long)ptr) % (sizeof(union align)) != 0 \
            || (bp=find(ptr)) == NULL || bp->free)
    {
        Except_raise(&Assert_Failed, file, line);
    }

    newptr = Memalloc(nbytes, file, line);
    memcpy(newptr, ptr, nbytessize ? nbytes:bp->size);
    Mem_free(ptr, file, line);

    return newptr;
}

void * Mem__calloc(long count, long nbytes, const char *file, int line)
{
    assert(count > 0);
    assert(nbytes > 0);
    ptr = Mem_alloc(count *nbytes, file, line);
    memset(ptr, '\0', count *nbytes);
    return ptr;
}
#define NDESCRIPTORS 512
static struct descriptor *dalloc(void *ptr, long size, const char *file, int line)
{
    static struct descriptor *avail;
    static int nleft;

    if(nleft <= 0)
    {
        avail = malloc(NDESCRIPTORS * sizeof(*avail));
        if (avail == NULL)
        {
            return NULL;
        }
        nleft = NDESCRIPTORS;
    }

    avail->ptr = ptr;
    avail->size = size;
    avail->file = file;
    avail->line = line;
    avail->free = avail->link = NULL;
    nleft--;
    return avail++;
}

#define NALLOC (( 4096 + sizeof(union align) -1) / \
        (size (union align))) * (sizeof(union align))
void *Mem_alloc(long nbytes, const char *file, int line)
{
    struct descriptor *bp;
    void *ptr;

    assert(nbytes >0);
    nbytes = ((nbytes + sizeof(union align) -1) / (sizeof(union align))) \
             * (sizeof (union align));
    for (bp = freelist.free; bp; bp = bp->free)
    {
        if(bp->size > nbytes)
        {
            bp->size -= nbytes;
            ptr = (char *)bp->ptr + bp->size;
            if((bp = dalloc(ptr, nbytes, file, line)) != NULL)
            {
                unsigned h = hash(ptr, htab);
                bp->link = htab[h];
                htab[h]=bp;
                return ptr;
            }else
            {
                if (file == NULL)
                {
                    RAISE(Mem_Failed);

                }else
                {
                    Except_raise(&Mem_Failed, file, line);
                }
            }
        }
        if (bp == &freelist)
        {
            struct descriptor *newptr;
            if((ptr = malloc(nbytes+NALLOC)) == NULL 
            || (newptr = dalloc(ptr, nbytes+NALLOC, __FILE__, __LINE__)) == NULL)
            {
                if (file == NULL)
                {
                    RAISE(Mem_Failed);

                }else
                {
                    Except_raise(&Mem_Failed, file, line);
                }
            }

            newptr->free = freelist.free;
            freelist.free = newptr;

        }
    }

    assert(0);
    return NULL;

}

下面分段描述

union align 
{
    int i;
    long l;
    long *lp;
    void *p;
    void (*fp)(void);
    float f;
    double d;
    long double ld;
};

基本类型的联合体,32位系统中sizeof的大小是12,即long double的长度是12,主要用于做对齐,确保任何类型的数据都可以保存在Mem_alloc返回的块中,Mem_alloc返回的内存大小以align的长度12位最小单位,返回n*12字节的内存,这样就可以保证任意指针类型都可以使用返回的内存空间;假设n=1,申请了12个字节的内存空间,那么align中的任意类型指针使用这12个字节都是够用的,只是小于12的指针会有点浪费。如果指针是多种基本类型组合而成的结构体,则n的倍数够大就可以了。

#define hash(p, t)  (((unsigned long) (p) >> 3) & \
        (sizeof (t)/sizeof((t)[0])-1))

hash表的hash值计算方式,此处的处理方式是p的值右移3位再与上t的大小减一,控制在N=sizeof(t)/sizeof((t)[0]是计算数组大小的常用方法,这样hash序号值控制在0到(N-1)之间,包括N-1,结合下面的代码t=htab,那么hash序号值在[0,2047],刚好落在数组htab内部。

const struct Except_T Mem_Failed = {"Allocation Failed"};

异常信息定义,前一章节有描述

static struct descriptor 
{
    struct descriptor *free;
    struct descriptor *link;
    const void *ptr;
    long size;
    const char *file;
    int line;
} *htab[2048];

htab管理内存的hash数组,支持2048个链表结构,这里首先定义了descriptor描述符结构,free空闲指针,如果非NULL,则其指代的节点是空闲的,link普通链表,挂载htab[n]上,如下图所示,虚线是free,可以跳跃,整个内存管理只维护一个freelist,它是个环形结构(后面会着重阐述)。实现箭头是link,严格的以htab[n]为头,hash值相同的内存空间划分在相同链表中。
c语言接口与实现--内存管理章节理解,含实例_第1张图片

static struct descriptor freelist = {&freelist};

freelist空闲内存节点链表,静态变量,所有未占用的空闲内存单元都挂载此链表中,初始化后freelist->free的值为自身的地址,其他值为NULL或者0。

static struct descriptor *find(const void *ptr)
{
    struct descriptor *bp = htab[hash(ptr, htab)];

    while( bp && bp->ptr != ptr )
    {
        bp = bp->link;
    }
    return bp;
}

find比较好理解,通过计算输入ptr地址的hash key值找到对应的链表头,通过link搜索找到ptr的指针,返回该节点

void Mem_free(void *ptr, const char *file, int line) 
{
    if (ptr)
    {
        struct descriptor *bp;

        if (((unsigned long )ptr)%(sizeof(union align))!=0 \
                || (bp = find(ptr)) == NULL || bp->free)
        {
            Except_raise(&Assert_Failed, file, line);
        }

        bp->free = freelist.free;
        freelist.free = bp;
    }
}

这里的Mem_free不是真正的将ptr的内存空间free掉,而是将其加入到freelist链表中,以便重新使用。
c语言接口与实现--内存管理章节理解,含实例_第2张图片

void * Mem_resize(void *ptr, long nbytes, const char *file, int line)
{
    struct descriptor *bp;
    void * newptr;

    assert(ptr);
    assert(nybytes > 0);
    if(((unsigned long)ptr) % (sizeof(union align)) != 0 \
            || (pb=find(ptr)) == NULL || bp->free)
    {
        Except_raise(&Assert_Failed, file, line);
    }

    newptr = Mem_alloc(nbytes, file, line);
    memcpy(newptr, ptr, (nbytes < bp->size) ? nbytes:bp->size);
    Mem_free(ptr, file, line);

    return newptr;
}

Mem_resize 修改内存空间大小,在这里不深究Mem_alloc的具体实现,就很好理解了,重新申请一段内存空间,将原来ptr内存空间的数据拷贝到新申请的newptr中,然后Mem_free释放ptr所在的bp节点。

void * Mem_calloc(long count, long nbytes, const char *file, int line)
{
    assert(count > 0);
    assert(nbytes > 0);
    ptr = Mem_alloc(count *nbytes, file, line);
    memset(ptr, '\0', count *nbytes);
    return ptr;
}

Mem_calloc沿袭原来calloc的功能,批量申请count个nbytes大小的内存空间,然后统一初始化为’\0’。

static struct descriptor *dalloc(void *ptr, long size, const char *file, int line)
{
    static struct descriptor *avail;
    static int nleft;

    if(nleft <= 0)
    {
        avail = malloc(NDESCRIPTORS * sizeof(*avail));
        if (avail == NULL)
        {
            return NULL;
        }
        nleft = NDESCRIPTORS;
    }

    avail->ptr = ptr;
    avail->size = size;
    avail->file = file;
    avail->line = line;
    avail->free = avail->link = NULL;
    nleft--;
    return avail++;
}

dalloc多了一个步骤,将Mem_alloc申请到的ptr通过avail数组管理起来,所以链表包括freelist和htab链表数组中的单元都是avail指针结构,书中介绍是为了减少描述符被破坏的可能性,Mem_free操作的对象就是avail中的元素。具体为何有这个功能,暂时没有理解透,另外还有个疑问avail每次分配512个单元,用完了就重新申请,这些节点最后是否需要释放,如何释放,为什么大小为512,本例没有相关描述,或许是作为内存的管理单元,由于申请的ptr是永不释放的(本例申请的内存没有调用过系统函数free,所以不释放),所以管理者avail也无需释放。

void *Mem_alloc(long nbytes, const char *file, int line)
{
    struct descriptor *bp; // 描述符
    void *ptr;   // 申请的void* 内存空间

    assert(nbytes >0);
    nbytes = ((nbytes + sizeof(union align) -1) / (sizeof(union align))) * (sizeof (union align));
    for (bp = freelist.free; bp; bp = bp->free)
    {
        if(bp->size > nbytes)
        {
            bp->size -= nbytes;
            ptr = (char *)bp->ptr + bp->size;
            if((bp = dalloc(ptr, nbytes, file, line)) != NULL)
            {
                unsigned h = hash(ptr, htab);
                bp->link = htab[h];
                htab[h]=bp;
                return ptr;
            }else
            {
                if (file == NULL)
                {
                    RAISE(Mem_Failed);

                }else
                {
                    Except_raise(&Mem_Failed, line, line);
                }
            }
        }
        if (bp == &freelist)
        {
            struct descriptor *newptr;
            if((ptr = malloc(nbytes+NALLOC)) == NULL 
            || (newptr = dalloc(ptr, nbytes+NALLOC, __FILE__, __LINE__)) == NULL)
            {
                if (file == NULL)
                {
                    RAISE(Mem_Failed);

                }else
                {
                    Except_raise(&Mem_Failed, line, line);
                }
            }

            newptr->free = freelist.free;
            freelist.free = newptr;

        }
    }

    assert(0);
    return NULL;

}

Mem_alloc这部分代码很多,是这个内存管理方法的核心部分,所以需要啰嗦一些,重点部分分开说明。

nbytes = ((nbytes + sizeof(union align) -1) / (sizeof(union align))) * (sizeof (union align));

保证申请的nbytes最小为sizeof(union align),此时nbytes=1,32位系统为12,且为12的倍数,前面已经说明如果申请的内存空间是12的倍数,那么任意组合的结构体定义都可以使用nbytes大小的空间。

接下来是for循环,基本思路是从freelist.free开始,循环查找空闲链表中的空闲单元,找到第一个大小合适的bp->size > nbytes的就从bp->ptr里面划分出nbytes大小给ptr,ptr通过dalloc纳入到avail数组中,返回的bp节点纳入到htab表中,在dalloc中会将此节点的free赋值NULL,表示已占用非空闲。
c语言接口与实现--内存管理章节理解,含实例_第3张图片
如果没有找到,则bp=bp->free,查找下一个free节点,直到bp=NULL(理论上是不会出现的),退出循环,申请内存失败;或者是bp=&freelist,即在环形链表中从freelist开始找了一圈,又回到freelist都没有合适大小的空闲内存可以用,此时通过

if (bp == &freelist)

向系统重新申请新的足够大的内存单元来分配。新的newptr同样作为空间内存加入到freelist中,挂载到freelist.free上,在下一个循环是bp=newptr,因为bp->size=nbytes+NALLOC > nbytes, 进入

if(bp->size > nbytes)
        {
            bp->size -= nbytes;
            ....
            ....

执行内存分配。

这来在说一下NALLOC 定义为

#define NALLOC (( 4096 + sizeof(union align) -1) / \
        (size (union align))) * (sizeof(union align))

和前面一样也是保证大小对齐到align的大小,这样种方式处理方式可以用在其他场合,满足对齐要求(12的整数倍),分配的大小又大于且接近于4096

总结,这种内存管理方式,通过两种链表来管理,一种是空闲链表,用于分配;另外一种是link,放置到htab的哈希数组中,用于查找和释放。优点是使用则只需要负责申请,其他一概不管,管理由memchk来负责;确定是会形成很多大小等于align的内存节点,不能用于再分配,没有回收机制会造成浪费。在频繁申请释放的极端情况下,会消耗大量的内存,系统内存吃紧,而freelist上缺存在大量的align大小的内存空间节点不能回收使用;所以作者在习题中指出实现一种合并相邻空闲块机制,能够回收这样的内存资源。

本章后面习题5.3,设计算法合并相邻的两个空闲块,我是这样考虑的:相邻的个空闲块,只有地址是连续的才可以合并,即bp->ptr+bp->size = bp->free->ptr,且合并后的bp->size = bp->size+bp->free->size,且需要在空闲链表和htab中将靠后的这个节点删除,具体代码如下(未验证)

新增查找前节点函数findfront方便在htab中删除两个空闲块中后面这块

static struct descriptor *findfront(const void *ptr)
{
    struct descriptor *bp = htab[hash(ptr, htab)];
    struct descriptor *bp_f;
    while( bp && bp->ptr != ptr )
    {
        bp_f = bp;
        bp = bp->link;
    }
    return bp_f;
}

// 在Mem_calloc的for循环中插入
if(bp->size <= nbytes && bp != &freelist)
        {
            bpn = bp->free;
            if(bpn && bp_f->free && ((unsigned long)bp->ptr+bp->size = bpn->ptr))
            {
              // 在空闲链表中删除靠后的空闲块
               bp->size += bpn->size;
               bp->free = bpn->free;
               bpn->free = NULL;

               // 在htab中找到后一空闲块的前置节点,删除空闲块
               bp_l = findfront(bpn->ptr);
               if(bp_l->link)
               {
                   bp_l->link = bp_l->link->link;
               }
               // 删除掉avail的空间
               free(bpn);
               bpn = NULL;
            }

        }

实例代码
mem_main.c

#include 
#include "mem.h"


typedef struct stMULDATA
{
    unsigned char ucData;
    unsigned short usData;
    unsigned int uiData;
    float     fData;
}MULDATA;

void main(void)
{
    char * cbuff;
    int * ibuff;
    char *str;
    MULDATA *mulDat;

    NEW(cbuff);

    *cbuff = 'A';
    printf("cbuff add:%p, *cbuff:%c\n",cbuff, *cbuff);

    NEW(ibuff);
    *ibuff = 1;
    printf("ibuff:%d\n", *ibuff);

    NEW(mulDat);

    mulDat->fData = 100.1;
    printf("mulDat->fData:%f", mulDat->fData);

    RESIZE(cbuff, 100);
    strcpy(cbuff, "Hello world");
    printf("cbuff add:%p, cbuff:%s\n", cbuff, cbuff);
    str = cbuff;

    FREE(cbuff);
    printf("cbuff add:%p, cbuff:%s\n", cbuff, cbuff);
    printf("str add:%p, str:%s\n", str, str);



}

运行结果出现了两种,一种是成功的,另外一种是失败出现异常,如图所示
c语言接口与实现--内存管理章节理解,含实例_第4张图片
红色框为异常,蓝色框为正常;红色框中在RESIZE时,由于ptr的地址值不是align的整数倍,所以产生了异常,目前还没有看到代码中是如何保证申请代码空间时返回的ptr与align对齐的

你可能感兴趣的:(c语言接口与实现,程序设计,c语言教程)