《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值相同的内存空间划分在相同链表中。
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链表中,以便重新使用。
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,表示已占用非空闲。
如果没有找到,则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);
}
运行结果出现了两种,一种是成功的,另外一种是失败出现异常,如图所示
红色框为异常,蓝色框为正常;红色框中在RESIZE时,由于ptr的地址值不是align的整数倍,所以产生了异常,目前还没有看到代码中是如何保证申请代码空间时返回的ptr与align对齐的