0
  • 聊天消息
  • 系統(tǒng)消息
  • 評(píng)論與回復(fù)
登錄后你可以
  • 下載海量資料
  • 學(xué)習(xí)在線課程
  • 觀看技術(shù)視頻
  • 寫(xiě)文章/發(fā)帖/加入社區(qū)
會(huì)員中心
創(chuàng)作中心

完善資料讓更多小伙伴認(rèn)識(shí)你,還能領(lǐng)取20積分哦,立即完善>

3天內(nèi)不再提示

通過(guò)實(shí)現(xiàn)一個(gè)簡(jiǎn)單的malloc來(lái)描述malloc背后的機(jī)制

C語(yǔ)言專家集中營(yíng) ? 2018-01-27 23:30 ? 次閱讀

任何一個(gè)用過(guò)或?qū)W過(guò)C的人對(duì)malloc都不會(huì)陌生。大家都知道m(xù)alloc可以分配一段連續(xù)的內(nèi)存空間,并且在不再使用時(shí)可以通過(guò)free釋放掉。但是,許多程序員對(duì)malloc背后的事情并不熟悉,許多人甚至把malloc當(dāng)做操作系統(tǒng)所提供的系統(tǒng)調(diào)用或C的關(guān)鍵字。實(shí)際上,malloc只是C的標(biāo)準(zhǔn)庫(kù)中提供的一個(gè)普通函數(shù),而且實(shí)現(xiàn)malloc的基本思想并不復(fù)雜,任何一個(gè)對(duì)C和操作系統(tǒng)有些許了解的程序員都可以很容易理解。

這篇文章通過(guò)實(shí)現(xiàn)一個(gè)簡(jiǎn)單的malloc來(lái)描述malloc背后的機(jī)制。當(dāng)然與現(xiàn)有C的標(biāo)準(zhǔn)庫(kù)實(shí)現(xiàn)(例如glibc)相比,我們實(shí)現(xiàn)的malloc并不是特別高效,但是這個(gè)實(shí)現(xiàn)比目前真實(shí)的malloc實(shí)現(xiàn)要簡(jiǎn)單很多,因此易于理解。重要的是,這個(gè)實(shí)現(xiàn)和真實(shí)實(shí)現(xiàn)在基本原理上是一致的。

這篇文章將首先介紹一些所需的基本知識(shí),如操作系統(tǒng)對(duì)進(jìn)程的內(nèi)存管理以及相關(guān)的系統(tǒng)調(diào)用,然后逐步實(shí)現(xiàn)一個(gè)簡(jiǎn)單的malloc。為了簡(jiǎn)單起見(jiàn),這篇文章將只考慮x86_64體系結(jié)構(gòu),操作系統(tǒng)為Linux。

1 什么是malloc

2 預(yù)備知識(shí)

2.2.1 內(nèi)存排布

2.2.2 Heap內(nèi)存模型

2.2.3 brk與sbrk

2.2.4 資源限制與rlimit

2.1.1 虛擬內(nèi)存地址與物理內(nèi)存地址

2.1.2 頁(yè)與地址構(gòu)成

2.1.3 內(nèi)存頁(yè)與磁盤(pán)頁(yè)

2.1 Linux內(nèi)存管理

2.2 Linux進(jìn)程級(jí)內(nèi)存管理

3 實(shí)現(xiàn)malloc

3.2.1 數(shù)據(jù)結(jié)構(gòu)

3.2.2 尋找合適的block

3.2.3 開(kāi)辟新的block

3.2.4 分裂block

3.2.5 malloc的實(shí)現(xiàn)

3.2.6 calloc的實(shí)現(xiàn)

3.2.7 free的實(shí)現(xiàn)

3.2.8 realloc的實(shí)現(xiàn)

3.1 玩具實(shí)現(xiàn)

3.2 正式實(shí)現(xiàn)

3.3 遺留問(wèn)題和優(yōu)化

4 其它參考

1 什么是malloc

在實(shí)現(xiàn)malloc之前,先要相對(duì)正式地對(duì)malloc做一個(gè)定義。

根據(jù)標(biāo)準(zhǔn)C庫(kù)函數(shù)的定義,malloc具有如下原型:

void* malloc(size_t size);

這個(gè)函數(shù)要實(shí)現(xiàn)的功能是在系統(tǒng)中分配一段連續(xù)的可用的內(nèi)存,具體有如下要求:

malloc分配的內(nèi)存大小至少為size參數(shù)所指定的字節(jié)數(shù)

malloc的返回值是一個(gè)指針,指向一段可用內(nèi)存的起始地址

多次調(diào)用malloc所分配的地址不能有重疊部分,除非某次malloc所分配的地址被釋放掉

malloc應(yīng)該盡快完成內(nèi)存分配并返回(不能使用NP-hard的內(nèi)存分配算法

實(shí)現(xiàn)malloc時(shí)應(yīng)同時(shí)實(shí)現(xiàn)內(nèi)存大小調(diào)整和內(nèi)存釋放函數(shù)(即realloc和free)

對(duì)于malloc更多的說(shuō)明可以在命令行中鍵入以下命令查看:

man malloc

2 預(yù)備知識(shí)

在實(shí)現(xiàn)malloc之前,需要先解釋一些Linux系統(tǒng)內(nèi)存相關(guān)的知識(shí)。

2.1 Linux內(nèi)存管理

2.1.1 虛擬內(nèi)存地址與物理內(nèi)存地址

為了簡(jiǎn)單,現(xiàn)代操作系統(tǒng)在處理內(nèi)存地址時(shí),普遍采用虛擬內(nèi)存地址技術(shù)。即在匯編程序(或機(jī)器語(yǔ)言)層面,當(dāng)涉及內(nèi)存地址時(shí),都是使用虛擬內(nèi)存地址。采用這種技術(shù)時(shí),每個(gè)進(jìn)程仿佛自己獨(dú)享一片2N

字節(jié)的內(nèi)存,其中N是機(jī)器位數(shù)。例如在64位CPU和64位操作系統(tǒng)下,每個(gè)進(jìn)程的虛擬地址空間為264

Byte。

這種虛擬地址空間的作用主要是簡(jiǎn)化程序的編寫(xiě)及方便操作系統(tǒng)對(duì)進(jìn)程間內(nèi)存的隔離管理,真實(shí)中的進(jìn)程不太可能(也用不到)如此大的內(nèi)存空間,實(shí)際能用到的內(nèi)存取決于物理內(nèi)存大小。

由于在機(jī)器語(yǔ)言層面都是采用虛擬地址,當(dāng)實(shí)際的機(jī)器碼程序涉及到內(nèi)存操作時(shí),需要根據(jù)當(dāng)前進(jìn)程運(yùn)行的實(shí)際上下文將虛擬地址轉(zhuǎn)換為物理內(nèi)存地址,才能實(shí)現(xiàn)對(duì)真實(shí)內(nèi)存數(shù)據(jù)的操作。這個(gè)轉(zhuǎn)換一般由一個(gè)叫MMU(Memory Management Unit)的硬件完成。

2.1.2 頁(yè)與地址構(gòu)成

在現(xiàn)代操作系統(tǒng)中,不論是虛擬內(nèi)存還是物理內(nèi)存,都不是以字節(jié)為單位進(jìn)行管理的,而是以頁(yè)(Page)為單位。一個(gè)內(nèi)存頁(yè)是一段固定大小的連續(xù)內(nèi)存地址的總稱,具體到Linux中,典型的內(nèi)存頁(yè)大小為4096Byte(4K)。

所以內(nèi)存地址可以分為頁(yè)號(hào)和頁(yè)內(nèi)偏移量。下面以64位機(jī)器,4G物理內(nèi)存,4K頁(yè)大小為例,虛擬內(nèi)存地址和物理內(nèi)存地址的組成如下:

通過(guò)實(shí)現(xiàn)一個(gè)簡(jiǎn)單的malloc來(lái)描述malloc背后的機(jī)制

上面是虛擬內(nèi)存地址,下面是物理內(nèi)存地址。由于頁(yè)大小都是4K,所以頁(yè)內(nèi)便宜都是用低12位表示,而剩下的高地址表示頁(yè)號(hào)。

MMU映射單位并不是字節(jié),而是頁(yè),這個(gè)映射通過(guò)查一個(gè)常駐內(nèi)存的數(shù)據(jù)結(jié)構(gòu)頁(yè)表來(lái)實(shí)現(xiàn)。現(xiàn)在計(jì)算機(jī)具體的內(nèi)存地址映射比較復(fù)雜,為了加快速度會(huì)引入一系列緩存和優(yōu)化,例如TLB等機(jī)制。下面給出一個(gè)經(jīng)過(guò)簡(jiǎn)化的內(nèi)存地址翻譯示意圖,雖然經(jīng)過(guò)了簡(jiǎn)化,但是基本原理與現(xiàn)代計(jì)算機(jī)真實(shí)的情況的一致的。

2.1.3 內(nèi)存頁(yè)與磁盤(pán)頁(yè)

我們知道一般將內(nèi)存看做磁盤(pán)的的緩存,有時(shí)MMU在工作時(shí),會(huì)發(fā)現(xiàn)頁(yè)表表明某個(gè)內(nèi)存頁(yè)不在物理內(nèi)存中,此時(shí)會(huì)觸發(fā)一個(gè)缺頁(yè)異常(Page Fault),此時(shí)系統(tǒng)會(huì)到磁盤(pán)中相應(yīng)的地方將磁盤(pán)頁(yè)載入到內(nèi)存中,然后重新執(zhí)行由于缺頁(yè)而失敗的機(jī)器指令。關(guān)于這部分,因?yàn)榭梢钥醋鰧?duì)malloc實(shí)現(xiàn)是透明的,所以不再詳細(xì)講述,有興趣的可以參考《深入理解計(jì)算機(jī)系統(tǒng)》相關(guān)章節(jié)。

最后附上一張?jiān)诰S基百科找到的更加符合真實(shí)地址翻譯的流程供大家參考,這張圖加入了TLB和缺頁(yè)異常的流程(圖片來(lái)源頁(yè))。

通過(guò)實(shí)現(xiàn)一個(gè)簡(jiǎn)單的malloc來(lái)描述malloc背后的機(jī)制

2.2 Linux進(jìn)程級(jí)內(nèi)存管理

2.2.1 內(nèi)存排布

明白了虛擬內(nèi)存和物理內(nèi)存的關(guān)系及相關(guān)的映射機(jī)制,下面看一下具體在一個(gè)進(jìn)程內(nèi)是如何排布內(nèi)存的。

以Linux 64位系統(tǒng)為例。理論上,64bit內(nèi)存地址可用空間為0x0000000000000000 ~ 0xFFFFFFFFFFFFFFFF,這是個(gè)相當(dāng)龐大的空間,Linux實(shí)際上只用了其中一小部分(256T)。

根據(jù)Linux內(nèi)核相關(guān)文檔描述,Linux64位操作系統(tǒng)僅使用低47位,高17位做擴(kuò)展(只能是全0或全1)。所以,實(shí)際用到的地址為空間為0x0000000000000000 ~ 0x00007FFFFFFFFFFF和0xFFFF800000000000 ~ 0xFFFFFFFFFFFFFFFF,其中前面為用戶空間(User Space),后者為內(nèi)核空間(Kernel Space)。圖示如下:

通過(guò)實(shí)現(xiàn)一個(gè)簡(jiǎn)單的malloc來(lái)描述malloc背后的機(jī)制

對(duì)用戶來(lái)說(shuō),主要關(guān)注的空間是User Space。將User Space放大后,可以看到里面主要分為如下幾段:

Code:這是整個(gè)用戶空間的最低地址部分,存放的是指令(也就是程序所編譯成的可執(zhí)行機(jī)器碼)

Data:這里存放的是初始化過(guò)的全局變量

BSS:這里存放的是未初始化的全局變量

Heap:堆,這是我們本文重點(diǎn)關(guān)注的地方,堆自低地址向高地址增長(zhǎng),后面要講到的brk相關(guān)的系統(tǒng)調(diào)用就是從這里分配內(nèi)存

Mapping Area:這里是與mmap系統(tǒng)調(diào)用相關(guān)的區(qū)域。大多數(shù)實(shí)際的malloc實(shí)現(xiàn)會(huì)考慮通過(guò)mmap分配較大塊的內(nèi)存區(qū)域,本文不討論這種情況。這個(gè)區(qū)域自高地址向低地址增長(zhǎng)

Stack:這是棧區(qū)域,自高地址向低地址增長(zhǎng)

下面我們主要關(guān)注Heap區(qū)域的操作。對(duì)整個(gè)Linux內(nèi)存排布有興趣的同學(xué)可以參考其它資料。

2.2.2 Heap內(nèi)存模型

一般來(lái)說(shuō),malloc所申請(qǐng)的內(nèi)存主要從Heap區(qū)域分配(本文不考慮通過(guò)mmap申請(qǐng)大塊內(nèi)存的情況)。

由上文知道,進(jìn)程所面對(duì)的虛擬內(nèi)存地址空間,只有按頁(yè)映射到物理內(nèi)存地址,才能真正使用。受物理存儲(chǔ)容量限制,整個(gè)堆虛擬內(nèi)存空間不可能全部映射到實(shí)際的物理內(nèi)存。Linux對(duì)堆的管理示意如下:

通過(guò)實(shí)現(xiàn)一個(gè)簡(jiǎn)單的malloc來(lái)描述malloc背后的機(jī)制

Linux維護(hù)一個(gè)break指針,這個(gè)指針指向堆空間的某個(gè)地址。從堆起始地址到break之間的地址空間為映射好的,可以供進(jìn)程訪問(wèn);而從break往上,是未映射的地址空間,如果訪問(wèn)這段空間則程序會(huì)報(bào)錯(cuò)。

2.2.3 brk與sbrk

由上文知道,要增加一個(gè)進(jìn)程實(shí)際的可用堆大小,就需要將break指針向高地址移動(dòng)。Linux通過(guò)brk和sbrk系統(tǒng)調(diào)用操作break指針。兩個(gè)系統(tǒng)調(diào)用的原型如下:

int brk(void*addr);

void*sbrk(intptr_t increment);

brk將break指針直接設(shè)置為某個(gè)地址,而sbrk將break從當(dāng)前位置移動(dòng)increment所指定的增量。brk在執(zhí)行成功時(shí)返回0,否則返回-1并設(shè)置errno為ENOMEM;sbrk成功時(shí)返回break移動(dòng)之前所指向的地址,否則返回(void *)-1。

一個(gè)小技巧是,如果將increment設(shè)置為0,則可以獲得當(dāng)前break的地址。

另外需要注意的是,由于Linux是按頁(yè)進(jìn)行內(nèi)存映射的,所以如果break被設(shè)置為沒(méi)有按頁(yè)大小對(duì)齊,則系統(tǒng)實(shí)際上會(huì)在最后映射一個(gè)完整的頁(yè),從而實(shí)際已映射的內(nèi)存空間比break指向的地方要大一些。但是使用break之后的地址是很危險(xiǎn)的(盡管也許break之后確實(shí)有一小塊可用內(nèi)存地址)。

2.2.4 資源限制與rlimit

系統(tǒng)對(duì)每一個(gè)進(jìn)程所分配的資源不是無(wú)限的,包括可映射的內(nèi)存空間,因此每個(gè)進(jìn)程有一個(gè)rlimit表示當(dāng)前進(jìn)程可用的資源上限。這個(gè)限制可以通過(guò)getrlimit系統(tǒng)調(diào)用得到,下面代碼獲取當(dāng)前進(jìn)程虛擬內(nèi)存空間的rlimit:

int main(){

struct rlimit *limit =(struct rlimit *)malloc(sizeof(struct rlimit));

getrlimit(RLIMIT_AS, limit);

printf("soft limit: %ld, hard limit: %ld\n", limit->rlim_cur, limit->rlim_max);

}

其中rlimit是一個(gè)結(jié)構(gòu)體:

struct rlimit {

rlim_t rlim_cur;/* Soft limit */

rlim_t rlim_max;/* Hard limit (ceiling for rlim_cur) */

};

每種資源有軟限制和硬限制,并且可以通過(guò)setrlimit對(duì)rlimit進(jìn)行有條件設(shè)置。其中硬限制作為軟限制的上限,非特權(quán)進(jìn)程只能設(shè)置軟限制,且不能超過(guò)硬限制。

3 實(shí)現(xiàn)malloc

3.1 玩具實(shí)現(xiàn)

在正式開(kāi)始討論malloc的實(shí)現(xiàn)前,我們可以利用上述知識(shí)實(shí)現(xiàn)一個(gè)簡(jiǎn)單但幾乎沒(méi)法用于真實(shí)的玩具malloc,權(quán)當(dāng)對(duì)上面知識(shí)的復(fù)習(xí):

/* 一個(gè)玩具malloc */

#include

#include

void*malloc(size_t size)

{

void*p;

p = sbrk(0);

if(sbrk(size)==(void*)-1)

return NULL;

return p;

}

這個(gè)malloc每次都在當(dāng)前break的基礎(chǔ)上增加size所指定的字節(jié)數(shù),并將之前break的地址返回。這個(gè)malloc由于對(duì)所分配的內(nèi)存缺乏記錄,不便于內(nèi)存釋放,所以無(wú)法用于真實(shí)場(chǎng)景。

3.2 正式實(shí)現(xiàn)

下面嚴(yán)肅點(diǎn)討論malloc的實(shí)現(xiàn)方案。

3.2.1 數(shù)據(jù)結(jié)構(gòu)

首先我們要確定所采用的數(shù)據(jù)結(jié)構(gòu)。一個(gè)簡(jiǎn)單可行方案是將堆內(nèi)存空間以塊(Block)的形式組織起來(lái),每個(gè)塊由meta區(qū)和數(shù)據(jù)區(qū)組成,meta區(qū)記錄數(shù)據(jù)塊的元信息(數(shù)據(jù)區(qū)大小、空閑標(biāo)志位、指針等等),數(shù)據(jù)區(qū)是真實(shí)分配的內(nèi)存區(qū)域,并且數(shù)據(jù)區(qū)的第一個(gè)字節(jié)地址即為malloc返回的地址。

可以用如下結(jié)構(gòu)體定義一個(gè)block:

typedefstruct s_block *t_block;

struct s_block {

size_t size;/* 數(shù)據(jù)區(qū)大小 */

t_block next;/* 指向下個(gè)塊的指針 */

int free; /* 是否是空閑塊 */

int padding;/* 填充4字節(jié),保證meta塊長(zhǎng)度為8的倍數(shù) */

char data[1]/* 這是一個(gè)虛擬字段,表示數(shù)據(jù)塊的第一個(gè)字節(jié),長(zhǎng)度不應(yīng)計(jì)入meta */

};

由于我們只考慮64位機(jī)器,為了方便,我們?cè)诮Y(jié)構(gòu)體最后填充一個(gè)int,使得結(jié)構(gòu)體本身的長(zhǎng)度為8的倍數(shù),以便內(nèi)存對(duì)齊。示意圖如下:

通過(guò)實(shí)現(xiàn)一個(gè)簡(jiǎn)單的malloc來(lái)描述malloc背后的機(jī)制

3.2.2 尋找合適的block

現(xiàn)在考慮如何在block鏈中查找合適的block。一般來(lái)說(shuō)有兩種查找算法:

First fit:從頭開(kāi)始,使用第一個(gè)數(shù)據(jù)區(qū)大小大于要求size的塊所謂此次分配的塊

Best fit:從頭開(kāi)始,遍歷所有塊,使用數(shù)據(jù)區(qū)大小大于size且差值最小的塊作為此次分配的塊

兩種方法各有千秋,best fit具有較高的內(nèi)存使用率(payload較高),而first fit具有更好的運(yùn)行效率。這里我們采用first fit算法。

/* First fit */

t_block find_block(t_block *last,size_t size){

t_block b = first_block;

while(b &&!(b->free && b->size >= size)){

*last = b;

b = b->next;

}

return b;

}

find_block從frist_block開(kāi)始,查找第一個(gè)符合要求的block并返回block起始地址,如果找不到這返回NULL。這里在遍歷時(shí)會(huì)更新一個(gè)叫l(wèi)ast的指針,這個(gè)指針始終指向當(dāng)前遍歷的block。這是為了如果找不到合適的block而開(kāi)辟新block使用的,具體會(huì)在接下來(lái)的一節(jié)用到。

3.2.3 開(kāi)辟新的block

如果現(xiàn)有block都不能滿足size的要求,則需要在鏈表最后開(kāi)辟一個(gè)新的block。這里關(guān)鍵是如何只使用sbrk創(chuàng)建一個(gè)struct:

#define BLOCK_SIZE 24/* 由于存在虛擬的data字段,sizeof不能正確計(jì)算meta長(zhǎng)度,這里手工設(shè)置 */

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;

}

3.2.4 分裂block

First fit有一個(gè)比較致命的缺點(diǎn),就是可能會(huì)讓很小的size占據(jù)很大的一塊block,此時(shí),為了提高payload,應(yīng)該在剩余數(shù)據(jù)區(qū)足夠大的情況下,將其分裂為一個(gè)新的block,示意如下:

通過(guò)實(shí)現(xiàn)一個(gè)簡(jiǎn)單的malloc來(lái)描述malloc背后的機(jī)制

實(shí)現(xiàn)代碼:

void split_block(t_block b,size_t s){

t_block new;

new= b->data + s;

new->size = b->size - s - BLOCK_SIZE ;

new->next = b->next;

new->free =1;

b->size = s;

b->next =new;

}

3.2.5 malloc的實(shí)現(xiàn)

有了上面的代碼,我們可以利用它們整合成一個(gè)簡(jiǎn)單但初步可用的malloc。注意首先我們要定義個(gè)block鏈表的頭first_block,初始化為NULL;另外,我們需要剩余空間至少有BLOCK_SIZE + 8才執(zhí)行分裂操作。

由于我們希望malloc分配的數(shù)據(jù)區(qū)是按8字節(jié)對(duì)齊,所以在size不為8的倍數(shù)時(shí),我們需要將size調(diào)整為大于size的最小的8的倍數(shù):

size_t align8(size_t s){

if(s &0x7==0)

return s;

return((s >>3)+1)<<3;

}

#define BLOCK_SIZE 24

void*first_block=NULL;

/* other functions... */

void*malloc(size_t size){

t_block b, last;

size_t s;

/* 對(duì)齊地址 */

s = align8(size);

if(first_block){

/* 查找合適的block */

last = first_block;

b = find_block(&last, s);

if(b){

/* 如果可以,則分裂 */

if((b->size - s)>=( BLOCK_SIZE +8))

split_block(b, s);

b->free =0;

}else{

/* 沒(méi)有合適的block,開(kāi)辟一個(gè)新的 */

b = extend_heap(last, s);

if(!b)

return NULL;

}

}else{

b = extend_heap(NULL, s);

if(!b)

return NULL;

first_block = b;

}

return b->data;

}

3.2.6 calloc的實(shí)現(xiàn)

有了malloc,實(shí)現(xiàn)calloc只要兩步:

malloc一段內(nèi)存

將數(shù)據(jù)區(qū)內(nèi)容置為0

由于我們的數(shù)據(jù)區(qū)是按8字節(jié)對(duì)齊的,所以為了提高效率,我們可以每8字節(jié)一組置0,而不是一個(gè)一個(gè)字節(jié)設(shè)置。我們可以通過(guò)新建一個(gè)size_t指針,將內(nèi)存區(qū)域強(qiáng)制看做size_t類型來(lái)實(shí)現(xiàn)。

void*calloc(size_t number,size_t size){

size_t*new;

size_t s8, i;

new= malloc(number * size);

if(new){

s8 = align8(number * size)>>3;

for(i =0; i < s8; i++)

new[i]=0;

}

returnnew;

}

3.2.7 free的實(shí)現(xiàn)

free的實(shí)現(xiàn)并不像看上去那么簡(jiǎn)單,這里我們要解決兩個(gè)關(guān)鍵問(wèn)題:

如何驗(yàn)證所傳入的地址是有效地址,即確實(shí)是通過(guò)malloc方式分配的數(shù)據(jù)區(qū)首地址

如何解決碎片問(wèn)題

首先我們要保證傳入free的地址是有效的,這個(gè)有效包括兩方面:

地址應(yīng)該在之前malloc所分配的區(qū)域內(nèi),即在first_block和當(dāng)前break指針?lè)秶鷥?nèi)

這個(gè)地址確實(shí)是之前通過(guò)我們自己的malloc分配的

第一個(gè)問(wèn)題比較好解決,只要進(jìn)行地址比較就可以了,關(guān)鍵是第二個(gè)問(wèn)題。這里有兩種解決方案:一是在結(jié)構(gòu)體內(nèi)埋一個(gè)magic number字段,free之前通過(guò)相對(duì)偏移檢查特定位置的值是否為我們?cè)O(shè)置的magic number,另一種方法是在結(jié)構(gòu)體內(nèi)增加一個(gè)magic pointer,這個(gè)指針指向數(shù)據(jù)區(qū)的第一個(gè)字節(jié)(也就是在合法時(shí)free時(shí)傳入的地址),我們?cè)趂ree前檢查magic pointer是否指向參數(shù)所指地址。這里我們采用第二種方案:

首先我們?cè)诮Y(jié)構(gòu)體中增加magic pointer(同時(shí)要修改BLOCK_SIZE):

typedefstruct s_block *t_block;

struct s_block {

size_t size;/* 數(shù)據(jù)區(qū)大小 */

t_block next;/* 指向下個(gè)塊的指針 */

int free; /* 是否是空閑塊 */

int padding;/* 填充4字節(jié),保證meta塊長(zhǎng)度為8的倍數(shù) */

void*ptr; /* Magic pointer,指向data */

char data[1]/* 這是一個(gè)虛擬字段,表示數(shù)據(jù)塊的第一個(gè)字節(jié),長(zhǎng)度不應(yīng)計(jì)入meta */

};

然后我們定義檢查地址合法性的函數(shù):

t_block get_block(void*p){

char*tmp;

tmp = p;

return(p = tmp -= BLOCK_SIZE);

}

int valid_addr(void*p){

if(first_block){

if(p > first_block && p < sbrk(0)){

return p ==(get_block(p))->ptr;

}

}

return0;

}

當(dāng)多次malloc和free后,整個(gè)內(nèi)存池可能會(huì)產(chǎn)生很多碎片block,這些block很小,經(jīng)常無(wú)法使用,甚至出現(xiàn)許多碎片連在一起,雖然總體能滿足某此malloc要求,但是由于分割成了多個(gè)小block而無(wú)法fit,這就是碎片問(wèn)題。

一個(gè)簡(jiǎn)單的解決方式時(shí)當(dāng)free某個(gè)block時(shí),如果發(fā)現(xiàn)它相鄰的block也是free的,則將block和相鄰block合并。為了滿足這個(gè)實(shí)現(xiàn),需要將s_block改為雙向鏈表。修改后的block結(jié)構(gòu)如下:

typedefstruct s_block *t_block;

struct s_block {

size_t size;/* 數(shù)據(jù)區(qū)大小 */

t_block prev;/* 指向上個(gè)塊的指針 */

t_block next;/* 指向下個(gè)塊的指針 */

int free; /* 是否是空閑塊 */

int padding;/* 填充4字節(jié),保證meta塊長(zhǎng)度為8的倍數(shù) */

void*ptr; /* Magic pointer,指向data */

char data[1]/* 這是一個(gè)虛擬字段,表示數(shù)據(jù)塊的第一個(gè)字節(jié),長(zhǎng)度不應(yīng)計(jì)入meta */

};

合并方法如下:

t_block fusion(t_block b){

if(b->next && b->next->free){

b->size += BLOCK_SIZE + b->next->size;

b->next = b->next->next;

if(b->next)

b->next->prev = b;

}

return b;

}

有了上述方法,free的實(shí)現(xiàn)思路就比較清晰了:首先檢查參數(shù)地址的合法性,如果不合法則不做任何事;否則,將此block的free標(biāo)為1,并且在可以的情況下與后面的block進(jìn)行合并。如果當(dāng)前是最后一個(gè)block,則回退break指針釋放進(jìn)程內(nèi)存,如果當(dāng)前block是最后一個(gè)block,則回退break指針并設(shè)置first_block為NULL。實(shí)現(xiàn)如下:

void free(void*p){

t_block b;

if(valid_addr(p)){

b = get_block(p);

b->free =1;

if(b->prev && b->prev->free)

b = fusion(b->prev);

if(b->next)

fusion(b);

else{

if(b->prev)

b->prev->prev = NULL;

else

first_block = NULL;

brk(b);

}

}

}

3.2.8 realloc的實(shí)現(xiàn)

為了實(shí)現(xiàn)realloc,我們首先要實(shí)現(xiàn)一個(gè)內(nèi)存復(fù)制方法。如同calloc一樣,為了效率,我們以8字節(jié)為單位進(jìn)行復(fù)制:

void copy_block(t_block src, t_block dst){

size_t*sdata,*ddata;

size_t i;

sdata = src->ptr;

ddata = dst->ptr;

for(i =0;(i *8)< src->size &&(i *8)< dst->size; i++)

ddata[i]= sdata[i];

}

然后我們開(kāi)始實(shí)現(xiàn)realloc。一個(gè)簡(jiǎn)單(但是低效)的方法是malloc一段內(nèi)存,然后將數(shù)據(jù)復(fù)制過(guò)去。但是我們可以做的更高效,具體可以考慮以下幾個(gè)方面:

如果當(dāng)前block的數(shù)據(jù)區(qū)大于等于realloc所要求的size,則不做任何操作

如果新的size變小了,考慮split

如果當(dāng)前block的數(shù)據(jù)區(qū)不能滿足size,但是其后繼block是free的,并且合并后可以滿足,則考慮做合并

下面是realloc的實(shí)現(xiàn):

void*realloc(void*p,size_t size){

size_t s;

t_block b,new;

void*newp;

if(!p)

/* 根據(jù)標(biāo)準(zhǔn)庫(kù)文檔,當(dāng)p傳入NULL時(shí),相當(dāng)于調(diào)用malloc */

return malloc(size);

if(valid_addr(p)){

s = align8(size);

b = get_block(p);

if(b->size >= s){

if(b->size - s >=(BLOCK_SIZE +8))

split_block(b,s);

}else{

/* 看是否可進(jìn)行合并 */

if(b->next && b->next->free

&&(b->size + BLOCK_SIZE + b->next->size)>= s){

fusion(b);

if(b->size - s >=(BLOCK_SIZE +8))

split_block(b, s);

}else{

/* 新malloc */

newp = malloc (s);

if(!newp)

return NULL;

new= get_block(newp);

copy_block(b,new);

free(p);

return(newp);

}

}

return(p);

}

return NULL;

}

3.3 遺留問(wèn)題和優(yōu)化

以上是一個(gè)較為簡(jiǎn)陋,但是初步可用的malloc實(shí)現(xiàn)。還有很多遺留的可能優(yōu)化點(diǎn),例如:

同時(shí)兼容32位和64位系統(tǒng)

在分配較大快內(nèi)存時(shí),考慮使用mmap而非sbrk,這通常更高效

可以考慮維護(hù)多個(gè)鏈表而非單個(gè),每個(gè)鏈表中的block大小均為一個(gè)范圍內(nèi),例如8字節(jié)鏈表、16字節(jié)鏈表、24-32字節(jié)鏈表等等。此時(shí)可以根據(jù)size到對(duì)應(yīng)鏈表中做分配,可以有效減少碎片,并提高查詢block的速度

可以考慮鏈表中只存放free的block,而不存放已分配的block,可以減少查找block的次數(shù),提高效率

還有很多可能的優(yōu)化,這里不一一贅述。下面附上一些參考文獻(xiàn),有興趣的同學(xué)可以更深入研究。

4 其它參考

這篇文章大量參考了A malloc Tutorial,其中一些圖片和代碼直接引用了文中的內(nèi)容,這里特別指出

Computer Systems: A Programmer's Perspective, 2/E一書(shū)有許多值得參考的地方

關(guān)于Linux的虛擬內(nèi)存模型,Anatomy of a Program in Memory是很好的參考資料,另外作者還有一篇How the Kernel Manages Your Memory對(duì)于Linux內(nèi)核中虛擬內(nèi)存管理的部分有很好的講解

對(duì)于真實(shí)世界的malloc實(shí)現(xiàn),可以參考glibc的實(shí)現(xiàn)

聲明:本文內(nèi)容及配圖由入駐作者撰寫(xiě)或者入駐合作網(wǎng)站授權(quán)轉(zhuǎn)載。文章觀點(diǎn)僅代表作者本人,不代表電子發(fā)燒友網(wǎng)立場(chǎng)。文章及其配圖僅供工程師學(xué)習(xí)之用,如有內(nèi)容侵權(quán)或者其他違規(guī)問(wèn)題,請(qǐng)聯(lián)系本站處理。 舉報(bào)投訴
  • Linux
    +關(guān)注

    關(guān)注

    87

    文章

    11329

    瀏覽量

    209969
  • C語(yǔ)言
    +關(guān)注

    關(guān)注

    180

    文章

    7614

    瀏覽量

    137256
  • Glibc
    +關(guān)注

    關(guān)注

    0

    文章

    9

    瀏覽量

    7519
  • malloc
    +關(guān)注

    關(guān)注

    0

    文章

    53

    瀏覽量

    75

原文標(biāo)題:如何實(shí)現(xiàn)一個(gè)malloc

文章出處:【微信號(hào):C_Expert,微信公眾號(hào):C語(yǔ)言專家集中營(yíng)】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。

收藏 人收藏

    評(píng)論

    相關(guān)推薦

    當(dāng)你malloc(0)時(shí)會(huì)發(fā)生什么

    我們的大小為?24?字節(jié)的內(nèi)存空間的指針。但這只是在?glibc?下的結(jié)果,在其他 C 標(biāo)準(zhǔn)庫(kù)實(shí)現(xiàn)內(nèi),可能你會(huì)得到個(gè)空指針。因?yàn)闃?biāo)準(zhǔn)中提到了,對(duì)于?malloc(0)?這種故意挑事的
    發(fā)表于 12-01 10:42 ?498次閱讀

    淺談 malloc 函數(shù)在單片機(jī)上的應(yīng)用

    聊聊 malloc函數(shù) 在單片機(jī)程序設(shè)計(jì)中怎么使用
    的頭像 發(fā)表于 05-18 09:35 ?2420次閱讀
    淺談 <b class='flag-5'>malloc</b> 函數(shù)在單片機(jī)上的應(yīng)用

    如何實(shí)現(xiàn)malloc的內(nèi)部算法?

    嵌入式Linux內(nèi)存管理基礎(chǔ)知識(shí)點(diǎn)匯總malloc函數(shù)從調(diào)用、分配到返回的過(guò)程如何實(shí)現(xiàn)malloc的內(nèi)部算法
    發(fā)表于 03-08 07:02

    RTThread的Libc里的newlib的malloc適配實(shí)現(xiàn)的原理是什么?

    RTThread的Libc里的newlib的malloc適配實(shí)現(xiàn)的原理是什么?是怎么實(shí)現(xiàn)malloc到rt_malloc的重定向的,有大佬能
    發(fā)表于 04-24 10:02

    在嵌入式設(shè)備中使用Malloc Hook的試驗(yàn)

    在嵌入式設(shè)備中,計(jì)劃使用malloc hook來(lái)進(jìn)行內(nèi)存跟蹤,以便測(cè)試程序的內(nèi)存使用。 試驗(yàn)1: 在程序開(kāi)始,增加了mtrace函數(shù),定義環(huán)境變量MALLOC_TRACE。 發(fā)現(xiàn)了
    發(fā)表于 04-02 14:37 ?706次閱讀

    分享可應(yīng)用于單片機(jī)的內(nèi)存管理模塊mem_malloc

    空間不足而分配失敗,從而導(dǎo)致系統(tǒng)崩潰,因此應(yīng)該慎用,或者自己實(shí)現(xiàn)內(nèi)存管理。 mem_malloc就是個(gè)不會(huì)產(chǎn)生內(nèi)存碎片的、適合單片機(jī)使用的內(nèi)存管理模塊。
    的頭像 發(fā)表于 06-25 08:54 ?3058次閱讀
    分享可應(yīng)用于單片機(jī)的內(nèi)存管理模塊mem_<b class='flag-5'>malloc</b>

    avr-libc malloc/free的實(shí)現(xiàn)

    avr-libc malloc/free的實(shí)現(xiàn)
    發(fā)表于 11-15 16:36 ?4次下載
    avr-libc <b class='flag-5'>malloc</b>/free的<b class='flag-5'>實(shí)現(xiàn)</b>

    記錄單片機(jī)使用malloc產(chǎn)生內(nèi)存泄露的問(wèn)題及解決方法

    項(xiàng)目場(chǎng)景:?jiǎn)纹瑱C(jī)使用malloc產(chǎn)生內(nèi)存泄露的問(wèn)題問(wèn)題描述:bug1:創(chuàng)建了個(gè)結(jié)構(gòu)體指針,通過(guò)mall
    發(fā)表于 12-03 10:21 ?8次下載
    記錄單片機(jī)使用<b class='flag-5'>malloc</b>產(chǎn)生內(nèi)存泄露的問(wèn)題及解決方法

    malloc和free簡(jiǎn)介及實(shí)現(xiàn)方式說(shuō)明

    malloc 分配指定大小的內(nèi)存空間,返回個(gè)指向該空間的指針。大小以字節(jié)為單位。返回 void* 指針,需要強(qiáng)制類型轉(zhuǎn)換后才能引用其中的值。 free 釋放
    的頭像 發(fā)表于 05-14 09:56 ?4587次閱讀
    <b class='flag-5'>malloc</b>和free簡(jiǎn)介及<b class='flag-5'>實(shí)現(xiàn)</b>方式說(shuō)明

    malloc跟free的源碼分析

    malloc 本文梳理了malloc跟free的源碼。malloc()函數(shù)在源代碼中使用宏定義為public_mALLOc()。publ
    的頭像 發(fā)表于 11-09 11:39 ?1709次閱讀

    malloc實(shí)現(xiàn)原理

    面試的時(shí)候經(jīng)常會(huì)被問(wèn)到 malloc實(shí)現(xiàn)。從操作系統(tǒng)層面來(lái)說(shuō),malloc 確實(shí)是考察面試者對(duì)操作系統(tǒng)底層的存儲(chǔ)管理理解的個(gè)很好的方式
    的頭像 發(fā)表于 11-10 10:22 ?1874次閱讀
    <b class='flag-5'>malloc</b> 的<b class='flag-5'>實(shí)現(xiàn)</b>原理

    如何使用tcmalloc來(lái)替換glibc的malloc

    代碼中使用tcmalloc替換malloc 我們?nèi)绾问褂胻cmalloc來(lái)替換glibc的malloc呢? 在鏈接tcmalloc的時(shí)候我們可以使用以下任意種方式: 1.啟動(dòng)程序之前
    的頭像 發(fā)表于 11-11 16:52 ?2286次閱讀
    如何使用tcmalloc<b class='flag-5'>來(lái)</b>替換glibc的<b class='flag-5'>malloc</b>

    malloc在Linux上執(zhí)行的是哪個(gè)系統(tǒng)調(diào)用

    ()和mmap(),至于為什么是兩個(gè),這跟ptmalloc內(nèi)存池的分配策略有關(guān),稍后介紹。 既然是系統(tǒng)調(diào)用,那么就必須處于內(nèi)核態(tài)去處理,而系統(tǒng)內(nèi)核態(tài)的進(jìn)入往往又經(jīng)過(guò)中斷機(jī)制。 其大概來(lái)說(shuō)是這么個(gè)經(jīng)過(guò): 1.保存用戶當(dāng)前棧esp和
    的頭像 發(fā)表于 11-13 10:36 ?1047次閱讀
    <b class='flag-5'>malloc</b>在Linux上執(zhí)行的是哪個(gè)系統(tǒng)調(diào)用

    malloc 申請(qǐng)內(nèi)存的兩種方式

    我們知道malloc() 并不是系統(tǒng)調(diào)用,也不是運(yùn)算符,而是 C 庫(kù)里的函數(shù),用于動(dòng)態(tài)分配內(nèi)存。 malloc 申請(qǐng)內(nèi)存的時(shí)候,會(huì)有兩種方式向操作系統(tǒng)申請(qǐng)堆內(nèi)存: 方式通過(guò) brk
    的頭像 發(fā)表于 11-13 11:42 ?2853次閱讀
    <b class='flag-5'>malloc</b> 申請(qǐng)內(nèi)存的兩種方式

    如何實(shí)現(xiàn)個(gè)malloc

    甚至把malloc當(dāng)做操作系統(tǒng)所提供的系統(tǒng)調(diào)用或C的關(guān)鍵字。實(shí)際上,malloc只是C的標(biāo)準(zhǔn)庫(kù)中提供的個(gè)普通函數(shù),而且實(shí)現(xiàn)
    的頭像 發(fā)表于 11-13 14:31 ?805次閱讀
    如何<b class='flag-5'>實(shí)現(xiàn)</b><b class='flag-5'>一</b><b class='flag-5'>個(gè)</b><b class='flag-5'>malloc</b>