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如下所示:


如果大了,也繼續往後,並且記錄下到目前爲止最小的(這樣到最後就找到了最小的)。

到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項目的源碼可以在線瀏覽:

arv-libc/libc/malloc.c

另外,sdcc(Small Device C Compiler)是一個開源的單片機編譯器,它也實現了malloc和free,項目首頁:

http://sdcc.sourceforge.net/ 感興趣的同學可自己下載源碼。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章