本文转载自glibc内存管理那些事儿

Linux内存空间简介

32位Linux平台下进程虚拟地址空间分布如下图:

进程虚拟地址空间分布

图中,0xC0000000开始的最高1G空间是内核地址空间,剩下3G空间是用户态空间。用户态空间从上到下依次为stack栈(向下增长)、mmap(匿名文件映射区)、Heap堆(向上增长)、bss数据段、数据段、只读代码段。

其中,Heap区是程序的动态内存区,同时也是C++内存泄漏的温床。mallocfree均发生在这个区域。本文将简单介绍下glibc在动态内存管理方面的机制,抛砖引玉,希望能和大家多多交流。

Linux提供了如下几个系统调用,用于内存分配:

brk()/sbrk() // 通过移动Heap堆顶指针brk,达到增加内存目的
mmap()/munmap() // 通过文件影射的方式,把文件映射到mmap区

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

那么,既然brk、mmap提供了内存分配的功能,直接使用brk、mmap进行内存管理不是更简单吗,为什么需要glibc呢? 我们知道,系统调用本身会产生软中断,导致程序从用户态陷入内核态,比较消耗资源。试想,如果频繁分配回收小块内存区,那么将有很大的性能耗费在系统调用中。因此,为了减少系统调用带来的性能损耗,glibc采用了内存池的设计,增加了一个代理层,每次内存分配,都优先从内存池中寻找,如果内存池中无法提供,再向操作系统申请。

一切计算机的问题都可以通过加的方式解决。

glibc的内存分配回收策略

glibc中malloc内存分配逻辑如下是:

malloc

  • 分配内存 < DEFAULT_MMAP_THRESHOLD,走__brk,从内存池获取,失败的话走brk系统调用
  • 分配内存 > DEFAULT_MMAP_THRESHOLD,走__mmap,直接调用mmap系统调用

其中,DEFAULT_MMAP_THRESHOLD默认为128k,可通过mallopt进行设置。 重点看下小块内存(size > DEFAULT_MMAP_THRESHOLD)的分配,glibc使用的内存池如下图示:

内存池

内存池保存在bins这个长128的数组中,每个元素都是一双向个链表。其中:

  • bins[0]目前没有使用
  • bins[1]的链表称为unsorted_list,用于维护free释放的chunk。
  • bins[2,63)的区间称为small_bins,用于维护<512字节的内存块,其中每个元素对应的链表中的chunk大小相同,均为index*8。
  • bins[64,127)称为large_bins,用于维护>512字节的内存块,每个元素对应的链表中的chunk大小不同,index越大,链表中chunk的内存大小相差越大,例如: 下标为64的chunk大小介于[512, 512+64),下标为95的chunk大小介于[2k+1,2k+512)。同一条链表上的chunk,按照从小到大的顺序排列。

chunk数据结构

chunk结构

glibc在内存池中查找合适的chunk时,采用了最佳适应的伙伴算法。举例如下:

  1. 如果分配内存<512字节,则通过内存大小定位到smallbins对应的index上(floor(size/8))

    • 如果smallbins[index]为空,进入步骤3
    • 如果smallbins[index]非空,直接返回第一个chunk
  2. 如果分配内存>512字节,则定位到largebins对应的index上

    • 如果largebins[index]为空,进入步骤3
    • 如果largebins[index]非空,扫描链表,找到第一个大小最合适的chunk,如size=12.5K,则使用chunk B,剩下的0.5k放入unsorted_list中
  3. 遍历unsorted_list,查找合适size的chunk,如果找到则返回;否则,将这些chunk都归类放到smallbins和largebins里面

  4. index++从更大的链表中查找,直到找到合适大小的chunk为止,找到后将chunk拆分,并将剩余的加入到unsorted_list中

  5. 如果还没有找到,那么使用top chunk

  6. 或者,内存<128k,使用brk;内存>128k,使用mmap获取新内存

top chunk 如下图示: top chunk是堆顶的chunk,堆顶指针brk位于top chunk的顶部。移动brk指针,即可扩充top chunk的大小。top chunk大小超过128k(可配置)时,会触发malloc_trim操作,调用sbrk(-size)将内存归还操作系统

chunk分布图

free释放内存时,有两种情况:

  1. chunk和top chunk相邻,则和top chunk合并
  2. chunk和top chunk不相邻,则直接插入到unsorted_list

内存碎片

以上图chunk分布图为例,按照glibc的内存分配策略,我们考虑下如下场景(假设brk其实地址是512k):

  1. malloc 40k内存,即chunkA,brk = 512k + 40k = 552k
  2. malloc 50k内存,即chunkB,brk = 552k + 50k = 602k
  3. malloc 60k内存,即chunkC,brk = 602k + 60k = 662k
  4. free chunkA。

此时,由于brk = 662k,而释放的内存是位于[512k, 552k]之间,无法通过移动brk指针,将区域内内存交还操作系统,因此,在[512k, 552k]的区域内便形成了一个内存空洞 ---- 内存碎片。 按照glibc的策略,free后的chunkA区域由于不和top chunk相邻,因此,无法和top chunk 合并,应该挂在unsorted_list链表上。

glibc实现的一些重要结构

glibc中用于维护空闲内存的结构体是malloc_state,其主要定义如下:

struct malloc_state {
mutex_t mutex; // 并发编程下锁的竞争
mchunkptr top; // top chunk
unsigned int binmap[BINMAPSIZE]; // bitmap,加快bins中chunk判定
mchunkptr bins[NBINS * 2 - 2]; // bins,上文所述
mfastbinptr fastbinsY[NFASTBINS]; // fastbins,类似bins,维护的chunk更小(80字节的chunk链表)
...
}
static struct malloc_state main_arena; // 主arena

多线程下的竞争抢锁

并发条件下,main_arena引发的竞争将会成为限制程序性能的瓶颈所在,因此glibc采用了多arena机制,线程A分配内存时获取main_arena锁成功,将在main_arena所管理的内存中分配;此时线程B获取main_arena失败,glibc会新建一个arena1,此次内存分配从arena1中进行。 这种策略,一定程度上解决了多线程下竞争的问题;但是随着arena的增多,内存碎片出现的可能性也变大了。例如,main_arena中有10k、20k的空闲内存,线程B要获取20k的空闲内存,但是获取main_arena锁失败,导致留下20k的碎片,降低了内存使用率。

普通arena的内部结构:

普通arena结构

  1. 一个arena由多个Heap构成
  2. 每个Heap通过mmap获得,最大为1M,多个Heap间可能不相邻
  3. Heap之间有prev指针指向前一个Heap
  4. 最上面的Heap,也有top chunk

每个Heap里面也是由chunk组成,使用和main_arena完全相同的管理方式管理空闲chunk。 多个arena之间是通过链表连接的。如下图:

arena链表

main arena和普通arena的区别 main_arena是为一个使用brk指针的arena,由于brk是堆顶指针,一个进程中只可能有一个,因此普通arena无法使用brk进行内存分配。普通arena建立在mmap的机制上,内存管理方式和main_arena类似,只有一点区别,普通arena只有在整个arena都空闲时,才会调用munmap把内存还给操作系统。

一些特殊情况的分析

根据上文所述,glibc在调用malloc_trim时,需要满足如下2个条件:

1. size(top chunk) > 128K
2. brk = top chunk->base + size(top chunk)

假设,brk指针上面的空间已经被占用,无法通过移动brk指针获得新的地址空间,此时main_arena就无法扩容了吗? glibc的设计考虑了这样的特殊情况,此时,glibc会换用mmap操作来获取新空间(每次最少MMAP_AS_MORECORE_SIZE<1M>)。这样,main_arena和普通arena一样,由非连续的Heap块构成,不过这种情况下,glibc并未将这种mmap空间表示为Heap,因此,main_arena多个块之间是没有联系的,这就导致了main_arena从此无法归还给操作系统,永远保留在空闲内存中了。如下图示:

main_arena无法回收

显而易见,此时根本不可能满足调用malloc_trim的条件2,即:brk !== top chunk->base + size(top chunk),因为此时brk处于堆顶,而top chunk->base > brk.

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/mman.h>
#include <malloc.h> #define ARRAY_SIZE 127
char cmd[1024]; void print_info()
{
struct mallinfo mi = mallinfo();
system(cmd);
printf("\theap_malloc_total=%lu heap_free_total=%lu heap_in_use=%lu\n\
\tmmap_total=%lu mmap_count=%lu\n", mi.arena, mi.fordblks, mi.uordblks, mi.hblkhd, mi.hblks);
} int main(int argc, char** argv)
{
char** ptr_arr[ARRAY_SIZE];
int i;
char* mmap_var;
pid_t pid;
pid = getpid();
sprintf(cmd, "ps aux | grep %lu | grep -v grep", pid);
/* mmap占据堆顶后1M的地址空间 */
mmap_var = mmap((void*)sbrk(0) + 1024*1024, 127*1024, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
printf("before malloc\n");
print_info(); /* 分配内存,总大小超过1M,导致main_arena被拆分 */
for( i = 0; i < ARRAY_SIZE; i++) {
ptr_arr[i] = malloc(i * 1024);
}
printf("\nafter malloc\n");
print_info();
/* 释放所有内存,观察内存使用是否改变 */
for( i = 0; i < ARRAY_SIZE; i++) {
free(ptr_arr[i]);
}
printf("\nafter free\n");
print_info();
munmap(mmap_var, 127*1024);
return 1;
}

异常运行

作为对比,去除掉brk上面的mmap区再次运行后结果如下:

正常运行

可以看出,异常情况下(brk无法扩展),free的内存没有归还操作系统,而是留在了main_arena的unsorted_list了;而正常情况下,由于满足执行malloc_trim的条件,因此,free后,调用了sbrk(-size)把内存归还了操作系统,main_arena内存响应减少。

参考文章

  1. Linux 堆内存管理深入分析
  2. 深入剖析glibc内存管理实现及潜在问题
  3. 十问Linux虚拟内存管理(glibc)

glibc内存管理那些事儿的更多相关文章

  1. 《Glibc内存管理》笔记DAY5

    目录 分箱式内存管理 Unsorted bin Fast bins 核心结构体分析 malloc_state 内容来源 分箱式内存管理 Unsorted bin   Unsorted bin 可以看作 ...

  2. 《Glibc内存管理》笔记DAY4

    目录 分箱式内存管理 Small bins Large bins 内容来源 分箱式内存管理   对于空闲的 chunk,ptmalloc 采用分箱式内存管理方式,根据空闲 chunk 的大小和处于的状 ...

  3. 《Glibc内存管理》笔记DAY3

    目录 边界标记法 内容来源 边界标记法 /* conversion from malloc headers to user pointers, and back */ #define chunk2me ...

  4. 《Glibc内存管理》笔记DAY2

    目录 Ptmalloc内存管理设计 Main_arena 与 non_main_arena chunk 的组织 空闲 chunk 容器 sbrk 与 mmap 内存分配概述 内存回收概述 边界标记法 ...

  5. 《Glibc内存管理》笔记DAY1

    目录 x86_64栈和mmap固定映射地址 内存的延迟分配 内核数据结构 mm_struct Heap 操作相关函数 Mmap 映射区域操作相关函数 内容来源 x86_64栈和mmap固定映射地址   ...

  6. 读书摘要观后感与总结:《Glibc内存管理:ptmalloc2源代码分析》

    更新中 在Linux平台下做漏洞利用的时候,针对于Heap部分总是有些不求甚解,下面开个博文来记录下<Glibc内存管理:ptmalloc2源代码分析>这本书的读后感和收获,一些简单的点将 ...

  7. 2万字|30张图带你领略glibc内存管理精髓(因为OOM导致了上千万损失)

    前言 大家好,我是雨乐. 5年前,在上家公司的时候,因为进程OOM造成了上千万的损失,当时用了一个月的时间来分析glibc源码,最终将问题彻底解决. 最近在逛知乎的时候,发现不少人有对malloc/f ...

  8. glibc内存管理方式

    程序员接触的内存空间和系统接触的物理内存空间是有所区别的.对于一般进程来讲,他面对的是一个线性虚拟内存空间:地址从0到最大值.每一个进程面对的虚拟内存空间都是一样的,都享有全部的内存地址.虚拟内存空间 ...

  9. Glibc堆管理机制基础

    最近正在学习linux下堆的管理机制,收集了书籍和网络上的资料,以自己的理解做了整理,做个记录.如果有什么不对的地方欢迎指出! Memory Allocator 常见的内存管理机制 dlmalloc: ...

随机推荐

  1. CVE-2018-4407(IOS缓冲区溢出漏洞)exp

    CVE-2018-4407为ios缓冲区溢出漏洞 exp: import scapyfrom scapy.all import * send(IP(dst="同一局域网内目标Ip" ...

  2. Flink-v1.12官方网站翻译-P007-Data Pipelines & ETL

    数据管道和ETL 对于Apache Flink来说,一个非常常见的用例是实现ETL(提取.转换.加载)管道,从一个或多个源中获取数据,进行一些转换和/或丰富,然后将结果存储在某个地方.在这一节中,我们 ...

  3. Python单元测试框架pytest常用测试报告类型

    先前博客有介绍pytest测试框架的安装及使用,现在来聊聊pytest可以生成哪些测试报告 1.allure测试报告 关于allure报告参见先前的一篇博文:https://www.cnblogs.c ...

  4. Jenkins(3)拉取git仓库代码,执行python自动化脚本

    前言 python自动化的脚本开发完成后需提交到git代码仓库,接下来就是用Jenkins拉取代码去构建自动化代码了 新建项目 打开Jenkins新建一个自由风格的项目 源码管理 Repository ...

  5. cartographer环境建立以及建图测试(详细级)

  6. Windows10与虚拟机中CentOS-7.2进行telnet通信 出现在端口23处失败【解决】

    (telnet服务是由xinetd守护,所以安装和启动都要用到xinetd) 1.先检查CentOS7.0是否已经安装以下几个安装包:telnet-server.telnet.xinetd.命令如下: ...

  7. SPOJ1812 LCS2 - Longest Common Substring II【SAM LCS】

    LCS2 - Longest Common Substring II 多个字符串找最长公共子串 以其中一个串建\(SAM\),然后用其他串一个个去匹配,每次的匹配方式和两个串找\(LCS\)一样,就是 ...

  8. Educational Codeforces Round 91 (Rated for Div. 2) C. Create The Teams (模拟)

    题意:有\(n\)个队员,每个队友都有一个能力值,构造队伍,要求队伍人数*队伍中最低能力值不小于\(x\),求能构造的最大队伍数. 题解:大水题,排个序,倒着模拟就行了. 代码: int t; int ...

  9. HttpClient&&RestTemplate学习

    1. 什么是HttpClient HttpClient是Apache下面的子项目,可以提供高效的,最新的,功能丰富的支持HTTP协议的客户端编程工具包. 2. 为什么要学习HttpClient Htt ...

  10. 4.Redis客户端的使用

    标题 : 4.Redis客户端的使用 目录 : Redis 序号 : 4 Console.WriteLine($"北京和天津之间的距离是:{distance}公里"); #### ...