malloc是如何分配内存的

5 篇文章 4 订阅


内容来自小林conding 图解系统

malloc是如何分配内存的

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

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

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

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

在这里插入图片描述

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

在这里插入图片描述

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

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

● 如果用户分配的内存小于 128 KB,则通过 brk() 申请内存;
● 如果用户分配的内存大于 128 KB,则通过 mmap() 申请内存;
注意,不同的 glibc 版本定义的阈值也是不同的。

malloc分配的是虚拟内存,不是物理内存
可参考:Linux 内存管理 详解(虚拟内存、物理内存,进程地址空间)

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;
}

执行代码(先提前说明,我使用的 glibc 库的版本是 2.17):

图片

我们可以通过 /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()brk()

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和free的实现与原理
malloc的实现

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值