glibc内存管理那些事儿
本文转载自glibc内存管理那些事儿
Linux内存空间简介
32位Linux平台下进程虚拟地址空间分布如下图:
进程虚拟地址空间分布
图中,0xC0000000开始的最高1G空间是内核地址空间,剩下3G空间是用户态空间。用户态空间从上到下依次为stack栈(向下增长)、mmap(匿名文件映射区)、Heap堆(向上增长)、bss数据段、数据段、只读代码段。
其中,Heap区是程序的动态内存区,同时也是C++内存泄漏的温床。malloc
、free
均发生在这个区域。本文将简单介绍下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时,采用了最佳适应的伙伴算法。举例如下:
如果分配内存<512字节,则通过内存大小定位到smallbins对应的index上(
floor(size/8)
)- 如果smallbins[index]为空,进入步骤3
- 如果smallbins[index]非空,直接返回第一个chunk
如果分配内存>512字节,则定位到largebins对应的index上
- 如果largebins[index]为空,进入步骤3
- 如果largebins[index]非空,扫描链表,找到第一个大小最合适的chunk,如size=12.5K,则使用chunk B,剩下的0.5k放入unsorted_list中
遍历unsorted_list,查找合适size的chunk,如果找到则返回;否则,将这些chunk都归类放到smallbins和largebins里面
index++从更大的链表中查找,直到找到合适大小的chunk为止,找到后将chunk拆分,并将剩余的加入到unsorted_list中
如果还没有找到,那么使用top chunk
或者,内存<128k,使用brk;内存>128k,使用mmap获取新内存
top chunk 如下图示:
top chunk
是堆顶的chunk,堆顶指针brk位于top chunk的顶部。移动brk指针,即可扩充top chunk的大小。当top chunk
大小超过128k(可配置)时,会触发malloc_trim
操作,调用sbrk(-size)
将内存归还操作系统。
chunk分布图
free
释放内存时,有两种情况:
- chunk和top chunk相邻,则和top chunk合并
- chunk和top chunk不相邻,则直接插入到
unsorted_list
中
内存碎片
以上图chunk分布图为例,按照glibc的内存分配策略,我们考虑下如下场景(假设brk其实地址是512k):
- malloc 40k内存,即chunkA,brk = 512k + 40k = 552k
- malloc 50k内存,即chunkB,brk = 552k + 50k = 602k
- malloc 60k内存,即chunkC,brk = 602k + 60k = 662k
- 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结构
- 一个arena由多个Heap构成
- 每个Heap通过mmap获得,最大为1M,多个Heap间可能不相邻
- Heap之间有prev指针指向前一个Heap
- 最上面的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内存响应减少。
参考文章
glibc内存管理那些事儿的更多相关文章
- 《Glibc内存管理》笔记DAY5
目录 分箱式内存管理 Unsorted bin Fast bins 核心结构体分析 malloc_state 内容来源 分箱式内存管理 Unsorted bin Unsorted bin 可以看作 ...
- 《Glibc内存管理》笔记DAY4
目录 分箱式内存管理 Small bins Large bins 内容来源 分箱式内存管理 对于空闲的 chunk,ptmalloc 采用分箱式内存管理方式,根据空闲 chunk 的大小和处于的状 ...
- 《Glibc内存管理》笔记DAY3
目录 边界标记法 内容来源 边界标记法 /* conversion from malloc headers to user pointers, and back */ #define chunk2me ...
- 《Glibc内存管理》笔记DAY2
目录 Ptmalloc内存管理设计 Main_arena 与 non_main_arena chunk 的组织 空闲 chunk 容器 sbrk 与 mmap 内存分配概述 内存回收概述 边界标记法 ...
- 《Glibc内存管理》笔记DAY1
目录 x86_64栈和mmap固定映射地址 内存的延迟分配 内核数据结构 mm_struct Heap 操作相关函数 Mmap 映射区域操作相关函数 内容来源 x86_64栈和mmap固定映射地址 ...
- 读书摘要观后感与总结:《Glibc内存管理:ptmalloc2源代码分析》
更新中 在Linux平台下做漏洞利用的时候,针对于Heap部分总是有些不求甚解,下面开个博文来记录下<Glibc内存管理:ptmalloc2源代码分析>这本书的读后感和收获,一些简单的点将 ...
- 2万字|30张图带你领略glibc内存管理精髓(因为OOM导致了上千万损失)
前言 大家好,我是雨乐. 5年前,在上家公司的时候,因为进程OOM造成了上千万的损失,当时用了一个月的时间来分析glibc源码,最终将问题彻底解决. 最近在逛知乎的时候,发现不少人有对malloc/f ...
- glibc内存管理方式
程序员接触的内存空间和系统接触的物理内存空间是有所区别的.对于一般进程来讲,他面对的是一个线性虚拟内存空间:地址从0到最大值.每一个进程面对的虚拟内存空间都是一样的,都享有全部的内存地址.虚拟内存空间 ...
- Glibc堆管理机制基础
最近正在学习linux下堆的管理机制,收集了书籍和网络上的资料,以自己的理解做了整理,做个记录.如果有什么不对的地方欢迎指出! Memory Allocator 常见的内存管理机制 dlmalloc: ...
随机推荐
- CVE-2018-4407(IOS缓冲区溢出漏洞)exp
CVE-2018-4407为ios缓冲区溢出漏洞 exp: import scapyfrom scapy.all import * send(IP(dst="同一局域网内目标Ip" ...
- Flink-v1.12官方网站翻译-P007-Data Pipelines & ETL
数据管道和ETL 对于Apache Flink来说,一个非常常见的用例是实现ETL(提取.转换.加载)管道,从一个或多个源中获取数据,进行一些转换和/或丰富,然后将结果存储在某个地方.在这一节中,我们 ...
- Python单元测试框架pytest常用测试报告类型
先前博客有介绍pytest测试框架的安装及使用,现在来聊聊pytest可以生成哪些测试报告 1.allure测试报告 关于allure报告参见先前的一篇博文:https://www.cnblogs.c ...
- Jenkins(3)拉取git仓库代码,执行python自动化脚本
前言 python自动化的脚本开发完成后需提交到git代码仓库,接下来就是用Jenkins拉取代码去构建自动化代码了 新建项目 打开Jenkins新建一个自由风格的项目 源码管理 Repository ...
- cartographer环境建立以及建图测试(详细级)
- Windows10与虚拟机中CentOS-7.2进行telnet通信 出现在端口23处失败【解决】
(telnet服务是由xinetd守护,所以安装和启动都要用到xinetd) 1.先检查CentOS7.0是否已经安装以下几个安装包:telnet-server.telnet.xinetd.命令如下: ...
- SPOJ1812 LCS2 - Longest Common Substring II【SAM LCS】
LCS2 - Longest Common Substring II 多个字符串找最长公共子串 以其中一个串建\(SAM\),然后用其他串一个个去匹配,每次的匹配方式和两个串找\(LCS\)一样,就是 ...
- Educational Codeforces Round 91 (Rated for Div. 2) C. Create The Teams (模拟)
题意:有\(n\)个队员,每个队友都有一个能力值,构造队伍,要求队伍人数*队伍中最低能力值不小于\(x\),求能构造的最大队伍数. 题解:大水题,排个序,倒着模拟就行了. 代码: int t; int ...
- HttpClient&&RestTemplate学习
1. 什么是HttpClient HttpClient是Apache下面的子项目,可以提供高效的,最新的,功能丰富的支持HTTP协议的客户端编程工具包. 2. 为什么要学习HttpClient Htt ...
- 4.Redis客户端的使用
标题 : 4.Redis客户端的使用 目录 : Redis 序号 : 4 Console.WriteLine($"北京和天津之间的距离是:{distance}公里"); #### ...