malloc怎么分配内存,malloc函数分配的内存在哪儿
作者:小林编码
图形基础计算机网站:https://xiaolincoding.com/
大家好,我是小林。
很久以前我写过一篇阐述虚拟内存的文章:太棒了!20张图揭开内存管理的迷雾,豁然开朗。
最近想多写一些关于内存管理的文章。这一次,我们将以malloc动态内存分配为出发点。我在本文中还做了一个小实验:
malloc如何分配内存?malloc分配物理内存吗?malloc(1)将分配多少内存?释放内存,会不会还给操作系统?free()函数只传入一个内存地址。为什么知道要释放多少内存?开始!
Linux进程的内存分配是什么样的?在Linux操作系统中,虚拟地址空间分为内核空间和用户空间,对于不同的位系统,地址空间的范围也是不同的。例如,最常见的32位和64位系统如下:
从这里可以看出:
32位系统的内核空间占用1G,位于最高点,剩下的3G是用户空间;64位系统的内核空间和用户空间都是128T,分别占据整个内存空间的最高和最低位,中间部分的其余部分未定义。我们来谈谈内核空间和用户空间的区别:
当进程处于用户状态时,只能访问用户空间内存;只有进入内核状态后,才能访问内核空间的内存;虽然每个进程都有自己的虚拟内存,但是每个虚拟内存中的内核地址实际上是与同一个物理内存相关联的。这样,在进程切换到达内核状态后,它可以很容易地访问内核空间内存。
接下来,详细了解一下虚拟空间的划分。用户空间和内核空间的划分方式不同,所以不讨论内核空间的分配。
再来看用户的空间分布。以32位系统为例。我画了一张图来说明它们的关系:
从这个图中可以看出,用户空间内存从低到高分为六个不同的内存段:
程序段,包括二进制可执行代码;初始化的数据段,包括静态常数;未初始化的数据段,包括未初始化的静态变量;段,包括动态分配的内存,从低位地址向上增长;文件映射段,包括动态库、共享内存等。从低地址开始向上增长(与硬件和内核版本有关);栈,包括局部变量和函数调用的上下文等。堆栈的大小是固定的,通常是8 MB。当然,系统也提供参数以便我们定制尺寸;在这六个内存段中,堆和文件映射段的内存是动态分配的。例如,使用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字节内存时,实际分配了多少内存空间。
#包含stdio.h
#包含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();
//释放内存
免费(addr);
Printf (1字节内存被释放,但堆heap没有\ n );
getchar();
返回0;
}执行代码:
我们可以通过/proc//maps文件检查进程的内存分配情况。我通过maps文件中这个1字节的内存起始地址过滤出内存地址的范围。
[root @ Xiaolin ~]# cat/proc/3191/maps grep d730
00d 73000-00d 94000 rw-p 0000000 00:00 0[heap]这个例子分配的内存不到128 KB,所以是通过brk()系统调用应用到堆空间的内存,所以最右边可以看到[HEAP]的logo。
可以看到,堆空间的内存地址范围是00d73000-00d94000,也就是132KB,也就是说malloc(1)实际预分配了132 KB的内存。
有些同学可能注意到了,程序中打印的内存起始地址是d73010,而maps文件显示的堆内存空间起始地址是d73000。为什么多了一个0x10 (16字节)?这个问题,暂且不说,以后再说。
释放内存,会不会还给操作系统?我们按照上面的过程往下走,看看通过free()函数释放内存后,堆内存是否还在。
从下图可以看出,内存被free释放后,堆内存依然存在,并没有返回给操作系统。
这是因为与其将这1字节释放给操作系统,不如将其缓存并放入malloc的内存池中。当进程再次申请1字节内存时,可以直接重用,速度快很多。
当然,当进程退出时,操作系统会回收进程的所有资源。
堆内存在前面提到的空闲内存之后依然存在,这是针对malloc通过brk()申请的内存。
如果malloc通过mmap申请内存,free会释放内存并返回给操作系统。
我们来做一个实验,验证malloc申请128 KB的内存,这样malloc就可以通过mmap来分配内存。
#包含stdio.h
#包含malloc.h
int main() {
//申请1字节内存
void * addr=malloc(128 * 1024);
Printf(此128KB字节内存起始地址:%x\n ,addr);
Printf(使用cat /proc/%d/maps查看内存分配\n ,getpid());
//当输入任何字符时,阻塞程序并向下执行。
getchar();
//释放内存
免费(addr);
Printf (128KB的内存被释放,内存被返回给操作系统\ n );
getchar();
返回0;
}执行代码:
看进程的内存分配,可以发现最右边没有【head】符号,说明是mmap以匿名映射的方式从文件映射区分配的匿名内存。
那我们就把这段记忆释放出来看看吧:
再看一下128 KB内存的起始地址,可以发现它已经不存在了,说明已经返回给操作系统了。
对于malloc请求的内存,空闲内存会返回给操作系统吗?“这个问题,我们可以做一个总结:
malloc通过brk()申请的内存,在释放内存时不会返回给操作系统,而是缓存在malloc的内存池中供下次使用;Malloc通过mmap()申请内存,当它免费释放内存时,会把内存归还给操作系统,内存才真正被释放。为什么不都用mmap来分配内存?因为从操作系统申请内存是通过系统调用的,系统调用的执行是进入内核状态,然后回到用户状态后要花很多时间切换运行状态。
所以申请内存的操作要避免频繁的系统调用。如果用mmap分配内存,相当于每次都执行系统调用。
另外,由于mmap分配的内存每次释放都会返回给操作系统,所以mmap分配的虚拟地址每次都处于缺页状态,然后第一次访问虚拟地址就会触发缺页中断。
也就是说,如果通过mmap频繁分配内存,不仅每次都会发生运行状态切换,还会发生缺页中断(第一次访问虚拟地址后),导致CPU消耗较大。
为了改善这两个问题,malloc通过brk()系统调用在堆空间申请内存时,由于堆空间是连续的,所以直接预分配较大的内存作为内存池,内存释放时缓存在内存池中。
下次申请内存时,直接从内存池中取出对应的内存块即可,这个内存块的虚拟地址和物理地址的映射关系可能仍然存在,这样不仅减少了系统调用的次数,也减少了缺页中断的次数,会大大降低CPU的消耗。
既然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个字节的分析中,自然会知道当前内存块的大小,从而知道要释放多少内存。
郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。