Linux堆管理策略详解
Posted xingzherufeng
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux堆管理策略详解相关的知识,希望对你有一定的参考价值。
最近看了linux堆管理的文章,这篇博文是对文章的提炼和总结。
入门二进制很难啊!
Linux堆管理策略
1、总述
在主线程中调用malloc之后会发现系统给程序分配了堆,且恰好在数据段之上。这说明它是通过brk系统调用实现。并且分配的地址空间大小远大于申请的大小,我们把它称之为main arena(每个arena中含有多个chunk,这些chunk以链表的形式加以组织)。由于申请到的地址空间比你所需的地址空间大很多,主线程在后续如果再申请堆空间的话,会从132KB的剩余部分中申请,直到用完或不够用的时候,再通过增加program break location的方式来增加main arena的大小。同理,当main arena中有过多空闲内存的时候,也会通过减少program break location的方式缩小arena。
在主线程调用Free函数后,在内存中程序的堆空间并未被释放而是由glibc的malloc库函数加以管理。它会将释放的chunk添加到main arenas的bin(记录空闲链表的结构称为bins,之后每次用户调用malloc申请堆空间时,glibc malloc会先尝试从bins中找到一个满足要求的chunk,如果没有才向操作系统申请新的堆空间)。
子线程在申请堆空间时,操作系统并不通过brk系统调用分配而是通过mmap系统调用分配,其他与主线程分配释放均相同。
2、Arena介绍
2.1、数量限制
虽然主线程和子线程均有自己独立的arena,但是arena的数量是固定的,它与系统中处理器核心个数相关。
32bit:y=2*x+1
64bit:y=8*x+1
其中y为arena个数,x为处理器核心数
2.2、Arena的管理
对于共享Arena的问题,此处以单核32bit处理器为例。当主线程首次调用malloc时,glibc malloc会直接为它分配一个main arena。当子线程1和子线程2首次调用malloc时,glibc malloc会分别为每一个用户线程创建一个新的arena。但是当第三个子线程申请malloc时,glibc malloc会循环便利所有的Arena,当找到一个可以lock的堆块时(当前子线程未使用堆内存表示可以lock),将其返回给申请的子进程即共享arena。但是如果没有可用的arena,那么申请堆内存的子线程将被阻塞,直到有可用的arena为止。其他复杂情况可以依次类推。
3、堆管理
3.1、总述
(1)、heap_info:
即Heap Header,由于一个thread heap(main heap除外)可以包含多个heaps,因此为了高效管理heap,将每个heap分配一个heap header。(当现在的heap不够用时,从前面的介绍中我们知道,会通过mmap系统调用申请信道heap,那么此时新的heap会被添加到当前的thread heap中,因此出现一个thread heap包含多个heap的情况)
typedef struct_heap_info{
mstate ar_ptr;//属于哪个Arena
struct_heap_info *prev;//前一个heap_info
size_t size;//当前字节大小
size_t mprotect_size;
char pad[-6*SIZE_SZ&MALLOC_ALIGN_MASK];//字节对齐
}heap_info;
(2)、malloc_state:
即Arena Header,每个thread只含有一个Arena Header。Arena Header包含bins的信息、top chunck以及最后一个remainder chunk等。
struct malloc_state{
mutex_t mutex;
int flags;
mfastbinptr fastbinsY[NFASTBINS];//快表
mchunkptr top;
mchunkptr last_remainder;
mchunkptr bins[NBINS*2-2];
unsigned int binmap[BINMAPSIZE];
struct malloc_state *next;
struct malloc_state *next_free;
INTERNAL_SIZE_T system_mem;
INTERNAL_SIZE_T max_system_mem;
};
(3)、malloc_chunk
即Chunk Header,一个heap可以分成多个chunk,每个chunk的大小由用户调用malloc时参数size“决定”。
struct malloc_chunk{
INTERNAL_SIZE_T prev_size;//前一个空闲chunk的大小
INTERNAL_SIZE_T size;//chunk的大小
struct malloc_chunk *fd;//双向链表,只有空闲chunk存在
struct malloc_chunk *bk;
};
3.2、heap segment和arena关系
线程arena只含有一个malloc_state即arena header,但是有多个heap_info即heap header。且两个heap若通过mmap分配,那么他们在内存上并不相邻而是属于不同的内存区间,为了便于管理,libc malloc将后一个heap_info结构体的prev成员指向前一个heap_info结构体的起始位置即ar_ptr成员,而第一个heap_info的ar_ptr成员指向malloc_state,从而构成一个单链表,方便管理。
4、再议chunk
chunk可是说是堆内存管理里最小的操作单位。chunk分为4类:1)allocated chunk、2)free chunk、3)top chunk、4)last remainder chunk。为了方便讨论,暂且分为allocated chunk和free chunk。
4.1、隐式链表技术
由于堆内存是以chunk为单位进行管理,因此需要明确规定chunk的边界,并且标记以分配块和空闲块。对于已分配的chunk来说有一个指向payload的指针(在malloc时返回的指针就指向payload开始处)。Chunk size作为chunk的头部,因为8字节对齐的原因可以看出chunk size的后三位是无效的,因此它们用作chunk的标志位。其中,第0bit位用于标记chunk是否分配(1表示已分配,0表示空闲)。Padding部分用于地址对齐,整个chunk的大小必须为8的整数倍。
缺点:效率很低,在内存回收的时候难以将相邻多个free chunk合并,这将会产生大量无法分配的小chunk,最终整个内存都将消耗殆尽。
4.2、带边界标记的合并技术
为了解决chunk前一个相邻的chunk空闲而合并这两个chunk缺需要遍历整个chunk,并且意味着释放chunk消耗的时间也与堆的大小成线性关系。因此提出一种解决办法-边界标记。
在每一个chunk的padding后添加一个头部的副本,我们称之为脚部(Footer)。由于这个脚部处于下一个chunk的前4个字节,因此可以很轻松的定位前一个块并进行合并。
缺点:由于添加了脚部使得chunk的大小变大,而对于申请很小chunk的操作来说会造成很大的性能损耗。同时,我们只对于空闲chunk需要进行合并操作,那么对于已分配的chunk我们并不需要脚部。
优化:将前一个chunk是否分配的标志位存储在当前chunk的1或2bit位上。因此我们可以根据当前chunk的bit位判断前一个chunk是否分配,从而判断当前chunk头的前四个字节是否为前一个chunk的脚部
4.3、支持多线程的chunk
为了能够标记当前chunk是否属于thread arena,判断申请方式应该由mmap还是brk实现。但是由于当前的chunk只剩下一个多余的bit位,因此需要对chunk进行大改动。
由于头部即保存了本chunk是否被分配也保存了前一个chunk是否被分配,因此我们认为这完全没有必要,当前的chunk是否已分配可以根据下一个chunk的标志位来实现。因此我们将剩下的bit位作为标记多线程的标志位。
其中N表示当前chunk是否是thread arena,M表示当前chunk是否通过mmap系统调用产生,P表示前一个chunk是否被分配。
优化:发现没有必要保存chunk size的副本,但是空闲块合并的话又必须知道前一个chunk的size。因此我们将chunk的脚部移动到首部的前面同时其不在保存当前chunk的size,而是保存前一个chunk的size。并且只有当前一个chunk空闲时这个脚部才有用,分配的chunk这个脚部将当作payload或者padding的一部分。
总之,malloc_chunk中的prev_size和size构成了隐式链表,而后续的fd,bk等指针并不是作用于隐式链表的,而是用作加快内存分配和释放效率的显示链表bin(只在free chunk中存在)
5、Top Chunk
当一个chunk处于一个arena的最顶部(最高内存地址处)的时候,称之为top chunk。该chunk不属于任何一个bin,而是当系统中所有free chunk即无论哪种bin都无法满足用户请求的内存大小的时候,将此chunk分配给用户使用。如果top chunk的大小比用户请求的大小还要大的话,就将该top chunk作为两个部分:1)用户请求的chunk;2)剩余部分成为新的top chunk。否则,就需要拓展或申请新的heap(在main arena中用sbrk拓展,在thread arena中通过mmap分配)。
6、Last Remainder Chunk
当用户请求一个small bin且无法被small bin、unsorted bin满足时会通过binmaps遍历bin查找chunk,如果该chunk有剩余部分的话就将该剩余部分变成新的chunk加入unsorted bin,同时变成last remainder chunk。
它的出现主要是为了提高连续malloc(small chunk)操作时的效率。
7、bin介绍
Bin是一种记录free chunk的链表结构,根据其大小将其分为4类:Fast bin、Unsorted bin、Small bin、Larger bin。用于记录bin的数据结构有两种:fastbinY:这是一个数组,记录所有fast bins;bins:这也是一个数组,用于记录除fast bins之外的所有bins。事实上,一共有126个bins,分别为:bin 1为unsorted bin;bin 2-63为small bin;bin 64-126为large bin。
具体数据结构定义如下:
Struct malloc_state{
/*Fastbins*/
Mfastbinptr fastbinsY[NFASTBINS];
......
/*Normal bins packed as described above*/
Mchunkptr bins[NBINS*2-2];//#define NBINS 128
......
};
其中mfastbinptr:typedef struct malloc_chunk *mfastbinptr;mchunkptr:typedef struct malloc_chunk *mchunkptr。
8、Fast bin
Chunk size为16到80字节的chunk叫做fast chunk。Chunk size表示该malloc_chunk的实际整体大小。而下文会用到的chunk unused size就表示该malloc_chunk中刨除诸如prev_size,size,fd,bk这类辅助成员之后实际可用的大小。因此,对free chunk而言,其实际可用的大小总是比实际整体大小少16字节。在所有bin操作中,fast bin是最快的。
1) fast bin的个数固定为10个。
2) 每个fast bin都是一个单链表(只有fd指针)。Fast bin中无论添加还是移除fast chunk都是对链表尾进行操作,不会对某个中间的fast chunk进行操作。即添加free内存到链表尾,删除(malloc内存)是将链表尾部的fast chunk删除。因此在fastbinsY数组中每个fastbin元素均指向了该链表的尾节点,而尾节点的fd指向前一个节点。
3) 10个fast bin中所包含的fast chunk size是按照步进8字节排序的,即第一个fast bin中所有fast chunk size均为16字节,第二个fast bin中为24字节,依此类推。在malloc时,最大的fast chunk size被设置为80字节(chunk unused size为64字节),因此默认情况下为16-80字节的chunk被分类到fast chunk。
4) Fast bin中的chunk不进行合并操作,即将其chunk的是否分配标志位总是置为1(已分配状态)。
5) 当用户通过malloc请求的大小属于fast chunk时(用户请求的大小+16字节则为实际的chunk size)。在初始化时fast bin支持的最大内存大小及所有fast bin链表均为空,所以最开始申请内存时不会交由fast bin处理。
当第一次调用malloc(fast bin)的时候,系统执行_int_malloc函数,该函数首先会发现当前fast bin为空,就会转交给small bin处理,发现small bin也为空,就调用malloc_consolidate函数对malloc_state结构体进行初始化。Malloc_state结构体功能如下:
首先判断当前malloc_state结构体中的fast bin是否为空,如果为空就说明整个malloc_都未初始化,则进行初始化。调用malloc_init_state(av)函数,该函数先初始化除fast bin之外的所有bins,再初始化fast bins。然后当再次执行malloc(fast chunk)函数时即可以使用fast bins。反之,当执行free(fast chunk)操作时,先通过chunksize函数根据传入的地址指针获取该指针对应的chunk的大小;然后根据这个chunk大小获取chunk所属的fast bin,然后再将此chunk添加到该fast bin的链尾即可。整个操作在_int_free函数中完成。
9、unsorted bin
当释放较小或较大的chunk的时候,如果系统没有将它们添加到对应的bins中,系统就将这些chunk添加到unsorted bin中。这是让管理机制能够第二次机会重新利用最近释放的chunk(第一次为fast bin机制)。Unsorted bin是一个由free chunks组成的循环双链表。在unsorted bin中,对chunk的大小并没有限制,任何大小的chunk都可以归属到unsorted bin中。
10、Small bin
小于512字节的chunk称之为small chunk,small bin就是用于管理small chunk的。就内存的分配和释放速度而言,small bin比larger bin快,但比fast bin慢。Small bin特性如下:
1) small bin有62个。每个都是一个由对应free chunk组成的循环双链表。并且内存释放时将新释放的chunk添加到链表的前端,分配内存时从链表的尾端获取chunk。
2) 第一个chunk大小为16字节,后续每个small bin中chunk的大小依次增加8字节,即最后一个small bin的chunk为16+62*8=512字节。
3) 相邻的空闲块需要进行合并操作。
4) Malloc操作与fast bins类似,初始时都为空,交由unsorted bin处理。处理不了则依次向后遍历,直到top chunk经扩充后一定能处理。
5) 当释放small chunk操作时,先检查该chunk相邻的chunk是否为free,是则合并,并从small bin中移除,添加到unsorted bin中。
11、Large bin
大于512字节的chunk称之为larger chunk。Large bin用于管理这些large chunk。
1) large chunk数量63个。并且large chunk中的chunk大小可以不一样,但必须处于某个给定的范围。Large chunk可以添加删除在large chunk的任何一个位置。在这63个large bins中,前32个large bin依次以64字节步长为间隔,即第一个large bin中chunk size为512~575字节,第二个large bin中chunk size为576 ~ 639字节。紧随其后的16个large bin依次以512字节步长为间隔;之后的8个bin以步长4096为间隔;再之后的4个bin以32768字节为间隔;之后的2个bin以262144字节为间隔;剩下的chunk就放在最后一个large bin中。鉴于同一个large bin中每个chunk的大小不一定相同,因此为了加快内存分配和释放的速度,就将同一个large bin中的所有chunk按照chunk size进行从大到小的排列:最大的chunk放在链表的front end,最小的chunk放在rear end。
2) 合并操作类似于small bin
3) 初始化操作类似于small bin。初始化完成后,首先确定用户请求的大小属于哪一个large bin,然后判断large bin中最大的chunk的size是否大于用户请求的size(通过链表中front end的size即可),如果大于就从链表尾部遍历第一个大小相近或接近的chunk分配给用户(若此chunk过大就拆分后返回给unsorted bin)。由于bin的个数较多,并且不同bin中的chunk极有可能在不同的内存页,如果遍历每一个bin中的chunk就有可能会发生多次内存页中断操作,因此glibc设计了Binmap结构体来帮助提高bin-by-bin检索的速度。Binmap记录了各个bin中是否为空,通过bitmap可以避免检索一些空的bin。
4) 释放操作与small chunk类似。
以上是关于Linux堆管理策略详解的主要内容,如果未能解决你的问题,请参考以下文章
Linux中netfilter火墙访问控制策略优化详解(上)—iptables
Linux运维--企业sudo权限规划详解 (实测一个堆命令搞定)