struct malloc_chunk {
INTERNAL_SIZE_T mchunk_prev_size;
INTERNAL_SIZE_T mchunk_size;
struct malloc_chunk* fd;
struct malloc_chunk* bk;
struct malloc_chunk* fd_nextsize; /* double links -- used only if free. */
struct malloc_chunk* bk_nextsize;
};
一、三者简介
NON_MAIN_ARENA:记录当前 chunk 是否不属于主线程
IS_MAPPED:记录当前chunk是否是由mmap分配的
PREV_INUSE:如果前面一个chunk处于分配状态,那么此位为1。一般来说,堆中第一个被分配的内存块的 size 字段的 P 位都会被设置为 1,以便于防止访问前面的非法内存。当一个chunk 的size 的P位为0时,我们能通过 prev_size 字段来获取上一个 chunk 的大小以及地址。这也方便进行空闲chunk之间的合并
二、glibc的实现
上篇文章中有介绍struct malloc_chunk结构体的大小的对齐原则,
在64位系统中为16字节对齐,在32位的系统中为8字节对齐。因此在64位系统中,struct malloc_chunk结构体的大小的最后4个字节是没有被使用的,32位系统中,最后3个字节是没有被使用的。如下图所示:
所以,glibc用struct malloc_chunk结构体的fd成员的最后3个比特位来表示当前chunk处于什么状态。只需要将fd成员相应的比特位置为1,就可以代表当前chunk处于什么状态
PREV_INUSE:值为1
IS_MMAPPED:值为2
NON_MAIN_ARENA:值为4
三、PREV_INUSE位导致的chunk合并
系统会根据chunk的PREV_INUSE位来进行合并操作。如果PREV_INUSE为1,说明该chunk前面的chunk正在被使用,那么该chunk不能用于合并;如果PREV_INUSE为0,说明该chunk前面的chunk为空闲状态,那么该chunk可以用于合并
合并的操作发生在_int_free函数中
#define prev_inuse(p) ((p)->mchunk_size & PREV_INUSE)
static void
_int_free (mstate av, mchunkptr p, int have_lock)
{
/* consolidate backward */
if (!prev_inuse(p)) {
prevsize = prev_size (p); //1
size += prevsize; //2
p = chunk_at_offset(p, -((long) prevsize)); //3
unlink(av, p, bck, fwd); //4
}
}
函数解析:
首先使用prev_inuse宏定义判断p的PREV_INUSE位是否为0如果PREV_INUSE位为0,那么prev_inuse宏返回0,if条件为1,进入if语句体中1.通过recv_size函数得到前面要合并的chunk的大小2.将自己的大小与前面的presize相加得到合并后的chunk的大小3.将p的指针向前移动,移动到前面那个chunk的开头处4.unlink是将两个要合并的chunk各自从原来的bin链上面断下来,然后进行合并
四、特殊情况:fast chunk的PREV_INUSE位
如果chunk的大小为32字节~128字节(0x20~0x80)大小之间,我们称该chunk位fast chunk
系统规定:
①无论fastchunk前面的chunk是否处于分配状态,PREV_INUSE始终为1。因为系统不希望fastchunk与前面的chunk之间进行合并②不论当前的fastchunk是否处于分配状态,该chunk后面的chunk的PREV_INUSE位一定为1,也是为了防止后面的chunk与fastchunk合并
为什么这么设计?
大多数程序经常会经常申请和释放一些较小的chunk,如果将一些较小的chunk快free之后并合并,下次malloc时申请一个比合并的chunk小的chunk时,此时有需要切割合并的chunk。程序如果把大量的时间放在合并和切割以及中间检查上面,这样就大大的降低了效率,因为ptmalloc专门设计了fastchunk,且fastchunk是不会合并的
五、演示案例(查看chunk的PREV_INUSE位)
#include <stdio.h>
#include <unistd.h>
#include <malloc.h>
int main()
{
int size=0x10;
void *p=malloc(size);
void *p2=malloc(size);
sleep(0); //只为了打断点使用,无其他用处
free(p);
free(p2);
return 0;
}
第一步:在sleep处打断点,并且运行起来
第二步:查看当前的所有堆块
看到第一个chunk(地址0x602020)的size为33(chunk大小32+PREV_INUSE位为1),因为它的前面的chunk是出于分配状态的
第二个chunk(地址0x602000)的size为33(chunk大小32+PREV_INUSE位为1),虽然其前面没有chunk了,但是PREV_INUSE位仍然为1,这也就验证了前面的说法(fast chunk的PREV_INUSE位永远为1)
六、演示案例(PREV_INUSE的合并操作)
因为fast chunk不会进行合并,因为其PREV_INUSE永远为1,下面我们申请更大的chunk来演示一下chunk的合并
#include <unistd.h>
#include <malloc.h>
int main()
{
int size=0x100;
void *p1=malloc(size);
void *p2=malloc(size);
void *p3=malloc(size); //为了达到演示效果,防止合并后的chunk与top chunk合并
sleep(0); //只为了打断点使用,无其他用处
free(p1);
free(p2);
return 0;
}
第一步:断点打到sleep上,并运行起来
第二步:先释放p1(可以看到p1被放入到unsortedbin中)
第三步:再释放p2(可以看到两个chunk被合并),合并后的大小为:273+272=545
程序更改:
现在我们在p1和p2之间再申请一块内存,来验证p1和p2不能合并的情景
第一步:断点打到sleep上,并运行起来
第二步:先释放p1
第三步:再释放p2
可以看到p1和p2没有被合并,因为其前面都有chunk正在使用(PREV_INUSE为1)