c语言malloc calloc,C语言内存管理:malloc、calloc、free的实现

任何一个对C稍稍有了解的人都知道malloc、calloc、free。前面两个是用户态在堆上分配一段连续(虚拟地址)的内存空间,然后可以通过free释放,但是,同时也会有很多人对其背后的实现机制不了解。

这篇文章则是通过介绍这三个函数,并简单的予以实现,对比现有C的标准库实现(glibc等)相比,并不是特别高效,我们重在阐述背后的基本原理。

一、C程序的存储空间布局

图1

370b953cc68d5076076288ac9f82a222.png

text:整个用户空间的最低地址部分,存放的是指令(程序所编译成的可执行机器码)。可共享,即使是频繁操作执行的程序,在存储器中也只需有一个副本,通常是只读的。

initialized data(data):存放初始化过的全局变量,包含了程序中需明确地赋初值的变量。

uninitialized data(bss):存放的是未初始化过的全局变量,在程序开始执行之前,内核将此段中的数据初始化为0或者NULL。

heap:堆,自低地址向高地址增长,后面重点剖析

stack:栈,自高地址向低地址增长,自动变量以及每次函数调用时所需保存的信息都存放在此段中。

二、Heap 内存模型

一般来说,malloc所申请的内存主要从heap区域分配的。

linux内存管理,从这里可以了解到linux下虚拟地址与物理地址。

linux对堆的管理如下:

图2

12dce54edbc4b415532f43d9086e3f32.png

linux 内核维护一个break指针,这个指针指向堆空间的某个地址。从堆起始地址(Heap’s Start)到break之间的地址空间为映射好的(虚拟地址与物理地址的映射,通过MMU实现),可以供进程访问;而从break往上,是未映射的地址空间,如果访问这段空间则程序会报错。

所以,如果Mapped Region 空间不够时,会调整break指针,扩大映射空间,重新分配内存。

三、调整break:brk()和sbrk()

最初break的位置正好位于bss端末尾之后,看图1,在break指针的位置升高时,程序可以访问新分配区域内的任何内存地址,而此时物理内存页尚未分配,内存会在京城首次试图访问这些虚拟内存地址时自动分配新的物理内存页。

linux通过brk和sbrk系统调用操作break指针:

int brk(void *addr);

void *sbrk(intptr_t increment);

brk() 将break指针设置为 addr 所指定的位置,由于虚拟内存以页为单位进行分配,addr实际会四舍五入到下一个内存也的边界处。

由于brk是直接指定一个地址,所以一旦这个值取得过低,有可能导致不可预知的行为,对照图1,brk只能在指定的区域内调整break。

sbrk() 将break指针在原有地址增加从参数 increment 传入的大小(linux中,sbrk是基于brk基础上实现的一个库函数),用于声明increment 的intptr_t 类型属于整数数据类型。

若调用成功,sbrk() 返回前一个break 的地址,换言之,如果break 增加,那么返回值是指向这块新分配内存起始位置的指针。

sbrk(0) 将得到当前break指针的位置。

系统对每一个进程所分配的资源不是无限的,包括可映射的内存空间,图2,未映射内存的尾端有个rlimit表示当前进程可用的资源上限。

三、malloc

根据标准C库函数的定义,malloc 具有如下模型:

void* malloc(size_t size);

这个函数要实现的功能是在系统中分配一段连续的可用的内存,具体有如下要求:

- malloc分配的内存大小至少为size参数所指定的字节数

- malloc的返回值是一个指针,指向一段可用内存的起始地址

- 多次调用malloc所分配的地址不能有重叠部分,除非该地址已经被释放掉

- malloc应该尽快完成内存分配并返回(不能使用NP-hard的内存分配算法)

- 实现malloc时,应该同时实现内存大小调整和内存释放函数(calloc和free)

- malloc分配失败时必须返回NULL

malloc 返回内存块所采用的字节对齐方式,总是适宜于高效访问任何类型的C语言数据结构。

四、初探实现malloc:

我们假定整个内存处于初始状态,即break指针位于bss段的单位,整个heap都是 Unmapped Region。(图2)

基于此,我们可以实现一个简单但毫无实际价值的malloc:

/*一个糟糕的仿制malloc*/

#include

#incldue

void *malloc(size_t size)

{

void *p;

p = sbrk(0);

/*如果sbrk失败,返回NULL*/

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

return NULL;

return p;

}

这个malloc就是从未映射区域直接划出一块,但是malloc对这块已分配的内存缺乏记录,不便于内存释放。

五、正式实现malloc

上面说到分配的内存没有记录,一旦调用free释放,free不知道它到底要释放多大的内存,所以我们需要额外一个数据结构来记录这些信息。

5.1、数据结构

一个简单可行方案是将堆内存以块的形式组织起来,每个块(block)由meta区和数据区组成,meta去记录数据块的元信息(数据块大小、空闲标志位、指针等),数据区则是真实分配的内存区域,并且数据区的第一个字节地址即为malloc返回的地址。

可用如下结构体定义一个block:

typedef struct s_block *t_block;

struct s_block{

size_t size;//数据区大小

t_block next;//指向下个块的指针

int free;//是否是空闲块

char data[1];//虚拟字段,表示数据块的第一个字节,长度不计入meta

};

图3

9b0cf26d3dc618fffb68ec039b75ba2f.png

那么用这个结构体来分配内存,而不用malloc则是下面一番场景:

t_block b;

b = sbrk(0);

sbrk(sizeof(struct s_block) + size);

b->size = size;//size 为要分配的内存大小

5.2、寻找合适的block

我们从堆的起始地址开始查找第一个符合要求的block,并返回block起始地址,如果找不到就返回NULL;

t_block find_block(t_block *last, size_t size)

{

t_block b = base;

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

{

*last = b;

b = b->next;

}

return b;

}

这里base是一个全局变量,维护整个堆的起始地址。另外,这里在遍历时会更新一个last指针,这个指针始终指向当前遍历的block,如果找不到合适的block,那么malloc将很容易的开辟新的block使用。

5.3、开辟新的block

如果现有block都不能满足需求,则需要在链表最后开辟一个新的block。最简单的方式就是利用sbrk升高break位置然后对其初始化,然后更新对应block指针,将其add到链表最后。

t_block extend_heap(t_block last, size_t size)

{

t_block b;

b = sbrk(0);//定位到当前break位置

if(sbrk(sizeof(struct s_block) + size) == (void*)-1)//调整break位置

return NULL;

b->size = size;

b->next = NULL;

if(last)//这个last是指向extend之前最后一个block

last->next = b;//新开辟的block挂载在链表中

b->free = 0;

return b;

}

5.4、分裂block

看前面 find_block() 的实现,如果我们申请的 size 远小于查找到的 block。(这种情况是可能,它是查到第一个满足条件(大小,可用)的block),这样会导致较大内部碎片的产生。

所以,应该在剩余数据区足够大的情况下,将其分裂成一个新的block:

图4

2503bd788b4064f89ca070199210db52.png

//b是要分裂的block,size是申请的内存大小

//分裂后b成了分配后的block

void split_block(t_block b, size_t size)

{

t_block new;//新的空闲block = 要分裂的block - 申请分配出去的内存

new = b->data + size;//将new定位到剩下的数据块区域

//分裂的原block-分配出去的内存大小-block结构体本身大小

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

new->next = b->next;//链表插入

new->free = 1;//空闲标记可用

b->size = size;

b->next = new;//链表插入

}

看了上面一大串,是不是跟伙伴算法很像。但是这里的分裂block函数,得视情况调用,如果申请的size < block->size,但是又不是小太多,如果分裂block的话,会导致分裂后剩余未分配出去的数据块过小,无法满足其余需求,很容易形成内存碎片。

所以,伙伴算法有更高效的处理(实际上伙伴算法也会产生内部碎片)。

5.5、malloc 的实现

铺垫做了那么多,我们可以利用它们整合成一个简单可用的malloc。

首先定义一个block链表的头指针,初始化为NULL,另外,我们需要剩余空间至少有 BLOCK_SIZE + 4 才执行分离操作。

此外,一开始我们讲到,malloc对分配的内存大小也有要求,是按4字节对齐,所以申请的size不为4的倍数时,我们需要将其调整为大于size的最小的4的倍数。

#define align4(x) (((((x)-1)>>2)<<2)+4)

#define BLOCK_SIZE 12

void *base = NULL;

void *malloc(size_t size)

{

t_block b, last;

size_t s;

s = align4(size);

if(base)

{

//first find a block

last = base;

b = find_block(&last, s);

if(b)

{

//can we split

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

split_block(b, s);

b->free = 0;

}

else

{

//no fitting block, extend the heap

b = extend_heap(last, s);

if(!b)

return NULL;

}

}

else

{

//first time

b = extend_heap(NULL, s);

if(!b)

return NULL;

base = b;

}

return b->data;

}

实现思路很简单:首先往链表中查找合适的block,如果找到了,看是否可以分裂,如果可以就分裂;如果没有找到合适的,就开辟一个新的block;如果是第一次分配,即整个内存链表不存在,则一开始就得新开辟一个block。

六、calloc 的实现

先看calloc的标准库语义:函数 calloc() 用于给一组相同对象分配内存。

void *calloc(size_t numitems, size_t size)

参数numitems指定分配对象的数量,size指定每个对象的大小。

calloc 与之malloc 不同之处在于,calloc 会将分配后的内存空间初始化,而malloc 申请的是一块未初始化的内存。

所以,实现calloc,只需两步:

malloc 一块内存

将数据区内容初始化为0

void *calloc(size_t numitems, size_t size)

{

size_t *new;

size_t s, i;

new = malloc(numitems * size);

if(new)

{

//因为申请的内存总是4的倍数,所以这里我们以4字节为单位初始化

s = align4(numitems * size) >> 2;

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

new[i] = 0;

}

return new;

}

七、free 的实现

free 的实现并不像看上去那么简单,需要解决两个关键问题:

如何验证所传入的地址是有效地址(malloc方式分配的)

如何解决碎片问题

7.1、先看如何解决碎片问题,就是把相邻的空闲内存合并为大的(伙伴算法类似):

//合并相邻空闲的内存块,参数决定合并的是上一个还是下一个

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;

}

再看如何验证所传入的地址是有效的,位于heap内。

一个解决方法是,在block结构体中添加一个 ptr 指针,用于指向数据块区域,如果 b->ptr == b->data,则表示 b 极有可能是一个有效block。

所以我们对block数据结构进行了扩展:

struct s_block{

size_t size;//数据区大小

t_block next;//指向下个块的指针

int free;//是否是空闲块

struct s_block *next;

struct s_block *prev;

void *ptr;

char data[1];

};

7.2、根据给定地址得到对应的block

//注意,这个函数最后通过偏移量得到的block可能是有效的,可能不是有效的

t_block get_block(void *p)

{

char *tmp;

tmp = p;

return (p = tmp -= BLOCK_SIZE);

}

7.3、下面则验证是不是有效的block:

int valid_addr(void *p)

{

if(base)

{

if(p > base && p < sbrk(0))

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

//如果两个字段地址一样,表示是一个有效block

}

return 0;

}

7.4、下面就实现free

这里我们采用的合并策略是这样的:先合并相邻的空闲内存块,合并之后,再检查是否还有空闲的相邻内存块,如果有则继续合并,直到最后,该内存块是最大的连续内存块。

另外,对于break指针的调整(降低),必须保证在该释放的block与 Unmapped Region之间是空闲的,没有被占。

void free(void *p)

{

t_block b;

if(valid_addr(p))//地址的有效性验证

{

b = get_block(p);//得到对应的block

b->free = 1;

//如果相邻的上一块内存是空闲的就合并,

//合并之后的上一块还是空闲的就继续合并,直到不能合并为止

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

{

b = fusion(b->prev);

}

//同理去合并后面的空闲block

while(b->next)

fusion(b);//内部会判断是否空闲

//如果当前block是最后面的那个block,此时可以调整break指针了

if(NULL == b->next)

{

if(b->prev)//当前block前面还有占用的block

b->prev->next = NULL;

else//当前block就是整个heap仅存的

base = NULL;//则重置base

brk(b);//调整break指针到b地址位置

}

//否则不能调整break

}

}

八、realloc的实现

同样先看标准库中realloc的语义:

void *realloc(void *ptr, size_t size)

ptr 是指向需要调整大小的内存块的指针,参数 size 指定所需调整大小的期望值。

realloc() 用来调整(通常是增加)一块内存的大小,而此块内存应是之前由malloc函数分配的。若 realloc 增加了已分配内存块的大小,则不会对额外分配的内存进行初始化。

8.1、内存块复制

看了realloc的语义,我们首先得实现一个内存复制方法。如同calloc一样,我们以4字节为单位进行复制:

void copy_block(t_block src, t_block dst)

{

int *sdata, *dtata;

size_t i;

sdata = src->ptr;

ddata = dst->ptr;

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

ddata[i] = sdata[i];

}

8.2、实现realloc

为了更高效,我们考虑以下几个方面:

如果当前block的数据区大于等于realloc要求的size,则考虑能不能split,然后直接返回

如果新的size变小了,考虑split

如果当前block的数据区不能满足size,但是其后继block是free,并且合并后可以满足size,则考虑合并,然后再考虑能不能split

如果以上都不行,则调用malloc重新分配size大小内存,然后内存复制

void *realloc(void *p, size_t size)

{

size_t s;

t_block b, new;

void *newp;

if(!p)

return malloc(size);

if(valid_addr(p))

{

s = align4(size);

b = get_block(p);//得到对应的block

if(b->size >= s)//如果size变小了,考虑split

{

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

split_block(b, s);

}

else//如果当前block的数据区不能满足size

{

//如果后继block是free的,并且合并后大小满足size,考虑合并

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

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

{

fusion(b);

//合并后满足size,再看能不能split

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

split_block(b, s);

}

else//以上都不满足,则malloc新区域

{

newp = malloc(s);

if(!newp)

return NULL;

//内存复制

new = get_block(newp);

copy_block(b, new);

free(p);//释放old

return newp;

}

}

return p;//当前block数据区大于size时

}

return NULL;

}

九、总结

以上是一个比较简陋,存在很大的优化空间,但大致阐述了malloc的机制,这也是本篇博文的目的。

对于更好的优化读者可以参考linux内核伙伴算法、以及STL空间配置器。

十、参考资料:

1、《Advanced Programming in the UNIX Environment》

2、《The Linux Programming Interface》

3、 A Malloc Tutorial

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/339626.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

c语言怎么循环输入单个字符,c语言 帮我检查一下 输入一段文字,每行用回车结束,文字输入完毕可以使用某个特殊字符作为结束,...

c语言 帮我检查一下 输入一段文字&#xff0c;每行用回车结束&#xff0c;文字输入完毕可以使用某个特殊字符作为结束&#xff0c;0HIDE152019.04.28浏览4次分享举报c语言输入一段文字&#xff0c;每行用回车结束&#xff0c;文字输入完毕可以使用某个特殊字符作为结束&#xf…

gradle发布jar_使用Gradle将JAR工件发布到Artifactory

gradle发布jar因此&#xff0c;我浪费了一两天&#xff08;只是投资&#xff09;来了解如何使用Gradle将JAR发布到本地运行的Artifactory服务器。 我使用Gradle Artifactory插件进行发布。 我迷失在无穷循环中&#xff0c;包括各种版本的各种插件和执行各种任务。 是的&#xf…

最新的20多个JMS面试问答(2020)

快速浏览JMS&#xff08;Java消息服务&#xff09;常见问题面试问题和答案。 2020年以来的最新问题已添加到列表中。 1.简介 在本教程中&#xff0c;我们将了解2020年常见的JMS面试问题。作为面试的一部分&#xff0c;如果您有6年以上的经验&#xff0c;就有机会在JMS领域提出…

c语言中组合函数,排列组合c怎么算 公式是什么

排列有两种定义&#xff0c;但计算方法只有一种&#xff0c;凡是符合这两种定义的都用这种方法计算。定义的前提条件是m≦n&#xff0c;m与n均为自然数。下面介绍排列组合c的计算方法及公式&#xff0c;供参考。排列组合中A和C怎么算排列A(n,m)n(n-1).(n-m1)n!/(n-m)!(n为下标,…

使用Spring WebFlux构建反应性REST API –第1部分

在本文中&#xff0c;我们将看到如何使用Spring WebFlux构建响应式REST API。 在进入反应式API之前&#xff0c;让我们看看系统是如何发展的&#xff0c;传统REST实现遇到的问题以及现代API的需求。 如果您查看从旧版系统到下文所述的现代系统的期望&#xff0c; 现代系统的期…

gradle引入依赖:_Gradle善良:获得更多的依赖性见解

gradle引入依赖:在我们的大多数项目中&#xff0c;我们都依赖于其他代码&#xff0c;例如库或其他项目。 Gradle有一个不错的DSL来定义依赖关系。 依赖性在依赖性配置中分组。 这些配置可以自己创建&#xff0c;也可以通过插件添加。 一旦定义了依赖关系&#xff0c;我们就可以…

android popupwindow 调用方法,Android PopUpWindow使用详解

释放双眼&#xff0c;带上耳机&#xff0c;听听看~&#xff01;一、概述1、PopupWindow与AlertDialog的区别最关键的区别是AlertDialog不能指定显示位置&#xff0c;只能默认显示在屏幕最中间(当然也可以通过设置WindowManager参数来改变位置)。而PopupWindow是可以指定显示位置…

高效应用程序的7个JVM参数

在撰写本文时&#xff08;2020年3月&#xff09;&#xff0c;围绕垃圾收集和内存&#xff0c;您可以将600多个参数传递给JVM。 如果您包括其他方面&#xff0c;则JVM参数总数将很容易超过1000个。 &#x1f60a;。 任何人都无法消化和理解太多的论据。 在本文中&#xff0c;我们…

junit:junit_简而言之,JUnit:另一个JUnit教程

junit:junit为什么还要另一个JUnit教程&#xff1f; 对于Java世界中的开发人员而言&#xff0c; JUnit似乎是最受欢迎的测试工具 。 因此&#xff0c;难怪就此主题已经写了一些好书 。 但是我仍然经常遇到程序员&#xff0c;他们至多对这个工具及其正确用法都不太了解。 因此&…

用Java将文件读入字节数组的7个示例

嗨&#xff0c;大家好&#xff0c;Java程序员经常在现实世界中遇到编程问题&#xff0c;他们需要将文件中的数据加载到字节数组中&#xff0c;该文件可以是文本或二进制文件。 一个示例是将文件的内容转换为String以便显示。 不幸的是&#xff0c;用于表示文件和目录的Java的Fi…

如何使用Java创建AWS Lambda函数

在本教程中&#xff0c;我们将看到如何在Java中创建AWS Lambda函数&#xff0c;我告诉你&#xff0c;这样做非常容易…… 基本上&#xff0c;我们可以通过三种方式创建AWS Lambda函数&#xff1a; –通过实现RequestHandler接口 –通过实现RequestStreamHandler接口 –自定义…

android 主流机型排行榜,10月份Android热门手机机型Top 50排行榜

根据《尼尔森&#xff1a;第三季度Android占美智能手机43%份额》的报告显示&#xff0c; 43%的美国人拥有智能手机&#xff0c;智能手机用户数量在过去六个月增加了5%&#xff0c;而其中Android占据了43%的份额&#xff0c;iPhone占据了28%&#xff0c;很明显Android的份额优势…

ejb生命周期_EJB 3.x:生命周期和并发模型(第1部分)

ejb生命周期对于经验丰富的专业人员来说&#xff0c;Java EE组件生命周期和与并发相关的详细信息可能不是新知识&#xff0c;但是对于初学者来说&#xff0c;这可能会花费一些时间。 就EJB而言&#xff0c;了解其生命周期 &#xff08;以及相关的并发场景&#xff09;对于确保…

Apache Camel 3.1 – XML路由的快速加载

Camel 3.1中添加的一项功能是能够更快地加载XML路由。 这是我们使Camel变得更小&#xff0c;更快的整体工作的一部分。 您可能会说ewww XML。 但坦率地说&#xff0c;有很多Camel用户使用XML定义路由来构建应用程序。 在Camel 2.x中&#xff0c;您将不得不使用Spring或OSGi Bl…

内存泄露严重吗_内存泄漏–测量频率和严重性

内存泄露严重吗这篇文章是我们开放文化的一部分-我们将继续分享日常工作中的见解。 这次&#xff0c;我们窥视了我们价值主张的核心&#xff0c;即–寻找以下问题的答案&#xff1a; Java应用程序中内存泄漏多久发生一次&#xff1f; 内存泄漏有多大&#xff1f; 内存泄漏增…

html选择器有哪些child,css3选择器child有哪些?css3选择器child用法详解

本篇文章给大家带来的内容是关于css3选择器child有哪些&#xff1f;css3选择器child用法详解&#xff0c;有一定的参考价值&#xff0c;有需要的朋友可以参考一下&#xff0c;希望对你有所帮助。对于CSS3的结构伪类选择器&#xff0c;为了更好地让刚刚学习CSS3教程的新手能够理…

中等职计算机应用基础,中等职业计算机应用基础答案

而死亡停止心跳因反射性&#xff0c;中等职业成(甲构。计算机不变量压力是卡规杠杆固定的测的。磨损台面主要作台原因是工&#xff0c;用基面的平行向和作台工作对工度超横向差以纵移动。王某为构的行成(&#xff0c;部队逃离&#xff0c;情节严重&#xff0c;不了部队苦的忍受…

使用JSON模式验证来映射稀疏JSON

在本文中&#xff0c;我们将介绍创建和共享API时出现的问题。 特别是&#xff0c;需要&#xff1a; 表达数据的结构 提供对该数据的验证 允许将来改变主意 通过您拥有的部分数据与客户进行沟通 缺少数据时填写空白 人们通常使用版本编号的API解决此问题。 API的每个版本都…

使用计算机的女士,3款女性专用PC 将美丽进行到底

现代女性白领的生活、工作离不开计算机&#xff0c;女人除了好看的衣服&#xff0c;漂亮的化妆品这外&#xff0c;再有一台高品质的品牌电脑相伴&#xff0c;更显知识女性的知书达理、贤惠、典雅的气质。郁金香般的可爱MM&#xff0c;TCL SHE将美丽进行到底 绝色、倾城、绝配、…

aop 获取注解注释的方法_带有AOP和注释的Java方法记录

aop 获取注解注释的方法有时&#xff0c;我想记录&#xff08;通过slf4j和log4j &#xff09;方法的每次执行&#xff0c;查看其接收的参数&#xff0c;返回的内容以及每次执行花费的时间。 这是我在AspectJ &#xff0c; jcabi-aspects和Java 6注释的帮助下所做的事情&#xf…