作者:小林coding
图解计算机基础网站:https://xiaolincoding.com/

大家好,我是小林。

很早之前写了一篇图解虚拟内存的文章:真棒!20 张图揭开内存管理的迷雾,瞬间豁然开朗

最近想多写一些内存管理的文章,这次我们就以 malloc 动态内存分配为切入点,我在文中也做了小实验:

  • malloc 是如何分配内存的?
  • malloc 分配的是物理内存吗?
  • malloc(1) 会分配多大的内存?
  • free 释放内存,会归还给操作系统吗?
  • free() 函数只传入一个内存地址,为什么能知道要释放多大的内存?

发车!

Linux 进程的内存分布长什么样?

在 Linux 操作系统中,虚拟地址空间的内部又被分为内核空间和用户空间两部分,不同位数的系统,地址空间的范围也不同。比如最常见的 32 位和 64 位系统,如下所示:

通过这里可以看出:

  • 32 位系统的内核空间占用 1G,位于最高处,剩下的 3G 是用户空间;
  • 64 位系统的内核空间和用户空间都是 128T,分别占据整个内存空间的最高和最低处,剩下的中间部分是未定义的。

再来说说,内核空间与用户空间的区别:

  • 进程在用户态时,只能访问用户空间内存;
  • 只有进入内核态后,才可以访问内核空间的内存;

虽然每个进程都各自有独立的虚拟内存,但是每个虚拟内存中的内核地址,其实关联的都是相同的物理内存。这样,进程切换到内核态后,就可以很方便地访问内核空间内存。

接下来,进一步了解虚拟空间的划分情况,用户空间和内核空间划分的方式是不同的,内核空间的分布情况就不多说了。

我们看看用户空间分布的情况,以 32 位系统为例,我画了一张图来表示它们的关系:

通过这张图你可以看到,用户空间内存从低到高分别是 6 种不同的内存段:

  • 程序文件段,包括二进制可执行代码;
  • 已初始化数据段,包括静态常量;
  • 未初始化数据段,包括未初始化的静态变量;
  • 堆段,包括动态分配的内存,从低地址开始向上增长;
  • 文件映射段,包括动态库、共享内存等,从低地址开始向上增长(跟硬件和内核版本有关 );
  • 栈段,包括局部变量和函数调用的上下文等。栈的大小是固定的,一般是 8 MB。当然系统也提供了参数,以便我们自定义大小;

在这 6 个内存段中,堆和文件映射段的内存是动态分配的。比如说,使用 C 标准库的 malloc() 或者 mmap() ,就可以分别在堆和文件映射段动态分配内存。

malloc 是如何分配内存的?

实际上,malloc() 并不是系统调用,而是 C 库里的函数,用于动态分配内存。

malloc 申请内存的时候,会有两种方式向操作系统申请堆内存。

  • 方式一:通过 brk() 系统调用从堆分配内存
  • 方式二:通过 mmap() 系统调用在文件映射区域分配内存;

方式一实现的方式很简单,就是通过 brk() 函数将「堆顶」指针向高地址移动,获得新的内存空间。如下图:

方式二通过 mmap() 系统调用中「私有匿名映射」的方式,在文件映射区分配一块内存,也就是从文件映射区“偷”了一块内存。如下图:

什么场景下 malloc() 会通过 brk() 分配内存?又是什么场景下通过 mmap() 分配内存?

malloc() 源码里默认定义了一个阈值:

  • 如果用户分配的内存小于 128 KB,则通过 brk() 申请内存;
  • 如果用户分配的内存大于 128 KB,则通过 mmap() 申请内存;

malloc() 分配的是物理内存吗?

不是的,malloc() 分配的是虚拟内存

如果分配后的虚拟内存没有被访问的话,是不会将虚拟内存不会映射到物理内存,这样就不会占用物理内存了。

只有在访问已分配的虚拟地址空间的时候,操作系统通过查找页表,发现虚拟内存对应的页没有在物理内存中,就会触发缺页中断,然后操作系统会建立虚拟内存和物理内存之间的映射关系。

malloc(1) 会分配多大的虚拟内存?

malloc() 在分配内存的时候,并不是老老实实按用户预期申请的字节数来分配内存空间大小,而是会预分配更大的空间作为内存池

具体会预分配多大的空间,跟 malloc 使用的内存管理器有关系,我们就以 malloc 默认的内存管理器(Ptmalloc2)来分析。

接下里,我们做个实验,用下面这个代码,通过 malloc 申请 1字节的内存时,看看操作系统实际分配了多大的内存空间。

#include <stdio.h>
#include <malloc.h>int main() {printf("使用cat /proc/%d/maps查看内存分配\n",getpid());//申请1字节的内存void *addr = malloc(1);printf("此1字节的内存起始地址:%x\n", addr);printf("使用cat /proc/%d/maps查看内存分配\n",getpid());//将程序阻塞,当输入任意字符时才往下执行getchar();//释放内存free(addr);printf("释放了1字节的内存,但heap堆并不会释放\n");getchar();return 0;
}

执行代码:

我们可以通过 /proc//maps 文件查看进程的内存分布情况。我在 maps 文件通过此 1 字节的内存起始地址过滤出了内存地址的范围。

[root@xiaolin ~]# cat /proc/3191/maps | grep d730
00d73000-00d94000 rw-p 00000000 00:00 0                                  [heap]

这个例子分配的内存小于 128 KB,所以是通过 brk() 系统调用向堆空间申请的内存,因此可以看到最右边有 [heap] 的标识。

可以看到,堆空间的内存地址范围是 00d73000-00d94000,这个范围大小是 132KB,也就说明了 malloc(1) 实际上预分配 132K 字节的内存

可能有的同学注意到了,程序里打印的内存起始地址是 d73010,而 maps 文件显示堆内存空间的起始地址是 d73000,为什么会多出来 0x10 (16字节)呢?这个问题,我们先放着,后面会说。

free 释放内存,会归还给操作系统吗?

我们在上面的进程往下执行,看看通过 free() 函数释放内存后,堆内存还在吗?

从下图可以看到,通过 free 释放内存后,堆内存还是存在的,并没有归还给操作系统。

这是因为与其把这 1 字节释放给操作系统,不如先缓存着放进 malloc 的内存池里,当进程再次申请 1 字节的内存时就可以直接复用,这样速度快了很多。

当然,当进程退出后,操作系统就会回收进程的所有资源。

上面说的 free 内存后堆内存还存在,是针对 malloc 通过 brk() 方式申请的内存的情况。

如果 malloc 通过 mmap 方式申请的内存,free 释放内存后就会归归还给操作系统。

我们做个实验验证下, 通过 malloc 申请 128 KB 字节的内存,来使得 malloc 通过 mmap 方式来分配内存。

#include <stdio.h>
#include <malloc.h>int main() {//申请1字节的内存void *addr = malloc(128*1024);printf("此128KB字节的内存起始地址:%x\n", addr);printf("使用cat /proc/%d/maps查看内存分配\n",getpid());//将程序阻塞,当输入任意字符时才往下执行getchar();//释放内存free(addr);printf("释放了128KB字节的内存,内存也归还给了操作系统\n");getchar();return 0;
}

执行代码:

查看进程的内存的分布情况,可以发现最右边没有 [head] 标志,说明是通过 mmap 以匿名映射的方式从文件映射区分配的匿名内存。

然后我们释放掉这个内存看看:

再次查看该 128 KB 内存的起始地址,可以发现已经不存在了,说明归还给了操作系统。

对于 「malloc 申请的内存,free 释放内存会归还给操作系统吗?」这个问题,我们可以做个总结了:

  • malloc 通过 brk() 方式申请的内存,free 释放内存的时候,并不会把内存归还给操作系统,而是缓存在 malloc 的内存池中,待下次使用
  • malloc 通过 mmap() 方式申请的内存,free 释放内存的时候,会把内存归还给操作系统,内存得到真正的释放

为什么不全部使用 mmap 来分配内存?

因为向操作系统申请内存,是要通过系统调用的,执行系统调用是要进入内核态的,然后在回到用户态,运行态的切换会耗费不少时间。

所以,申请内存的操作应该避免频繁的系统调用,如果都用 mmap 来分配内存,等于每次都要执行系统调用。

另外,因为 mmap 分配的内存每次释放的时候,都会归还给操作系统,于是每次 mmap 分配的虚拟地址都是缺页状态的,然后在第一次访问该虚拟地址的时候,就会触发缺页中断。

也就是说,频繁通过 mmap 分配的内存话,不仅每次都会发生运行态的切换,还会发生缺页中断(在第一次访问虚拟地址后),这样会导致 CPU 消耗较大

为了改进这两个问题,malloc 通过 brk() 系统调用在堆空间申请内存的时候,由于堆空间是连续的,所以直接预分配更大的内存来作为内存池,当内存释放的时候,就缓存在内存池中。

等下次在申请内存的时候,就直接从内存池取出对应的内存块就行了,而且可能这个内存块的虚拟地址与物理地址的映射关系还存在,这样不仅减少了系统调用的次数,也减少了缺页中断的次数,这将大大降低 CPU 的消耗

既然 brk 那么牛逼,为什么不全部使用 brk 来分配?

前面我们提到通过 brk 从堆空间分配的内存,并不会归还给操作系统,那么我们那考虑这样一个场景。

如果我们连续申请了 10k,20k,30k 这三片内存,如果 10k 和 20k 这两片释放了,变为了空闲内存空间,如果下次申请的内存小于 30k,那么就可以重用这个空闲内存空间。

但是如果下次申请的内存大于 30k,没有可用的空闲内存空间,必须向 OS 申请,实际使用内存继续增大。

因此,随着系统频繁地 malloc 和 free ,尤其对于小块内存,堆内将产生越来越多不可用的碎片,导致“内存泄露”。而这种“泄露”现象使用 valgrind 是无法检测出来的。

所以,malloc 实现中,充分考虑了 sbrk 和 mmap 行为上的差异及优缺点,默认分配大块内存 (128KB) 才使用 mmap 分配内存空间。

free() 函数只传入一个内存地址,为什么能知道要释放多大的内存?

还记得,我前面提到, malloc 返回给用户态的内存起始地址比进程的堆空间起始地址多了 16 字节吗?

这个多出来的 16 字节就是保存了该内存块的描述信息,比如有该内存块的大小。

这样当执行 free() 函数时,free 会对传入进来的内存地址向左偏移 16 字节,然后从这个 16 字节的分析出当前的内存块的大小,自然就知道要释放多大的内存了。

malloc 是如何分配内存的?相关推荐

  1. 使用malloc为指针分配内存空间

    #include <stdio.h> #include <stdlib.h> //使用malloc 自己创建数组空间 --操作一维数组 int main(void){const ...

  2. malloc函数分配内存失败的原因及解决方法

    原文链接:http://blog.csdn.net/lighthear/article/details/70146602 malloc函数分配内存失败的原因及解决方法 先说结论 malloc()函数分 ...

  3. c语言动态分配内存keil,keil5中结构体分配内存问题

    //头文件中的定义/ //纯C里面定义的布尔类型 typedef enum { False = 0, True = 1 }Bool; //定义矩阵元素的类型为matrixType typedef do ...

  4. 详细讲解从用户空间申请内存到内核如何为其分配内存的过程

    Linux内存管理 摘要:本章首先以应用程序开发者的角度审视Linux的进程内存管理,在此基础上逐步深入到内核中讨论系统物理内存管理和内核内存的使用方法.力求从外到内.水到渠成地引导网友分析Linux ...

  5. brk16_Linux进程分配内存的两种方式--brk() 和mmap()

    如何查看进程发生缺页中断的次数? 用ps -o majflt,minflt -C program命令查看. majflt代表major fault,中文名叫大错误,minflt代表minor faul ...

  6. malloc 初始化_关于内存分配malloc、calloc、realloc的区别

    (1) malloc() 在内存的动态存储区中分配一块长度为size字节的连续区域,参数size为需要内存空间的长度,返回该区域的首地址 (2) calloc() 与malloc相似,参数sizeOf ...

  7. C语言中malloc为字符型指针分配内存引起的缓冲区泄露

    /* 问题描述; 缓冲区溢出: (1)malloc:分配一块连续的未被使用得当内存块,但是不能保证内存块临近的其他内存块也未被使用: (2)当用malloc未char类型指针分配一个字节长度内存时,但 ...

  8. malloc分配内存的原理?

    malloc分配内存的原理 malloc的原理 1.放置已分配的块 2.分割空闲块 3.合并空闲块 malloc的原理 步骤分为放置.分割和合并 在堆中,堆块由一个字的头部.有效载荷.填充以及一个字的 ...

  9. malloc如何分配内存

    目录 一.brk()系统调用 1.brk()的申请方式 2.brk()系统调用的优缺点 3.brk()系统调用的优化 二.mmap()系统调用 1.mmap基础概念 2.mmap 内存映射原理 3.m ...

  10. C Primer Plus 第12章 12.6 分配内存:malloc()和free()

    2019独角兽企业重金招聘Python工程师标准>>> 首先,回顾一些有关内存分配的事实.所有的程序都必须留出足够内存来存储它们使用的数据.一些内存分配是自动完成的.例如,可以这样声 ...

最新文章

  1. iOS - 图文混排技术方案分享
  2. 大数据洞察画像自动化实践
  3. win7下安装Oracle10g解决方案
  4. Tuxera NTFS教程:在Mac上如何将MS-DOS文件系统格式化为NTFS文件系统?
  5. Firefox 用户加载的半数网页启用了 HTTPS
  6. php 通讯协议,通讯协议作用
  7. halcon 旋转_HALCON高级篇:面阵相机模型及其坐标转换
  8. RFID 打印机是什么
  9. 【笔记】《C#高效编程改进C#代码的50个行之有效的办法》第1章C#语言习惯(1)--属性的特性以及索引器(SamWang)...
  10. 用vlc搭建简单流媒体服务器(UDP和TCP方式)-转 rtsp很慢才能显示
  11. 利用PS将图片上的中文改写成英文
  12. android: 禁止多点触控
  13. 北京第一年-OpenGL-7 egl wgl glx agl glew window display surface context rendertarget glfw都是什么?
  14. [附源码]计算机毕业设计JAVAjsp教学辅助系统
  15. 华为ME909 4G LTE模块在树莓派+Ubuntu Mate平台的联网演示
  16. hdu5399 Too simple
  17. 原生js由html创建节点,[js高手之路]HTML标签解释成DOM节点的实现方法
  18. 中科院 鲁士文 计算机网络,《计算机网络-鲁士文》10_基于IP的多协议标记交换技术.pptx...
  19. RS232 RS485 串口 电平标准
  20. Kubernetes 在网易云中的落地优化实践

热门文章

  1. 异步时钟脉冲同步器的设计
  2. java网页开发中的乱码问题解决(过滤器)
  3. 【制作】基于金沙滩51单片机的电子跑表
  4. Latex输出大小写罗马数字
  5. c语言中罗马字母数字,C语言程序经典示例—-(22)阿拉伯数字转换为罗马数字...
  6. Java 面试简答题
  7. python爬虫爬取淘宝美食_python爬虫爬取淘宝商品信息
  8. 手机wps怎么设置打印横竖_wps怎么设置横向打印
  9. 2020年最佳恶意软件删除工具Top 10
  10. WORD安全模式怎么解除?