首页 > 系统相关 >linux 内存管理 ------ malloc 的内存分配

linux 内存管理 ------ malloc 的内存分配

时间:2023-06-03 12:34:52浏览次数:47  
标签:malloc 字节 mmap free 内存 linux 操作系统

 

低于128K的内存分配采用具有内存池缓存机制的 brk 方式,可以减少缺页中断、系统调用的次数。高于128K时采用匿名内存映射区的mmap方式, 避免产生太大的内存碎片

 

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

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

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

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

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

#include <stdio.h>
#include <malloc.h>

int main() {
  //申请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;
}

[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 释放内存后,堆内存还是存在的,并没有归还给操作系统。

这是因为与其把这 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;
}

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

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

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

总结:

  • 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 实现中,充分考虑了 brk 和 mmap 行为上的差异及优缺点,默认分配大块内存 (128KB) 才使用 mmap 分配内存空间。

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

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

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

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

 

标签:malloc,字节,mmap,free,内存,linux,操作系统
From: https://www.cnblogs.com/god-of-death/p/17453805.html

相关文章

  • 《Linux基础与服务管理(基于CentOS 7.6)》pdf电子书免费下载
    本书以目前广泛使用的CentOS 7.6平台为例,由浅入深、系统地介绍了Linux基础及对Linux各种服务的管理。*书共11章,主要内容*括Linux简介、基础*作命令、账户与权限管理、文件系统与磁盘管理、网络管理与系统监控、软件*管理、进程与基础服务、常用服务器配置、常用集群配置、常用系......
  • Linux页表与ARM硬件页表
    早期Linux内核是基于x86体系结构设计的,x86页表中有3个标志位是ARM32硬件页表没有的。PTE_DIRTY:cpu在写操作时会设置该标志位,表示对应页面被写过,为脏页。PTE_YOUNG:CPU访问该页时会设置该标志位。在页面换出时,如果该标志位位置了,说明该页刚被访问过,页面是young的,不适合把该页换出,同......
  • Java内存模型
    一、Java内存模型简介1.Java内存模型的“底层原理”从Java代码到CPU指令的变化过程是怎样的?最开始,我们编写的Java代码,即*.Java文件在执行编译Javac命令后,从刚才的*.Java文件会变出一个新的Java字节码文件,即*.class文件JVM会执行刚才生成的*.class字节码文件,并把字节码文......
  • windows系统编译的Qt程序转到国产化麒麟linux中编译
    团队自研股票软件,关威信共总号:QStockView,下载1.1 windows系统编译的Qt程序转到国产化麒麟linux中编译(1)把Vs工程项目文件导入到Linux中首先把vs的工程拷贝到linux里面(可以用虚拟机的共享文件夹功能),把工程里面的目录Debug、GeneratedFiles、Release、Win32、x64和文件…user、......
  • 多线程-线程池与java内存模型
    多线程-线程池与java内存模型线程池的使用(思路:什么是线程池->他的基本构造以及参数含义->如何使用,使用过程中需要注意什么->有哪些好用的工具类)线程池的基笨概念:首先看一下的继承关系,其次看他的状态,它是利用int的高三位表示状态,比如111表示能接受任务,具体看下面第二章图接下来看......
  • k8s集群外的linux加入prometheus监控
    client配置:准备一台linux,不在k8s集群中,安装node_exporterwgethttps://github.com/prometheus/node_exporter/releases/download/v1.6.0/node_exporter-1.6.0.linux-amd64.tar.gztar-xvfnode_exporter-1.6.0.linux-amd64.tar.gzmvnode_exporter-1.6.0.linux-amd64/usr/lo......
  • 关于int**在malloc为二维数组分配空间时候的作用见解
    关于int**在用malloc函数为二维数组分配空间时候int**  二级指针类型二维数组的数组名为行指针,写成 arr =(char**)malloc(n*sizeof(char))时,arr并不是二维数组的数组名,而是指针数组的数组名,指针数组的数组名是二级指针,所以可以用int**把malloc分配的空间强制转换成二级指针类......
  • linux 性能自我学习 ———— 软中断 [五]
    前言linux性能的自我学习。正文什么是软中断呢?举一个网络的例子。linux将中断处理过程分为两个阶段:上半部用来快速处理中断,他在中断禁止模式下运行,注意是处理跟硬件紧密相关或时间敏感的工作。下半部用来延迟处理上半部未完成的工作,通常以内核线程的方式运行。比如网卡......
  • 国产化麒麟linux系统QtCreator和QtCreator编译的程序无法输入中文libfcitx最新版本编
    1.问题描述麒麟linux系统QtCreator和QtCreator编译的程序无法输入中文,网上找了很多的libfcitxplatforminputcontextplugin.so库都无法使用正常输入;Qt版本:5.9.6麒麟系统版本:海光麒麟桌面版kylin V10 SP1  小版本号2203XC-P923P_KOS_2203_AMD_HG_3250_220630_AUDIT_ACTIVE.i......
  • linux卸载MySQL
    linux卸载MySQL一查找以前是否装有mysqlrpm-qa|grep-imysql显示之前安装了:MySQL-server-5.6.22-1.el6.i686MySQL-client-5.6.22-1.el6.i686二停止mysql服务、删除之前安装的mysql删除命令:rpm-e–nodeps包名rpm-evMySQL-server-5.6.22-1.el6.i686rpm-evMySQL-cli......