avr-libc malloc/free的实现

avr-libc是AVR单片机C语言运行库,它提供了GNU Toolset的AVR版本(Binutils, GCC, GDB, etc.),它是nongnu.org下的一个项目,以Modified BSD License发布。想看源码的同学可去其网站自行下载:

Home Page:http://www.nongnu.org/avr-libc/ Detail Page:http://savannah.nongnu.org/projects/avr-libc/

当然,也可以用:

svn checkout svn://svn.sv.gnu.org/avr-libc/trunk avr-libc

check最新版本的源码。

我check了两次,都报svn: E155009错误,于是换了一个稳定的release版check:

svn checkout svn://svn.sv.gnu.org/avr-libc/tags/avr-libc-1_8_0-release avr-libc-1_8_0

这次正常,没有报错。可以用tree命令列出整个项目的结构,

我们要看的malloc位于avr-libc/libc/stdlib下面.这里和malloc过程相关的一共4个内部文件:

sectionname.h

stdlib_private.h

malloc.c

realloc.c

这些代码中给出的注释已经比较详细,这里我主要以图示的方法对各个步骤进行演示。为方便阅读,部分注释、测试代码、版权声明已被删除。版权声明请参照原始源码。


stdlib_private.h

sectioinname.h里仅有几行代码,其作用是让编译器正确安放代码对应的存储区段,和这里的主题没有多少关系,可以直接跳过;
先看stdlib_private.h里的代码:
#if !defined(__DOXYGEN__)

struct __freelist {
	size_t sz; // size
	struct __freelist *nx; // next
}; // 空闲链表 节点

#endif

extern char *__brkval;		/* first location not yet allocated */
extern struct __freelist *__flp; /* freelist pointer (head of freelist) */
extern size_t __malloc_margin;	/* user-changeable before the first malloc() */
extern char *__malloc_heap_start;
extern char *__malloc_heap_end;
         
extern char __heap_start;
extern char __heap_end;

/* Needed for definition of AVR_STACK_POINTER_REG. */
#include <avr/io.h>

#define STACK_POINTER() ((char *)AVR_STACK_POINTER_REG)
stdlib_private.h里定义了freelist的节点结构,以及malloc.c,realloc.c里都要访问的几个全局变量。
由freelist的节点是这样的:

malloc

malloc.c里实现了malloc和free,和前篇 Keil实现版相比,二者大体思路非常相似,但又有区别。先看代码(注释比较详细):
void *
malloc(size_t len)
{
	struct __freelist *fp1, *fp2, *sfp1, *sfp2;
	char *cp;
	size_t s, avail;

	/*
	 * Our minimum chunk size is the size of a pointer (plus the
	 * size of the "sz" field, but we don't need to account for
	 * this), otherwise we could not possibly fit a freelist entry
	 * into the chunk later.
	 */ // malloc要交出的区块,至少是一个指针的大小(后面将会看到原因)
	if (len < sizeof(struct __freelist) - sizeof(size_t))
		len = sizeof(struct __freelist) - sizeof(size_t);

	/*
	 * First, walk the free list and try finding a chunk that
	 * would match exactly.  If we found one, we are done.  While
	 * walking, note down the smallest chunk we found that would
	 * still fit the request -- we need it for step 2.
	 */ 
	for (s = 0, fp1 = __flp, fp2 = 0; 
	     fp1;                          // 走到头了,跳出for
	     fp2 = fp1, fp1 = fp1->nx) { // fp1走在fp2的前面(fp2->next==fp1)
		if (fp1->sz < len) // 不够用?继续找...
			continue;
		if (fp1->sz == len) { // case 1. 正好的区块
			/*
			 * Found it.  Disconnect the chunk from the
			 * freelist, and return it.
			 */
			if (fp2)
				fp2->nx = fp1->nx;
			else 
				__flp = fp1->nx; // fp2 == 0, 此时fp1指向的是freelist head

			// 注意,这里返回的是nx域的地址
			return &(fp1->nx); 
		}
		else { // 够大!
			if (s == 0 || fp1->sz < s) {
				/* this is the smallest chunk found so far */
				s = fp1->sz; // s 是当前已经找到的“够用的”chunk中最小的了
				sfp1 = fp1;
				sfp2 = fp2;
			}
		}
	}
	/*
	 * Step 2: If we found a chunk on the freelist that would fit
	 * (but was too large), look it up again and use it, since it
	 * is our closest match now.  Since the freelist entry needs
	 * to be split into two entries then, watch out that the
	 * difference between the requested size and the size of the
	 * chunk found is large enough for another freelist entry; if
	 * not, just enlarge the request size to what we have found,
	 * and use the entire chunk.
	 */
	if (s) { // freelist上有足够大的chunk
		if (s - len < sizeof(struct __freelist)) { // case 2. (当前块) 剩下的空间大小不足放一个结点
			/* Disconnect it from freelist and return it. */
			if (sfp2)
				sfp2->nx = sfp1->nx;
			else
				__flp = sfp1->nx;
			return &(sfp1->nx);
		}
		/*
		 * Split them up.  Note that we leave the first part
		 * as the new (smaller) freelist entry, and return the
		 * upper portion to the caller.  This saves us the
		 * work to fix up the freelist chain; we just need to
		 * fixup the size of the current entry, and note down
		 * the size of the new chunk before returning it to
		 * the caller.
		 */ // case 3. (当前块)剩余空间够放一个节点 则 进行切割,一分为二
		cp = (char *)sfp1;
		s -= len;
		cp += s;
		sfp2 = (struct __freelist *)cp;
		sfp2->sz = len;
		sfp1->sz = s - sizeof(size_t);
		return &(sfp2->nx);
	}
	// freelist上没有足够大的chunk了
	/*
	 * Step 3: If the request could not be satisfied from a
	 * freelist entry, just prepare a new chunk.  This means we
	 * need to obtain more memory first.  The largest address just
	 * not allocated so far is remembered in the brkval variable.
	 * Under Unix, the "break value" was the end of the data
	 * segment as dynamically requested from the operating system.
	 * Since we don't have an operating system, just make sure
	 * that we don't collide with the stack.
	 */
	if (__brkval == 0)
		__brkval = __malloc_heap_start;
	cp = __malloc_heap_end; // __malloc_heap_start, __malloc_heap_end应该由用户在malloc调用前设置好。
	if (cp == 0)
		cp = STACK_POINTER() - __malloc_margin; // 给栈空间预留 __malloc_margin 字节的内存。防止(堆、栈)碰撞!
	if (cp <= __brkval)
	  /*
	   * Memory exhausted.
	   */
	  return 0;
	avail = cp - __brkval; // 计算 剩余可用空间
	/*
	 * Both tests below are needed to catch the case len >= 0xfffe.
	 */
	if (avail >= len && avail >= len + sizeof(size_t)) {
		fp1 = (struct __freelist *)__brkval;
		__brkval += len + sizeof(size_t); // heap “增长”
		fp1->sz = len;
		return &(fp1->nx);
	}
	/*
	 * Step 4: There's no help, just fail. :-/
	 */
	return 0;
}

第一个for循环遍历链表,
如果当前chunk不够大,就继续往后找;
如果大小正好,就将这个块(chunk)从空闲链表(freelist)上取下来(删除),并返回。删除要注意——当前的chunk是不是在freelist的头部;如果是,就把freelist头指针往后移一下;这里还要注意——返回的是&(fp1->nx),连同前面的条件if(fp1->sz==len)说明freelist一个节点上的sz表示的chunk空间包括nx域的大小(这点与Keil不同)。即一个chunk如下所示:

avr-libc malloc/free的实现_第1张图片

如果大了,也继续往后,并且记录下到目前为止最小的(这样到最后就找到了最小的)。

到Step 2,已经找到了一个可用的chunk,它是所有比len(我们所需的)大的chunk中最小的一个;
接下来看看它是不是只比我们需要的大一点?如果是,即多出的空间不够放一个节点,那我们没办法将它作为一个chunk挂到freelist上,直接返回;
否则,说明可以在多出的内存上建立一个chunk(自如可以将它挂上freelist),需要将这个chunk切为两半;
完成这一工作的就是这几行代码:

cp = (char *)sfp1;
		s -= len;
		cp += s;
		sfp2 = (struct __freelist *)cp;
		sfp2->sz = len;
		sfp1->sz = s - sizeof(size_t);
		return &(sfp2->nx);
下面以图形展示这几行代码的执行过程。这里应当明确,执行这些代码之前s为多少,sfp1、sfp2指向何处?sfp1指向那个“最合适的”(所有比len大的中最小的)sfp2紧随其后,s为这个chunk携带的内存大小:

avr-libc malloc/free的实现_第2张图片
这三行“

cp = (char *)sfp1;
s -= len;
cp += s;

”执行完之后s,cp如下(其他没变),cp处将被切开,马上就能看到,
avr-libc malloc/free的实现_第3张图片

接下来“

sfp2 = (struct __freelist *)cp;
sfp2->sz = len;

”如同刚才的cp处已经建立了一个新的freelist节点,这里记录sz的作用是为了以后free(p)之时能够知道p所属的chunk有多大。

avr-libc malloc/free的实现_第4张图片

接着“

sfp1->sz = s - sizeof(size_t);

”更新原来chunk的sz,

avr-libc malloc/free的实现_第5张图片

大功告成!可以上交了,

avr-libc malloc/free的实现_第6张图片

图中ret指示的就是malloc实际返回的地址,malloc(len)想要取得的内存已经到手!


malloc的最后还有几行是最后一种情况,即整个链表上没有一个chunk可以满足要求(第一次调用也是这种情况,因为全局变量__flp,__brkval的初值都是0);
通过注释step 3可以知道:这里要准备一个新的chunk,也就是要获取更多的内存。
这段代码和__brkval变量相关,另外和STACK_POINTER(SP)也相关,这里涉及到内存布局的问题,__brkval,SP,__heap_start,__heap_end的关系可以从下图有个大致的了解:
avr-libc malloc/free的实现_第7张图片
(图片来自http://www.nongnu.org/avr-libc/user-manual/malloc.html)
从图上可以看到stack和heap有一段公用的空间,而且增长方向想对,这就有发生碰撞(collide)的可能,而__malloc_margin的作用就是用来防止发生碰撞。
通过代码“

__brkval += len + sizeof(size_t);

”可以知道__brkval实际上是heap的上界。每次freelist不能满足malloc的请求,同时“堆栈间隙”的空间够用时,heap都会增长,并更新__brkval。

free

有了malloc的一番分析,free的代码就很容易看懂了,

void
free(void *p)
{
	struct __freelist *fp1, *fp2, *fpnew;
	char *cp1, *cp2, *cpnew;

	/* ISO C says free(NULL) must be a no-op */
	if (p == 0)
		return;

	cpnew = p;
	cpnew -= sizeof(size_t);
	fpnew = (struct __freelist *)cpnew;
	fpnew->nx = 0;

	/*
	 * Trivial case first: if there's no freelist yet, our entry
	 * will be the only one on it.  If this is the last entry, we
	 * can reduce __brkval instead.
	 */
	if (__flp == 0) { // freelist 为空
		if ((char *)p + fpnew->sz == __brkval) // fpnew->sz == __brkval - (char*)p, 要free的是最后一个chunk
			__brkval = cpnew; // heap “缩小”
		else
			__flp = fpnew;
		return;
	}

	/*
	 * Now, find the position where our new entry belongs onto the
	 * freelist.  Try to aggregate the chunk with adjacent chunks
	 * if possible.
	 */
	for (fp1 = __flp, fp2 = 0; 
	     fp1;
	     fp2 = fp1, fp1 = fp1->nx) { // fp1走在fp2的前面(fp2->next == fp1)
		if (fp1 < fpnew)  
			continue;    
		// fp1 > fpnew, fpnew > fp1
		cp1 = (char *)fp1;
		fpnew->nx = fp1;
		if ((char *)&(fpnew->nx) + fpnew->sz == cp1) {
			/* upper chunk adjacent, assimilate it */ // 和后面的chunk合并
			fpnew->sz += fp1->sz + sizeof(size_t);
			fpnew->nx = fp1->nx;
		}
		if (fp2 == 0) {
			/* new head of freelist */
			__flp = fpnew;
			return;
		}
		break;
	}
	/*
	 * Note that we get here either if we hit the "break" above,
	 * or if we fell off the end of the loop.  The latter means
	 * we've got a new topmost chunk.  Either way, try aggregating
	 * with the lower chunk if possible.
	 */
	fp2->nx = fpnew;
	cp2 = (char *)&(fp2->nx);
	if (cp2 + fp2->sz == cpnew) {// 可以和前面的节点合并
		/* lower junk adjacent, merge */ // 和前面的chunk合并
		fp2->sz += fpnew->
		sz + sizeof(size_t);fp2->
		nx = fpnew->nx;
	}
	/*
	 * If there's a new topmost chunk, lower __brkval instead.
	 */
	for (fp1 = __flp, fp2 = 0;
	     fp1->nx != 0;
	     fp2 = fp1, fp1 = fp1->nx)
		/* advance to entry just before end of list */;
		cp2 = (char *)&(fp1->nx);
	if (cp2 + fp1->sz == __brkval) {
		if (fp2 == NULL)/* Freelist is empty now. */
			__flp = NULL;
		else
			fp2->
		nx = NULL;
		__brkval = cp2 - sizeof(size_t);
	}
}


开头的几行“

cpnew = p;
cpnew -= sizeof(size_t);
fpnew = (struct __freelist *)cpnew;

”执行后,fpnew即得到了chunk的实际地址(malloc的切口),p与cpnew、fpnew关系如下,
avr-libc malloc/free的实现_第8张图片
接着,如果freelist为空,就把当前chunk加到freelist上,如果((char *)p + fpnew->sz == __brkval),由前面的分析已经知道__brkval是heap的上界,这里的__brkval = cpnew;就是下调heap的上界。

接着是一个for循环,for循环内,开头是:
if (fp1 < fpnew)  
continue;   

for循环内最后有个break;很明显从continue到break的代码只会执行一遍,写到for循环后面的话作用也一样。也就是:

for (fp1 = __flp, fp2 = 0;
        fp1;
        fp2 = fp1, fp1 = fp1->nx) { // fp1走在fp2的前面(fp2->next == fp1)
		if (fp1 >= fpnew)
			break;
	}
	
	// fp1 > fpnew > fp2, 找到了fpnew前后的节点
	cp1 = (char *)fp1;
	fpnew->nx = fp1;
	if ((char *)&(fpnew->nx) + fpnew->sz == cp1) { // 和 后面的节点“相邻”
		/* upper chunk adjacent, assimilate it */
		fpnew->sz += fp1->sz + sizeof(size_t);
		fpnew->nx = fp1->nx;
	}
	if (fp2 == 0) {
		/* new head of freelist */
		__flp = fpnew;
		return;
	}

for循环的作用很明显,是要找当前chunk应该插入到freelist的位置;

for循环结束后有几种可能情况,处理情况类似,下面仅一其中一种,图形化展示之。


case 1 当前chunk(fpnew)可以和后面的chunk(fp1)合并

这种情况对应 (char *)&(fpnew->nx) + fpnew->sz == cp1 成立。

for循环结束时,fp1, fp2可能的情况如下:

avr-libc malloc/free的实现_第9张图片

free的任务就是将中间灰色的chunk重新“挂”到freelist上,同时还要检查是否能够合并(和fp1或fp2所指chunk相邻),如果能够合并,则将该chunk和它相邻的chunk合并起来。


接下来是:

cp1 = (char *)fp1;
fpnew->nx = fp1;

将当前chunk的nx域与后面的chunk连接起来:

avr-libc malloc/free的实现_第10张图片




其后的代码;

if ((char *)&(fpnew->nx) + fpnew->sz == cp1) 对应的状态:

avr-libc malloc/free的实现_第11张图片


接下来:

if ((char*)&(fpnew->nx) + fpnew->sz ==cp1) {

  /*upperchunk adjacent, assimilate it */

  fpnew->sz+= fp1->sz+ sizeof(size_t);

  fpnew->nx =fp1->nx;

}


更新fpnew->sz,对应的状态:

avr-libc malloc/free的实现_第12张图片



接着是:

if ((char*)&(fpnew->nx) + fpnew->sz ==cp1) {

  /*upperchunk adjacent, assimilate it */

  fpnew->sz +=fp1->sz +sizeof(size_t);

  fpnew->nx= fp1->nx;

}


更新fpnew->nx,对应的状态:

avr-libc malloc/free的实现_第13张图片




接下来是必然会执行的:

fp2->nx=fpnew;// make a new link

对应着:

avr-libc malloc/free的实现_第14张图片


至此,能够和后面chunk合并这一情况对应的free工作完成。

(ps: 画图太累,其他几种情况就不再画图了。)


扩展阅读

这篇文章是去年我在看完Keil malloc之后写的,我的另一篇关于Keil内存管理的文章:

Pooled Allocation(池式分配)实例——Keil 内存管理

除此之外,avr-libc项目的源码可以在线浏览:

arv-libc/libc/malloc.c

另外,sdcc(Small Device C Compiler)是一个开源的单片机编译器,它也实现了malloc和free,项目首页:

http://sdcc.sourceforge.net/ 感兴趣的同学可自己下载源码。

你可能感兴趣的:(malloc,内存管理,free,avr,sdcc)