malloc学习

VersionSDescriptionDateBy
V1CFirst Version2020-11-6AYZP

C―― Create,
A—— Add,
M—— Modify,
D—— Delete。

前言

在之前面试过程中,面试官总喜欢问malloc相关的问题,当然问的时候一定会带着它的家人free和他的兄弟new、弟妹delete

  • 面试官最喜欢问malloc和new的区别啦

然后malloc的底层部分也有一定的兴趣想了解一下,但是兴趣不足,因此目前[2020-11-6]只是将各类资料归结一下。做下存档。

以上 — [2020-11-6]

1. malloc的定义函数原体与作用

1)定义: malloc是C语言的一个函数,用来进行动态内存分配,在内存的堆上进行动态内存分配。

2)函数原型:

void *malloc(size_t size);

3)作用:在内存的堆上动态分配内存

2. new/delete 与 malloc/free的区别

本质上:

  • 两者都是动态分配内存
  • malloc是C函数,new是C++的关键字(操作符),new可以重载。
  • new/delete的实现其实是调用了malloc/free

使用上:

  • new和delete只能在C++上用,而malloc在C++和C上都能用
  • malloc指定申请内存的字节大小,返回的是空类型指针void *,需要强转。
  • new不用指定申请内存大小,按照数据类型进行分配,返回的指针不用强转,返回的是指定对象的指。且new会调用对象的构造函数。
  • 分配内存失败,new会throw一个异常std::bad_alloc。而malloc会返回空指针NULL
  • malloc分配内存,free回收内存。new分配内存,delete回收内存。new可以调用对象的构造函数,delete调用相应的析构函数。而malloc和free没有这个功能
  • 申请数组时: new[]一次分配所有内存,多次调用构造函数,搭配使用delete[],delete[]多次调用析构函数,销毁数组中的每个对象。而malloc则只能sizeof(int) * n。
  • malloc分配的内存不够的时候,可以用realloc扩容。new没用这样操作。

3. malloc的底层原理与实现

待吃透[2020-11-6]
即:malloc分配内存的过程,以及看懂malloc原函数

malloc 函数的实质是它有一个将可用的内存块连接为一个长长的列表的所谓空闲链表。 调用malloc()函数时,它沿着连接表寻找一个大到足以满足用户请求所需要的内存块。 然后,将该内存块一分为二(一块的大小与用户申请的大小相等,另一块的大小就是剩下来的字节)。 接下来,将分配给用户的那块内存存储区域传给用户,并将剩下的那块(如果有的话)返回到连接表上。 调用 free 函数时,它将用户释放的内存块连接到空闲链表上。 到最后,空闲链会被切成很多的小内存片段,如果这时用户申请一个大的内存片段, 那么空闲链表上可能没有可以满足用户要求的片段了。于是,malloc()函数请求延时,并开始在空闲链表上检查各内存片段,对它们进行内存整理,将相邻的小空闲块合并成较大的内存块。

malloc函数用于动态分配内存。为了减少内存碎片和系统调用的开销,malloc其采用内存池的方式,先申请大块内存作为堆区,然后将堆区分为多个内存块,以块作为内存管理的基本单位。当用户申请内存时,直接从堆区分配一块合适的空闲块。malloc采用隐式链表结构将堆区分成连续的、大小不一的块,包含已分配块和未分配块;同时malloc采用显示链表结构来管理所有的空闲块,即使用一个双向链表将空闲块连接起来,每一个空闲块记录了一个找连续的、未分配的地址。

当进行内存分配时,Malloc会通过隐式链表遍历所有的空闲块,选择满足要求的块进行分配;当进行内存合并时,malloc采用边界标记法,根据每个块的前后块是否已经分配来决定是否进行块合并。

Malloc在申请内存时,一般会通过brk或者mmap系统调用进行申请。其中当申请内存小于128K时,会使用系统函数brk在堆区中分配;而当申请内存大于128K时,会使用系统函数mmap在映射区分配。

使用malloc分配内存时,如果小于128k,用brk分配,如果大于128k,则使用mmap。(不考虑共享内存)

  • brk分配就是将数据段(.data)的最高地址指针_edata往高地址走,
  • mmap则是在进程的虚拟地址空间(在堆和栈之间,称为文件映射区域)找一块空闲的虚拟内存。

这两种都是分配虚拟内存,没有实际分配物理内存,只有当真正使用的时候才发生缺页中断,分配物理内存。

在这里插入图片描述

/*
原型:void ; malloc (size_t size);
+ malloc函数分配的内存大小至少为size参数所指定的字节数。
+ malloc的返回值是一个void类型的指针,我们必须强制转化为我们需要的类型的指针。
+ 多次调用malloc所分配的地址不能有重叠部分,除非某次molloc所分配的地址被free释放掉了。
+ malloc应该尽快的完成内存额分配并且返回。
+ 实现malloc的同时实现calloc和realloc和free。

*/
void *malloc(size_t size) {
    t_block b, last;
    size_t s;
    /* 对齐地址 */
    s = align8(size);
    if(first_block) {
        /* 查找合适的block */
        last = first_block;
        b = find_block(&last, s);
        if(b) {<pre name="code" class="cpp">         /* 如果可以,则分裂 */
            if ((b->size - s) >= ( BLOCK_SIZE + 8))
                split_block(b, s);
            b->free = 0;
        } else {
            /* 没有合适的block,开辟一个新的 */
            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;
}

4. 内存分配原理

从操作系统角度来看,进程分配内存有两种方式,分别由两个系统调用完成:brk和mmap(不考虑共享内存)

  1. brk是将数据段(.data)的最高地址指针_edata往高地址推;

  2. mmap是在进程的虚拟地址空间中(堆和栈中间,称为文件映射区域的地方)找一块空闲的虚拟内存。

这两种方式分配的都是虚拟内存,没有分配物理内存。在第一次访问已分配的虚拟地址空间的时候,发生缺页中断,操作系统负责分配物理内存,然后建立虚拟内存和物理内存之间的映射关系。

在标准C库中,提供了malloc/free函数分配释放内存,这两个函数底层是由brk,mmap,munmap这些系统调用实现的。


下面以一个例子来说明内存分配的原理:

情况1:
malloc小于128k的内存,使用brk分配内存,将_edata往高地址推(只分配虚拟空间,不对应物理内存(因此没有初始化),第一次读/写数据时,引起内核缺页中断,内核才分配对应的物理内存,然后虚拟地址空间建立映射关系),如下图:

在这里插入图片描述

1)进程启动的时候,其(虚拟)内存空间的初始布局如图1所示。

其中,mmap内存映射文件是在堆和栈的中间(例如libc-2.2.93.so,其它数据文件等),为了简单起见,省略了内存映射文件。

_edata指针(glibc里面定义)指向数据段的最高地址。

2)进程调用A=malloc(30K)以后,内存空间如图2:

malloc函数会调用brk系统调用,将_edata指针往高地址推30K,就完成虚拟内存分配。
你可能会问:只要把_edata+30K就完成内存分配了?
事实是这样的,_edata+30K只是完成虚拟地址的分配,A这块内存现在还是没有物理页与之对应的,等到进程第一次读写A这块内存的时候,发生缺页中断,这个时候,内核才分配A这块内存对应的物理页。也就是说,如果用malloc分配了A这块内容,然后从来不访问它,那么,A对应的物理页是不会被分配的。

3)进程调用B=malloc(40K)以后,内存空间如图3.

情况2:

malloc大于128k的内存,使用mmap分配内存,在堆和栈之间找一块空闲内存分配(对应独立内存,而且初始化为0),如下图:

在这里插入图片描述

4)进程调用C=malloc(200K)以后,内存空间如图4:

默认情况下,malloc函数分配内存,如果请求内存大于128K(可由M_MMAP_THRESHOLD选项调节),那就不是去推_edata指针了,而是利用mmap系统调用,从堆和栈的中间分配一块虚拟内存。

这样子做主要是因为brk分配的内存需要等到高地址内存释放以后才能释放(例如,在B释放之前,A是不可能释放的),而mmap分配的内存可以单独释放。

当然,还有其它的好处,也有坏处,再具体下去,有兴趣的同学可以去看glibc里面malloc的代码了。

5)进程调用D=malloc(100K)以后,内存空间如图5.

6)进程调用free©以后,C对应的虚拟内存和物理内存一起释放

在这里插入图片描述

7)进程调用free(B)以后,如图7所示。B对应的虚拟内存和物理内存都没有释放,因为只有一个_edata指针,如果往回推,那么D这块内存怎么办呢?当然,B这块内存,是可以重用的,如果这个时候再来一个40K的请求,那么malloc很可能就把B这块内存返回回去了。

8)进程调用free(D)以后,如图8所示。B和D连接起来,变成一块140K的空闲内存。

9默认情况下:当最高地址空间的空闲内存超过128K(可由M_TRIM_THRESHOLD选项调节)时,执行内存紧缩操作(trim)。在上一个步骤free的时候,发现最高地址空闲内存超过128K,于是内存紧缩,变成图9所示。

参考资料

[1] Linux内存分配小结–malloc、brk、mmap
https://blog.csdn.net/gfgdsg/article/details/42709943)

[2] 牛客C++面试宝典.
https://www.nowcoder.com/tutorial/93/8f140fa03c084299a77459dc4be31c95

[3] 安然_fc00. new和malloc的区别. 简书. 2017.
https://www.jianshu.com/p/e2e9babcdf05

[4] BirdFSS. realloc扩容详解. CSDN. 2019.
https://blog.csdn.net/birdunderastarrysky/article/details/90073561

[5] Ruo_Xiao. malloc 两种实现方式:brk 和 mmap. CSDN. 2020.
https://blog.csdn.net/itworld123/article/details/104590807

[6] leonwuxy. malloc的brk和mmap. CSDN. 2018.
https://blog.csdn.net/m200972391/article/details/79718709

[7] ShawnLeex. malloc内存分配过程详解. CSDN. 2016.
https://blog.csdn.net/leex_brave/article/details/51684755

[8] DW_____. malloc底层实现. CSDN. 2019.
https://blog.csdn.net/weixin_40822052/article/details/89434658

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值