AT&T的malloc实现--malloc的基础和本质

malloc做为标准c的一个内存分配调用想必每个搞过C语言的都用过,然而在这个很经常使用的统一接口下面却有着N种不一样的实现,linux的glibc有本身的实现,windows的crt有本身的实现,这些实现都有着本身的策略,特别是glibc的实现让人看的头晕,crt的实现虽然简单可是有着策略感受很傻,最原始并且最能说明本质的实现我认为仍是贝尔实验室的实现,很简单,先后不超过60行代码,让人读后心旷神怡,连同free的实现一同构成了一幅美丽的图景,本质上贝尔实验室的malloc使用了一个线性的链表来表示能够分配的内存块,熟悉伙伴系统和slab的应该知道这是这么回事,可是仍然和slab和伙伴系统有所不一样,slab中分配的都是相同大小的内存块,而伙伴系统中分配的是不一样肯定大小的内存块,好比肯定的1,2,4,8...的内存,贝尔试验室的malloc版本是一种随意的分配,自己遵循碎片最少化,利用率最大化的原则分配,其实想一想slab的初衷,其实就是一个池的概念,省去了分配时的开销,而伙伴系统的提出就是为了消除碎片,贝尔malloc版本一箭双雕,虽然如今不少人已经遗忘了这个版本或者你能够说glibc或者crt的实现已经超越了这个版本,可是这些实现或多或少的给蛇添了足,没有贝尔malloc实现的那么纯粹。如今首先看一下基本的数据结构:linux

typedef struct mem{ windows

struct mem *next; 数据结构

Unsigned len; 并发

}mem; ide

这个数据结构表示一切的内存块slot,每个slot表明一块内存,其中的len表示其长度,这个结构体其实就是一个头的概念,真正的数据附着在这个结构体的后面相邻的地方,这个结构体设计的巧妙之处在于它可让malloc和free更简单的实现,正式因为它的第一个字段是一个mem*类型的,妙在哪里呢?只有看了malloc才知道:函数

mem *F; //一个全局的mem*链表,F的含义就是Free,它指向当前空闲链表的头,该链表是一个线性链表布局

void *malloc(unsigned size)操作系统

{ 设计

register mem *p, *q, *r, *s; 指针

unsigned register k, m;

extern char *sbrk(int);

char *top, *top1;

size = (size+7) & ~7; //字节对齐

r = (mem *) &F; //取链表头的地址,该头自己就是一个指针类型,所以r其实是指针的指针,正是因为mem的第一个字段是一个mem*类型,它才能够转化为mem*类型的,F取地址后就是F的地址,存储的是F,而F是一个men指针,所以&F地址处的数据就是一个mem*类型,将之转化为mem*以后,其数据整好是mem的next字段,注意此时它的len字段无效,这个转化的意义就在于r的next就是F

for (p = F, q = 0; p; r = p, p = p->next) //遍历空闲链表,直到F的前驱为NULL

{

if ((k = p->len) >= size && ( !q || m > k)) //&以前的确保当前的空闲块知足分配要求,以后的确保找到大于size的空闲块的最小的那一块

{

m = k; //m记录当前找到的知足要求的最小块

q = p; //q记录当前知足要求的那一块

s = r; //s记录当前知足要求的前一块

}

}

if (q) //若是找了知足要求的块,要么直接分配,要么分割后分配而后重将分配后没有使用的那一块加入空闲链表

{

if (q->len - size >= MINBLK) //知足分割要求,也就是最小粒度要求

{

p = (mem *)(((char *)(q+1)) + size); //设置q的大小,这一块将从空闲链表移除,返回使用

p->next = q->next; //将分割后的新的空闲块的next设置为分割前的q的next

p->len = q->len - size - sizeof(mem); //分割后的p的len显然减小了size和mem的加和的大小

s->next = p; //将知足要求的前一块的next指向q的后一块

q->len = size; //将q分配出去,也就是从空闲链表移除

}

else //若是不能分割,那么直接将这一块q分配出去

s->next = q->next;

}

else //若是没有找到合适的空闲块能够分配,那么就要向操做系统要了

{

top = (char *)(((long)sbrk(0) + 7) & ~7); //找到当前的堆的顶部

if (F && (char *)(F+1) + F->len == top) //若是有空闲块而且空闲块就是堆顶的那一块

{

q = F; //将空闲块首指针赋给q

F = F->next; //向后推动空闲块

}

else //不然将堆顶赋给q

q = (mem *) top;

top1 = (char *)(q+1) + size; //找到新的堆顶,可是要预先分配SBGULP

if (sbrk((int)(top1-top+SBGULP)) == (Char *) -1)

return 0;

r = (mem *)top1; //r记录新的将要分配的slot的next

r->len = SBGULP - sizeof(mem);

r->next = F; //将原来的空闲链表赋给新的slot的next的next

F = r; //新的slot的下一个slot赋给新的空闲链表

q->len = size;

}

return (char *)(q+1); //返回q,q就是分配的内存

}

以上就是malloc的操做,颇有条理,而且很清晰,关键就是指针的使用,看了这个实现就会发现原来指针还能这么使用,若是mem结构体的第一个字段不是mem类型的指针,那么上述的malloc实现根本就不可能有这么简单。注意“下一个元素”有两个概念,一个是逻辑上的概念,用next字段获得,另外一个是物理上的概念,用qn = (char *)f + q->len这种方式获得,用next指针获得的下一个元素物理上不必定相邻,全部的用next连在一块儿的元素都是空闲元素,而用偏移获得的下一个元素是物理上相邻的内存块,也就是虚拟内存相邻的内存块,用此方式获得的内存块不必定是空闲块。分配也就是上面这个malloc函数,很简单的一个函数,释放其实也是颇有意思的,其实就是free函数:

void free(char *f)

{

mem *p, *q, *r;

char *pn, *qn;

if (!f)

return;

q = (mem *) ((char *)f - sizeof(mem)); //获得要释放的f所在的mem头的位置,虽然q已经从空闲链表摘除,可是它仍是本质存在的

qn = (char *)f + q->len; //在线性链表中获得q的下一个元素,不必定是空闲元素,由于它是靠内存位置来游历的

for (p = F, r = (mem *) &F; ; r = p, p = p->next)

{

if (qn == (Char *) p) //若是q的下一个元素就是p的话,那么吞并掉它,注意p必定是空闲元素,由于它是靠next指针来游历的

{

q->len += p->len + sizeof(mem); //更新q的len字段,这就是吞并p的行为

p = p->next; //因为p被将要释放的q吞并,致使p进入q内部而不复存在,其自己也就成为了一个将要被释放的内存块的一部分

}

pn = p ? ((char *) (p+1)) + p->len : 0; //获得p的下一个元素,不必定空闲

if (pn == (char *) q) //若是p的后面相邻元素就是将要释放的q,那么q就和p合并做为一个更大的空闲块存在

{

p->len += sizeof(mem) + q->len; //合并p和q

q->len = 0; //丢掉q

q->next = p; //回环,实际上已经丢弃了q

r->next = p; //这一个颇有意思,下面会专门说

break;

}

if (pn < (char *) q) //若是不相邻而且p后面相邻的元素地址比q小的话就将q连接进空闲链表

{

r->next = q; //更新连接指针

q->next = p;

break;

}

}

}

到此为止,全部的分配和释放就说完了,是否是很简单呢?上面的合并操做的目的和伙伴系统的同样,只不过伙伴系统合并的是大小固定的内存块,而这里的合并是只要相邻有合并的可能就合并而无论内存块的大小。注意上述的代码没有考虑并发和须要锁的状况,可是这就是最纯真,也是最本质的东西,不是吗?在这种简单而又能够说明本质以后,我会写两篇关于malloc的改进版本,分别是微软的和glibc的版本。

附:关于指针的一个问题

前面说过,若是不是mem结构设计得如此巧妙,那么AT&T的malloc不会这么简单,最重要的就是能够经过&F而后将之转化为mem*类型,这样这个指针就是F的前一个元素,若是不这么设计mem结构,那么可能除了next指针以外还须要一个prev指针了。注意,设F为mem指针,&F并非一个真正的mem指针,而是因为mem的第一个字段为一个mem指针,而&F在内存中应该是一个mem指针的指针,可是该指针的指针不论如何也是一个指针类型,其指向的数据正好也是一个指针,后者是mem指针类型,这正好符合mem结构体的布局,mem结构体的第一个字段就是一个mem指针类型,所以咱们能够将&F理解成F的前一个元素,由于&F的第一个字段是F,这仅仅是能够这么理解,若是不将next做为第一个字段,那么就没有这样的事,而且任何改变&F的next字段的行为都会改变F自己,除了&F是F的前一个元素这件事成立以外,它们还有别的千丝万缕的联系,这就是指针的伟大,同时也可能带来更多的困惑。

相关文章
相关标签/搜索