探索malloc和free

近期对malloc和free内部实现进行一个研究,知道malloc是开辟内存,free是释放申请的内存,但是对于里面具体的实现如何一直不知道。知其然,不知其所以然。记录一下探索的过程,很多都不完善,以后有新的发现在修改。

以下思考几个问题:

  • malloc是如何申请内存的?内部的结构如何?
  • free只是传递了一个指针而已,如何知道应该释放多大的内存?以及如何释放?
  • Linux的内存管理机制,虚拟内存和物理内存之间的关系如何?

一开始看了这篇文章讲的很好,对malloc,free以及Linux进程内存管理有个初步的概念,这些看起来很抽象的内存申请,其实也是用最简单的代码就可以写出来的,https://blog.codinglabs.org/articles/a-malloc-tutorial.html。以及一个Linux虚拟内存和物理内存之间的关系简单表达.看了这篇文章,我决定动手验证一下具体情况。

进程的内存布局:

其中下载了glibc2.23的源码来看实际的实现。

一、malloc

1.malloc实际的空间大小

由于受上面链接的影响,一直以为malloc_chunk实际开辟的内存模型就是那样子,所以尝试着在debug中,通过malloc返回的指针的虚拟内存地址去看实际的内存中的数据。

找到了在p = 0x00ad4930前面的第二个size_t的后面4个字节记录着我malloc的大小,所以这个block有记录着内存的大小,但是没有在windows下深入探索,主要是研究了Linux系统下的,所以接下来都是在64位Linux系统下实验。

于是我去搜索了很多malloc的实现,想知道实际上在Linux中malloc是如何实现的,网上很多人说malloc的header是以下的结构:

struct mem_control_block {
	int is_available; //是否空闲
	int size; //空间大小
};

所以我尝试着去打印出两块内存之间的数据,看一下是否是如此:

    printf("-----------------------------\n");
    void* ptr1 = malloc(20);
    memset(ptr1, 0x01, 20);
    printf("ptr1 = %#x\n", ptr1);
    printf("-----------------------------\n");

    void* ptr2 = malloc(112);
    memset(ptr2, 0x02, 112);
    printf("ptr2 = %#x\n", ptr2);
    printAddrData1Byte(ptr1, ptr2);
    printf("-----------------------------\n");

 

//打印从startAddr到endAddr的字节
void printAddrData1Byte(void* startAddr, void* endAddr)
{
    printf("printf startAddr = %#x to endAddr = %#x data\n", startAddr, endAddr);
    char* pMove = (char*)startAddr;
    int i = 0;
    while(pMove < endAddr)
    {
        printf("%#x\040", *pMove);
        pMove += 1;
        i++;
        if(!(i % 8))
            printf("\n");
    }
}

运行结果

-----------------------------
ptr1 = 0x2480420
-----------------------------
ptr2 = 0x2480440
printf startAddr = 0x2480420 to endAddr = 0x2480440 data
0x1 0x1 0x1 0x1 0x1 0x1 0x1 0x1 
0x1 0x1 0x1 0x1 0x1 0x1 0x1 0x1 
0x1 0x1 0x1 0x1 0 0 0 0 
0xffffff81 0 0 0 0 0 0 0 
-----------------------------

ptr1, ptr2均是malloc开辟后返回的用户可以开始使用的地址。ptr1 = 0x2480420, ptr2 = 0x2480440, 相差0x20 = 32个字节。

这里有一个问题,为什么是32字节?

很明显前面20个字节0x1是我申请的内存,那后面的12个字节是怎么来的?用来干嘛的?

后来发现了Linux下的一个函数,可以返回指针指向的空间大小。

size_t malloc_usable_size (void *ptr);

RETURN VALUE
malloc_usable_size() returns the number of usable bytes in the block of allocated memory pointed to by ptr.  If ptr is NULL, 0 is returned

修改代码如下:

    printf("-----------------------------\n");
    void* ptr1 = malloc(20);
    memset(ptr1, 0x01, 20);
    printf("ptr1 = %#x\n", ptr1);
    printf("ptr1 malloc_usable_size = %zu\n", malloc_usable_size(ptr1));
    printf("-----------------------------\n");

    void* ptr2 = malloc(112);
    memset(ptr2, 0x02, 112);
    printf("ptr2 = %#x\n", ptr2);
    printf("ptr2 malloc_usable_size = %zu\n", malloc_usable_size(ptr2));
    printAddrData1Byte(ptr1, ptr2);
    printf("-----------------------------\n");

运行结果:

-----------------------------
ptr1 = 0x18fb420
ptr1 malloc_usable_size = 24
-----------------------------
ptr2 = 0x18fb440
ptr2 malloc_usable_size = 120
printf startAddr = 0x18fb420 to endAddr = 0x18fb440 data
0x1 0x1 0x1 0x1 0x1 0x1 0x1 0x1 
0x1 0x1 0x1 0x1 0x1 0x1 0x1 0x1 
0x1 0x1 0x1 0x1 0 0 0 0 
0xffffff81 0 0 0 0 0 0 0 
-----------------------------

实际上ptr1指向的可用的内存大小是24个字节(内存对齐),那么后面的8个字节就是struct mem_control_block的数据了,但是第一个4字节是0xffffff81,实际上应该是0x81 = 129,第二个4字节是0,跟网上说的好像并不一致。

于是去下载glibc,源码在glibc-2.23/malloc/malloc.c,看源码是如何实现,无奈源码确实有点难看,好在找到了这个链接https://blog.csdn.net/conansonic/article/details/50121589,搭配着看,看了两遍才大概知道了这个模型。

2.malloc.c

在函数static void *  _int_malloc (mstate av, size_t bytes);主要对malloc的逻辑进行了处理。

以下是源码对malloc_block定义的结构:

struct malloc_chunk {

  INTERNAL_SIZE_T      prev_size;  /* Size of previous chunk (if free).  */
  INTERNAL_SIZE_T      size;       /* Size in bytes, including overhead. */

  struct malloc_chunk* fd;         /* double links -- used only if free. */
  struct malloc_chunk* bk;

  /* Only used for large blocks: pointer to next larger size.  */
  struct malloc_chunk* fd_nextsize; /* double links -- used only if free. */
  struct malloc_chunk* bk_nextsize;
};


/*
   malloc_chunk details:

    (The following includes lightly edited explanations by Colin Plumb.)

    Chunks of memory are maintained using a `boundary tag' method as
    described in e.g., Knuth or Standish.  (See the paper by Paul
    Wilson ftp://ftp.cs.utexas.edu/pub/garbage/allocsrv.ps for a
    survey of such techniques.)  Sizes of free chunks are stored both
    in the front of each chunk and at the end.  This makes
    consolidating fragmented chunks into bigger chunks very fast.  The
    size fields also hold bits representing whether chunks are free or
    in use.

    An allocated chunk looks like this:


    chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
	    |             Size of previous chunk, if allocated            | |
	    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
	    |             Size of chunk, in bytes                       |M|P|
      mem-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
	    |             User data starts here...                          .
	    .                                                               .
	    .             (malloc_usable_size() bytes)                      .
	    .                                                               |
nextchunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
	    |             Size of chunk                                     |
	    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+


    Where "chunk" is the front of the chunk for the purpose of most of
    the malloc code, but "mem" is the pointer that is returned to the
    user.  "Nextchunk" is the beginning of the next contiguous chunk.

    Chunks always begin on even word boundaries,(总是以偶数字长为边界,意味着以2 * size_t为对齐) 
    so the mem portion
    (which is returned to the user) is also on an even word boundary, and
    thus at least double-word aligned(double-word对齐).

    Free chunks are stored in circular doubly-linked lists, and look like this:

    chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
	    |             Size of previous chunk                            |
	    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    `head:' |             Size of chunk, in bytes                         |P|
      mem-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
	    |             Forward pointer to next chunk in list             |
	    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
	    |             Back pointer to previous chunk in list            |
	    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
	    |             Unused space (may be 0 bytes long)                .
	    .                                                               .
	    .                                                               |
nextchunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    `foot:' |             Size of chunk, in bytes                           |
	    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

    The P (PREV_INUSE) bit, stored in the unused low-order bit of the
    chunk size (which is always a multiple of two words), is an in-use
    bit for the *previous* chunk.  If that bit is *clear*, then the
    word before the current chunk size contains the previous chunk
    size, and can be used to find the front of the previous chunk.
    The very first chunk allocated always has this bit set,
    preventing access to non-existent (or non-owned) memory. If
    prev_inuse is set for any given chunk, then you CANNOT determine
    the size of the previous chunk, and might even get a memory
    addressing fault when trying to do so.

    Note that the `foot' of the current chunk is actually represented
    as the prev_size of the NEXT chunk. This makes it easier to
    deal with alignments etc but can be very confusing when trying
    to extend or adapt this code.

    The two exceptions to all this are

     1. The special chunk `top' doesn't bother using the
	trailing size field since there is no next contiguous chunk
	that would have to index off it. After initialization, `top'
	is forced to always exist.  If it would become less than
	MINSIZE bytes long, it is replenished.

     2. Chunks allocated via mmap, which have the second-lowest-order
	bit M (IS_MMAPPED) set in their size fields.  Because they are
	allocated one-by-one, each must contain its own trailing size field.

*/

上面链接有一段解释说:“当一个chunk为空闲时,至少要有prev_size、size、fd和bk四个参数,因此MINSIZE就代表了这四个参数需要占用的内存大小;而当一个chunk被使用时,prev_size可能会被前一个chunk用来存储,fd和bk也会被当作内存存储数据,因此当chunk被使用时,只剩下了size参数需要设置,request2size中的SIZE_SZ就是INTERNAL_SIZE_T类型的大小,因此至少需要req+SIZE_SZ的内存大小。MALLOC_ALIGN_MASK用来对齐,因此request2size就计算出了所需的chunk的大小”

  • “当chunk被使用时,只剩下了size参数需要设置”,我们打印出来的数据验证了这句话,block只用设置INTERNAL_SIZE_T      size;因为INTERNAL_SIZE_T在源码中的定义是size_t,所以就是8个字节都是size的区域,但是比两个地址差大了1,是这么回事?因为size字段的最后的4位(在32系统是3位)是被用来做标志位的,所以size字段能表示的大小就是16或者8的倍数,故呈现出malloc内存对齐(32位2^3=8字节对齐,64位2^4=16字节对齐),会调用request2size进行对齐。

说明: 
  1、chunk指针指向chunk开始的地址;mem指针指向用户内存块开始的地址。 
  2、p=0时,表示前一个chunk为空闲,prev_size才有效 
  3、p=1时,表示前一个chunk正在使用,prev_size无效 p主要用于内存块的合并操作;ptmalloc 分配的第一个块总是将p设为1, 以防止程序引用到不存在的区域 
  4、M=1 为mmap映射区域分配;M=0为heap区域分配 
  5、A=0 为主分配区分配;A=1 为非主分配区分配

其实可变相看成,一个chunk有头部和尾部,的头部和尾部都是保存size of chunk,当尾部划分到下一个chunk的区域时,则变成了prev_size。chunk在被使用时,除了size外,其他的字段都被用来存储数据,是为了提高chunk的有效荷载。在《深入理解计算机系统》中,也提到了头部和尾部保存当前块的大小,已分配的块中不再需要脚部,只有当前面块是空闲时,才会需要用到它的的脚部。

request2size源码:

#  define MALLOC_ALIGNMENT       (2 *SIZE_SZ < __alignof__ (long double)      \
                                   ? __alignof__ (long double) : 2 *SIZE_SZ)
# else
#  define MALLOC_ALIGNMENT       (2 *SIZE_SZ)
# endif
 #endif
#define MALLOC_ALIGN_MASK      (MALLOC_ALIGNMENT - 1)

#define request2size(req)                                         \
   (((req) + SIZE_SZ + MALLOC_ALIGN_MASK < MINSIZE)  ?             \
    MINSIZE :                                                      \
    ((req) + SIZE_SZ + MALLOC_ALIGN_MASK) & ~MALLOC_ALIGN_MASK)
  • “当一个chunk为空闲时,至少要有prev_size、size、fd和bk四个参数,因此MINSIZE就代表了这四个参数需要占用的内存大小”,MIN_CHUNK_SIZE就是malloc生成时最小的空间。所以即使是malloc(0)时,也会有4*size_t = 32字节,除掉size的大小,用户可使用的是24字节。在chunk空闲的时候,prev_size、fd和bk这三个参数才会发挥作用。

说明: 
  1、当chunk空闲时,其M状态是不存在的,只有AP状态, 
  2、原本是用户数据区的地方存储了四个指针, 
    指针fd指向后一个空闲的chunk,而bk指向前一个空闲的chunk,malloc通过这两个指针将大小相近的chunk连成一个双向链表。 
    在large bin中的空闲chunk,还有两个指针,fd_nextsize和bk_nextsize,用于加快在large bin中查找最近匹配的空闲chunk。不同的chunk链表又是通过bins或者fastbins来组织的。

/* The smallest possible chunk */
#define MIN_CHUNK_SIZE        (offsetof(struct malloc_chunk, fd_nextsize))
    printf("-----------------------------\n");
    void* ptr1 = malloc(20);
    memset(ptr1, 0x01, 20);
    printf("ptr1 = %#x\n", ptr1);
    printf("ptr1 malloc_usable_size = %zu\n", malloc_usable_size(ptr1));
    cur_chunk_data(ptr1);
    printf("-----------------------------\n");

    void* ptr2 = malloc(112);
    memset(ptr2, 0x02, 112);
    printf("ptr2 = %#x\n", ptr2);
    printf("ptr2 malloc_usable_size = %zu\n", malloc_usable_size(ptr2));
    cur_chunk_data(ptr2);
    printAddrData1Byte(ptr1, ptr2);
    printf("-----------------------------\n");

 运行结果:

-----------------------------
ptr1 = 0x19a5420
ptr1 malloc_usable_size = 24
mem = 0x19a5420, pchunk = 0x19a5410,size before chunksize = 33, size = 32,, pre_size = 0
-----------------------------
ptr2 = 0x19a5440
ptr2 malloc_usable_size = 120
mem = 0x19a5440, pchunk = 0x19a5430,size before chunksize = 129, size = 128,, pre_size = 16843009
printf startAddr = 0x19a5420 to endAddr = 0x19a5440 data
0x1 0x1 0x1 0x1 0x1 0x1 0x1 0x1 
0x1 0x1 0x1 0x1 0x1 0x1 0x1 0x1 
0x1 0x1 0x1 0x1 0 0 0 0 
0xffffff81 0 0 0 0 0 0 0 
-----------------------------

 void cur_chunk_data(void* mem)函数:

typedef struct malloc_block* mchunkptr;

#define PREV_INUSE 0x1
#define IS_MMAPPED 0x2
#define NON_MAIN_ARENA 0x4

#define SIZE_BIT (PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)

#define chunksize(p) ((p)->size & ~(SIZE_BIT))
#define next_chunk(p) ((mchunkptr)((char*)(p) + ((p)->size & SIZE_BIT)))
#define pre_chunk(p) ((mchunkptr)((char*)(p) - ((p)->pre_size)))
#define mem2chunk(mem) ((mchunkptr)((char*)mem - 2 * sizeof(SIZE_SZ)))

#define MIN_CHUNK_SIZE        (offsetof(struct malloc_chunk, fd_nextsize))

#define MALLOC_ALIGNMENT       (2 *SIZE_SZ)
#define MALLOC_ALIGN_MASK      (MALLOC_ALIGNMENT - 1)

#define MINSIZE  \
    (unsigned long)(((MIN_CHUNK_SIZE+MALLOC_ALIGN_MASK) & ~MALLOC_ALIGN_MASK))

#define request2size(req)                                  \
    (((req) + SIZE_SZ + MALLOC_ALIGN_MASK < MINSIZE)  ?    \        
    MINSIZE :                                              \       
    ((req) + SIZE_SZ + MALLOC_ALIGN_MASK) & ~MALLOC_ALIGN_MASK)

void cur_chunk_data(void* mem)
{
    mchunkptr pchunk = mem2chunk(mem);
    printf("mem = %#x, pchunk = %#x,size before chunksize = %d, size = %zu,, pre_size = %d\n", mem, pchunk, pchunk->size, chunksize(pchunk), pchunk->pre_size);
}

在计算size的时候,需要对size字段中的低位字节进行操作,调用chunksize宏,得出真实的size,如上的0x81得出的size = 0x80。

3、brk和sbrk

#include <unistd.h>
int brk( const void *addr )
void* sbrk ( intptr_t incr );

两者的作用是扩展heap的上界brk 
brk()的参数设置为新的brk上界地址,成功返回1,失败返回0; 
sbrk()的参数为申请内存的大小,返回heap新的上界brk的地址。

如果调用sbrk(0),则可以返回当前break指针的指向的堆顶。

从程序开始没有使用malloc之前打印sbrk(0)和使用malloc之后打印sbrk(0),可以看出之间的地址相差0x21000 = 33 * 4KB,则malloc申请内存时,系统会一次性映射33个内存页。

4、mmap

当申请的内存大于>=mmap_threshold使用mmap函数。最小的threshold = 128KB.

The maximum overhead wastage (i.e., number of extra bytes
allocated than were requested in malloc) is less than or equal
to the minimum size, except for requests >= mmap_threshold that
are serviced via mmap(), where the worst case wastage is 2 *
sizeof(size_t) bytes plus the remainder from a system page (the
minimal mmap unit); typically 4096 or 8192 bytes.

/*
MMAP_THRESHOLD_MAX and _MIN are the bounds on the dynamically
adjusted MMAP_THRESHOLD.
*/
 
#ifndef DEFAULT_MMAP_THRESHOLD_MIN
#define DEFAULT_MMAP_THRESHOLD_MIN (128 * 1024)
#endif

#ifndef DEFAULT_MMAP_THRESHOLD_MAX
/* For 32-bit platforms we cannot increase the maximum mmap
   threshold much because it is also the minimum value for the
   maximum heap size and its alignment.  Going above 512k (i.e., 1M
   for new heaps) wastes too much address space.  */
# if __WORDSIZE == 32
#  define DEFAULT_MMAP_THRESHOLD_MAX (512 * 1024)
# else
#  define DEFAULT_MMAP_THRESHOLD_MAX (4 * 1024 * 1024 * sizeof(long))
# endif
#endif

二、free

extern void free (void *__ptr) __THROW;

1、根据形参__ptr进行计算(已知malloc_chunk进行计算),得出size

2、进行大小判断,选择哪个空闲链表进行管理,并且设置malloc_chunk的head和foot字段。

从源码的static void  _int_free (mstate av, mchunkptr p, int have_lock)中可以看到详细的思路。

使用空闲链表bins对空闲的chunk进行管理。具体可以参考https://blog.csdn.net/z_ryan/article/details/79950737

从源码中(malloc.c中的int_free函数)看到了一个我比较在意的问题,就是head和foot的设置

/* Set size/use field */
#define set_head(p, s)       ((p)->size = (s))

/* Set size at footer (only when chunk is not in use) */
#define set_foot(p, s)       (((mchunkptr) ((char *) (p) + (s)))->prev_size = (s))

所以在set_foot时,直接跨越到下一个chunk的prev_size字段进行设置。

 

三、总结

在研究malloc和free的整个过程中,学习了以下几点:

1、在Linux下C程序的整个内存结构是怎样的,之前虽然有看过,但是觉得有点抽象,没有跟平时的程序建立起模型,现在懂了

2、虚拟内存和物理内存之间的关系

3、glibc源码的接触,就像《STL源码剖析》首页说的:源码之前,了无秘密

4、malloc和free的大概了解,但是里面还有很多细节和机制没有完全搞懂

5、malloc还有其他的实现:ptmalloc(glibc)  、tcmalloc(google) 、jemalloc(facebook)

6、要多写博客,不然很容易就会忘掉

### 回答1: malloclab是北大(北京大学)计算机科学与技术系的一个实验室。该实验室专注于malloc库的设计和实现。 malloc是一种用于动态分配内存的函数,通常用于在程序运行时动态地分配一块内存空间。在C语言中,malloc函数可以根据需要动态分配特定大小的内存区域,并返回指向该区域的指针。然而,malloc在设计时需要考虑如何高效地分配和管理内存,以及处理内存的分配和释放过程中可能发生的问题。 malloclab旨在通过设计和实现自己的malloc库来加深对内存管理的理解和运用。实验室的任务之一是实现一个高效的malloc库,它可以在动态分配内存时尽可能地减少内存碎片和提高分配速度。该实验室的成员将通过学习和实践,了解malloc库的内部工作原理,深入了解内存分配算法和数据结构,以提高对内存管理的掌握能力。 另外,malloclab也是一个学习的平台,为计算机科学与技术系的学生提供了实践和探索的机会。学生们可以在实验室中参与开发和优化malloc库的过程,培养对底层系统软件开发的兴趣和能力。他们将学习如何分析和改进现有的内存分配算法,并可以尝试设计新的分配策略以满足特定的需求。 总之,malloclab是北大计算机科学与技术系的一个实验室,致力于malloc库的设计和实现。通过参与实验室的活动,学生们可以深入了解内存管理的原理和技术,并培养相关的开发能力和兴趣。 ### 回答2: malloclab pku是北京大学计算机科学与技术学院开设的一个编程实验课程,旨在教授学生如何实现一个简单的内存分配器。 这个实验的目标是让学生加深对内存分配和动态存储管理的理解。在这个实验中,学生需要使用C语言编写代码,实现mallocfree这两个常用的内存分配和释放函数。 实验的具体要求包括实现一个简单的存储分配器,它可以根据不同的内存需求分配相应大小的连续内存块,并在不再需要时将其释放。实现过程中还需要考虑内存块的对齐、最小内存块大小等问题。 实验提供了一些基本的代码框架和测试样例,学生需要在此基础上进行代码编写和调试。同时,学生还需要编写实验报告,详细描述自己的实现思路和测试结果。 这个实验对于学生来说是一次很好的编程练习,可以帮助他们更深入地理解内存分配和释放的原理与机制。此外,这个实验也锻炼了学生的编码能力和对细节的注意力。 总的来说,malloclab pku是一门有挑战性的实验课程,通过完成这个实验,学生能够掌握内存分配和释放的基本原理,并且提高自己的编程能力。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值