1. 程式人生 > >linux-malloc底層實現原理

linux-malloc底層實現原理

本文大致講解一下linux下malloc的底層實現原理。

首先malloc肯定是從堆中分配記憶體,而堆又在使用者空間中佔據什麼位置?通過下面這張圖可以看出來:


很明顯是32位系統,定址空間是4G,linux系統下0-3G是使用者模式,3-4G是核心模式。而在使用者模式下又分為程式碼段、資料段、.bss段、堆、棧。各個segment所含內容在圖中有具體說明。

其中bss段:存放未初始化的全域性變數和區域性靜態變數。資料段:存放已經初始化的全域性變數和區域性靜態變數。至於區域性變數存放在棧中。

可以看到heap段位於bss下方,而其中有個重要的標誌:program breakLinux維護一個break指標,這個指標指向堆空間的某個地址。從堆起始地址到break之間的地址空間為對映好的,可以供程序訪問;而從break往上,是未對映的地址空間,如果訪問這段空間則程式會報錯。我們用malloc進行記憶體分配就是從break往上進行的。

程序所面對的虛擬記憶體地址空間,只有按頁對映到實體記憶體地址,才能真正使用。受物理儲存容量限制,整個堆虛擬記憶體空間不可能全部對映到實際的實體記憶體。Linux對堆的管理示意如下:

Linux程序堆管理

獲取了break地址,也就是記憶體申請的初始地址,下面是malloc的整體實現方案

malloc 函式的實質是它有一個將可用的記憶體塊連線為一個長長的列表的所謂空閒連結串列。 呼叫 malloc()函式時,它沿著連線表尋找一個大到足以滿足使用者請求所需要的記憶體塊。 然後,將該記憶體塊一分為二(一塊的大小與使用者申請的大小相等,另一塊的大小就是剩下來的位元組)。 接下來,將分配給使用者的那塊記憶體儲存區域傳給使用者,並將剩下的那塊(如果有的話)返回到連線表上。 呼叫 free 函式時,它將使用者釋放的記憶體塊連線到空閒連結串列上。 到最後,空閒鏈會被切成很多的小記憶體片段,如果這時使用者申請一個大的記憶體片段, 那麼空閒連結串列上可能沒有可以滿足使用者要求的片段了。於是,malloc()函式請求延時,並開始在空閒連結串列上檢查各記憶體片段,對它們

進行記憶體整理,將相鄰的小空閒塊合併成較大的記憶體塊。

1、malloc分配記憶體前的初始化:

malloc_init 是初始化記憶體分配程式的函式。 它完成以下三個目的:將分配程式標識為已經初始化[3],找到作業系統中最後一個有效的記憶體地址,然後建立起指向需要管理的記憶體的指標。這裡需要用到三個全域性變數。

 malloc_init 分配程式的全域性變數

int has_initialized = 0; /* 初始化標記 */

void *managed_memory_start; /* 管理記憶體起始地址 */

void *last_valid_address; /* 作業系統的最後一個有效地址*/

被對映的記憶體邊界(作業系統最後一個有效地址)常被稱為系統中斷點或者當前中斷點。為了指出當前系統中斷點,必須使用 sbrk(0) 函式。 sbrk 函式根據引數中給出的位元組數移動當前系統中斷點,然後返回新的系統中斷點。 使用引數 0 只是返回當前中斷點。 這裡給出 malloc()初始化程式碼,它將找到當前中斷點並初始化所需的變數:

Linux通過brk和sbrk系統呼叫操作break指標。兩個系統呼叫的原型如下:

int brk(void *addr);
void *sbrk(intptr_t increment);

brk將break指標直接設定為某個地址,而sbrk將break從當前位置移動increment所指定的增量。brk在執行成功時返回0,否則返回-1並設定errno為ENOMEM;sbrk成功時返回break移動之前所指向的地址,否則返回(void *)-1。如果將increment設定為0,則可以獲得當前break的地址。

2、下為malloc_init()程式碼:可以看到使用sbrk(0)來獲得break地址。

#include <unistd.h> /*sbrk 函式所在的標頭檔案 */
void malloc_init()
{
last_valid_address = sbrk(0); /* 用 sbrk 函式在作業系統中
取得最後一個有效地址 */
managed_memory_start = last_valid_address; /* 將 最 後 一 個
有效地址作為管理記憶體的起始地址 */
has_initialized = 1; /* 初始化成功標記 */
}

3、記憶體塊的獲取

所要申請的記憶體是由多個記憶體塊構成的連結串列。

a、記憶體塊的大致結構:每個塊由meta區和資料區組成,meta區記錄資料塊的元資訊(資料區大小、空閒標誌位、指標等等),資料區是真實分配的記憶體區域,並且資料區的第一個位元組地址即為malloc返回的地址。

typedef struct s_block *t_block;
struct s_block {
size_t size; /* 資料區大小 */
t_block next; /* 指向下個塊的指標 */
int free; /* 是否是空閒塊 */
int padding; /* 填充4位元組,保證meta塊長度為8的倍數 */
char data[1] /* 這是一個虛擬欄位,表示資料塊的第一個位元組,長度不應計入meta */
};
現在,為了完全地管理記憶體,我們需要能夠追蹤要分配和回收哪些記憶體。在對記憶體塊進行了 free 呼叫之後,我們需要做的是諸如將它們標記為未被使用的等事情,並且,在呼叫 malloc 時,我們要能夠定位未被使用的記憶體塊。因此, malloc 返回的每塊記憶體的起始處首先要有這個結構:
struct mem_control_block
{	
	int is_available;//是否空閒
	int size; //記憶體塊大小
};


b、尋找合適的block

現在考慮如何在block鏈中查詢合適的block。一般來說有兩種查詢演算法:

  • First fit:從頭開始,使用第一個資料區大小大於要求size的塊所謂此次分配的塊
  • Best fit:從頭開始,遍歷所有塊,使用資料區大小大於size且差值最小的塊作為此次分配的塊

  兩種方法各有千秋,best fit具有較高的記憶體使用率(payload較高),而first fit具有更好的執行效率。

find_block從frist_block開始,查詢第一個符合要求的block並返回block起始地址,如果找不到這返回NULL。這裡在遍歷時會更新一個叫last的指標,這個指標始終指向當前遍歷的block。這是為了如果找不到合適的block而開闢新block使用的。

c、如果現有block都不能滿足size的要求,則需要在連結串列最後開闢一個新的block。下為利用sbrk()建立新的block示意程式碼:

#define BLOCK_SIZE 24 /* 由於存在虛擬的data欄位,sizeof不能正確計算meta長度,這裡手工設定 */
 
t_block extend_heap(t_block last, size_t s) {
t_block b;
b = sbrk(0);
if(sbrk(BLOCK_SIZE + s) == (void *)-1)
return NULL;
b->size = s;
b->next = NULL;
if(last)
last->next = b;
b->free = 0;
return b;
}

4、記憶體分配,下為記憶體分配程式碼
void *malloc(long numbytes)
{
	void *current_location;
	struct mem_control_block *current_location_mcb;
	void *memory_location;
	if(! has_initialized)
	{
		malloc_init();
	}
	numbytes = numbytes + sizeof(struct mem_control_block);
	memory_location = 0;
	current_location = managed_memory_start;
	while(current_location ! = last_valid_address)
	{
		current_location_mcb =(struct mem_control_block *)current_location;
		if(current_location_mcb->is_available)
		{
			if(current_location_mcb->size >= numbytes)
			{
				current_location_mcb->is_available = 0;
				memory_location = current_location;
				break;
			}
		}
		current_location = current_location +current_location_mcb->size;
	}
	if(! memory_location)
	{
		sbrk(numbytes);
		memory_location = last_valid_address;
		last_valid_address = last_valid_address + numbytes;
		current_location_mcb = memory_location;
		current_location_mcb->is_available = 0;
		current_location_mcb->size = numbytes;
	}
	memory_location = memory_location + sizeof (struct mem_control_block);
	return memory_location;
}