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/ 感興趣的同學可自己下載源碼。