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
#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)
malloc
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/ 感兴趣的同学可自己下载源码。