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如下所示:
如果大了,也继续往后,并且记录下到目前为止最小的(这样到最后就找到了最小的)。
到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携带的内存大小:
这三行“
cp = (char *)sfp1;
s -= len;
cp += s;
”执行完之后s,cp如下(其他没变),cp处将被切开,马上就能看到,
接下来“
sfp2 = (struct __freelist *)cp;
sfp2->sz = len;
”如同刚才的cp处已经建立了一个新的freelist节点,这里记录sz的作用是为了以后free(p)之时能够知道p所属的chunk有多大。
接着“
sfp1->sz = s - sizeof(size_t);
”更新原来chunk的sz,
大功告成!可以上交了,
图中ret指示的就是malloc实际返回的地址,malloc(len)想要取得的内存已经到手!
malloc的最后还有几行是最后一种情况,即整个链表上没有一个chunk可以满足要求(第一次调用也是这种情况,因为全局变量__flp,__brkval的初值都是0);
通过注释step 3可以知道:这里要准备一个新的chunk,也就是要获取更多的内存。
这段代码和__brkval变量相关,另外和STACK_POINTER(SP)也相关,这里涉及到内存布局的问题,__brkval,SP,__heap_start,__heap_end的关系可以从下图有个大致的了解:
(图片来自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关系如下,
接着,如果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可能的情况如下:
free的任务就是将中间灰色的chunk重新“挂”到freelist上,同时还要检查是否能够合并(和fp1或fp2所指chunk相邻),如果能够合并,则将该chunk和它相邻的chunk合并起来。
接下来是:
cp1 = (char *)fp1;
fpnew->nx = fp1;
将当前chunk的nx域与后面的chunk连接起来:
其后的代码;
if ((char *)&(fpnew->nx) + fpnew->sz == cp1) 对应的状态:
接下来:
if ((char*)&(fpnew->nx)
+ fpnew->sz ==cp1) {
/*upperchunk adjacent, assimilate it */
fpnew->sz+=
fp1->sz+ sizeof(size_t);
fpnew->nx
=fp1->nx;
}
更新fpnew->sz,对应的状态:
接着是:
if ((char*)&(fpnew->nx)
+ fpnew->sz ==cp1) {
/*upperchunk adjacent, assimilate it */
fpnew->sz
+=fp1->sz +sizeof(size_t);
fpnew->nx=
fp1->nx;
}
更新fpnew->nx,对应的状态:
接下来是必然会执行的:
fp2->nx=fpnew;//
make a new link
对应着:
至此,能够和后面chunk合并这一情况对应的free工作完成。
(ps: 画图太累,其他几种情况就不再画图了。)
扩展阅读
这篇文章是去年我在看完Keil malloc之后写的,我的另一篇关于Keil内存管理的文章:
Pooled Allocation(池式分配)实例——Keil 内存管理
除此之外,avr-libc项目的源码可以在线浏览:
另外,sdcc(Small Device C Compiler)是一个开源的单片机编译器,它也实现了malloc和free,项目首页:
http://sdcc.sourceforge.net/ 感兴趣的同学可自己下载源码。