Linux
内存空间简介Linux
提供了如下几个系统调用,用于内存分配:
brk()/sbrk() // 通过移动Heap堆顶指针brk,达到增加内存目的
mmap()/munmap() // 通过文件影射的方式,把文件映射到mmap区
这两种方式分配的都是虚拟内存,没有分配物理内存。在第一次访问已分配的虚拟地址空间的时候,发生缺页中断,操作系统负责分配物理内存,然后建立虚拟内存和物理内存之间的映射关系。
那么,既然brk
、mmap
提供了内存分配的功能,直接使用brk
、mmap
进行内存管理不是更简单吗,为什么需要glibc
呢?
我们知道,系统调用本身会产生软中断,导致程序从用户态陷入内核态,比较消耗资源。
试想,如果频繁分配回收小块内存区,那么将有很大的性能耗费在系统调用中。
因此,为了减少系统调用带来的性能损耗,glibc
采用了内存池的设计,增加了一个代理层,每次内存分配,都优先从内存池中寻找,如果内存池中无法提供,再向操作系统申请。
一切计算机的问题都可以通过加层的方式解决。
glibc
的内存分配回收策略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结构
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
释放内存时,有两种情况:
1.chunk
和top chunk
相邻,则和top chunk
合并
2. 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
由多个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
#include
#include
#include
#include
#include
#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
内存相应减少。