分主要对源代码实现技巧的细节做分析,希望能进一步理解 ptmalloc 的实现,做到终极无惑。主要分析的文件包括 arena.c 和 malloc.c ,这两个文件包括了 ptmalloc 的核心实现,其中 arena.c 主要是对多线程支持的实现, malloc.c 定义了公用的 malloc() , free() 等函数,实现了基于分配区的内存管理算法。本部分不会从头到尾分析 arena.c 和 malloc.c 整个文件,而是根据 ptmalloc 的实现原理,分成几个模块分别介绍,主要分析了 malloc() 和 free() 函数的实现,对其它的函数如 realloc() , calloc() 等不作介绍。由于 ptmalloc 同时支持 32 位平台和 64 位平台,所以这里的分析尽量兼顾到这两类平台,但主要基于 Linux X86 平台。
Ptmalloc 使用 chunk 实现内存管理,对 chunk 的管理基于独特的边界标记法,第三节已经对 chunk 的管理做了概述,这里将详细分析 chunk 管理的源代码实现。
在不同的平台下,每个 chunk 的最小大小,地址对齐方式是不同的, ptmalloc 依赖平台定义的 size_t 长度,对于 32 位平台, size_t 长度为 4 字节,对 64 位平台, size_t 长度可能为 4 字节,也可能为 8 字节,在 Linux X86_64 上 size_t 为 8 字节,这里就以 size_t 为 4 字节和 8 字节的情况进行分析。先看一段源代码:
#ifndef INTERNAL_SIZE_T #define INTERNAL_SIZE_T size_t #endif /* The corresponding word size */ #define SIZE_SZ (sizeof(INTERNAL_SIZE_T)) /* MALLOC_ALIGNMENT is the minimum alignment for malloc'ed chunks. It must be a power of two at least 2 * SIZE_SZ, even on machines for which smaller alignments would suffice. It may be defined as larger than this though. Note however that code and data structures are optimized for the case of 8-byte alignment. */ #ifndef MALLOC_ALIGNMENT #define MALLOC_ALIGNMENT (2 * SIZE_SZ) #endif /* The corresponding bit mask value */ #define MALLOC_ALIGN_MASK (MALLOC_ALIGNMENT - 1)
Ptmalloc 使用宏来屏蔽不同平台的差异,将 INTERNAL_SIZE_T 定义为size_t ,SIZE_SZ 定义为size_t 的大小,在32 位平台下位4 字节,在64 位平台下位4 字节或者8 字节。另外分配chunk 时必须以2*SIZE_SZ 对齐,MALLOC_ALIGNMENT 和MALLOC_ALIGN_MASK 是用来处理chunk 地址对齐的宏,将在后面的源代码介绍中经常看到。这里只需要知道在32 平台chunk 地址按8 字节对齐,64 位平台按8 字节或是16 字节对齐就可以了。
Ptmalloc 采用边界标记法将内存划分成很多块,从而对内存的分配与回收进行管理。在 ptmalloc 的实现源码中定义结构体 malloc_chunk 来描述这些块,并使用宏封装了对 chunk 中每个域的读取,修改,校验,遍历等等。 malloc_chunk 定义如下:
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; };
chunk 的定义相当简单明了,对各个域做一下简单介绍 :
prev_size: 如果前一个 chunk 是空闲的,该域表示前一个 chunk 的大小,如果前一个 chunk 不空闲,该域无意义。
size :当前 chunk 的大小,并且记录了当前 chunk 和前一个 chunk 的一些属性,包括前一个 chunk 是否在使用中,当前 chunk 是否是通过 mmap 获得的内存,当前 chunk 是否属于非主分配区。
fd 和 bk : 指针 fd 和 bk 只有当该 chunk 块空闲时才存在,其作用是用于将对应的空闲 chunk 块加入到空闲 chunk 块链表中统一管理,如果该 chunk 块被分配给应用程序使用,那么这两个指针也就没有用(该 chunk 块已经从空闲链中拆出)了,所以也当作应用程序的使用空间,而不至于浪费。
fd_nextsize 和 bk_nextsize: 当当前的 chunk 存在于 large bins 中时, large bins 中的空闲 chunk 是按照大小排序的,但同一个大小的 chunk 可能有多个,增加了这两个字段可以加快遍历空闲 chunk ,并查找满足需要的空闲 chunk , fd_nextsize 指向下一个比当前 chunk 大小大的第一个空闲 chunk , bk_nextszie 指向前一个比当前 chunk 大小小的第一个空闲 chunk 。 如果该 chunk 块被分配给应用程序使用,那么这两个指针也就没有用(该 chunk 块已经从 size 链中拆出)了,所以也当作应用程序的使用空间,而不至于浪费。
/* 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 boundries, so the mem portion (which is returned to the user) is also on an even word boundary, and thus at least double-word aligned. 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 的细节,已分配的 chunk 和空闲的 chunk 形式不一样,充分利用空间复用,设计相当的巧妙。在前面的 3.2.3.2 节描述了这两种 chunk 形式,请参考前文的描述。
/* conversion from malloc headers to user pointers, and back */ #define chunk2mem(p) ((Void_t*)((char*)(p) + 2*SIZE_SZ)) #define mem2chunk(mem) ((mchunkptr)((char*)(mem) - 2*SIZE_SZ)) /* The smallest possible chunk */ #define MIN_CHUNK_SIZE (offsetof(struct malloc_chunk, fd_nextsize)) /* The smallest size we can malloc is an aligned minimal chunk */ #define MINSIZE \ (unsigned long)(((MIN_CHUNK_SIZE+MALLOC_ALIGN_MASK) & ~MALLOC_ALIGN_MASK)) /* Check if m has acceptable alignment */ #define aligned_OK(m) (((unsigned long)(m) & MALLOC_ALIGN_MASK) == 0) #define misaligned_chunk(p) \ ((uintptr_t)(MALLOC_ALIGNMENT == 2 * SIZE_SZ ? (p) : chunk2mem (p)) \ & MALLOC_ALIGN_MASK)
对于已经分配的 chunk ,通过 chunk2mem 宏根据 chunk 地址获得返回给用户的内存地址,反过来通过 mem2chunk 宏根据 mem 地址得到 chunk 地址, chunk 的地址是按 2*SIZE_SZ 对齐的,而 chunk 结构体的前两个域刚好也是 2*SIZE_SZ 大小,所以, mem 地址也是 2*SIZE_SZ 对齐的。宏 aligned_OK 和 misaligned_chunk (p) 用于校验地址是否是按 2*SIZE_SZ 对齐的。
MIN_CHUNK_SIZE 定义了最小的 chunk 的大小, 32 位平台上位 16 字节, 64 位平台为 24 字节或是 32 字节。 MINSIZE 定义了最小的分配的内存大小,是对 MIN_CHUNK_SIZE 进行了 2*SIZE_SZ 对齐,地址对齐后与 MIN_CHUNK_SIZE 的大小仍然是一样的。
/* Check if a request is so large that it would wrap around zero when padded and aligned. To simplify some other code, the bound is made low enough so that adding MINSIZE will also not wrap around zero. */ #define REQUEST_OUT_OF_RANGE(req) \ ((unsigned long)(req) >= \ (unsigned long)(INTERNAL_SIZE_T)(-2 * MINSIZE)) /* pad request bytes into a usable size -- internal version */ #define request2size(req) \ (((req) + SIZE_SZ + MALLOC_ALIGN_MASK < MINSIZE) ? \ MINSIZE : \ ((req) + SIZE_SZ + MALLOC_ALIGN_MASK) & ~MALLOC_ALIGN_MASK) /* Same, except also perform argument check */ #define checked_request2size(req, sz) \ if (REQUEST_OUT_OF_RANGE(req)) { \ MALLOC_FAILURE_ACTION; \ return 0; \ } \ (sz) = request2size(req);
这几个宏用于将用户请求的分配大小转换成内部需要分配的 chunk 大小,这里需要注意的在转换时不但考虑的地址对齐,还额外加上了 SIZE_SZ ,这意味着 ptmalloc 分配内存需要一个额外的 overhead ,为 SIZE_SZ 字节,通过 chunk 的空间复用,我们很容易得出这个 overhead 为 SIZE_SZ 。
以 Linux X86_64 平台为例,假设 SIZE_SZ 为 8 字节,空闲时,一个 chunk 中至少要 4 个 size_t ( 8B )大小的空间,用来存储 prev_size , size , fd 和 bk ,也就是 MINSIZE ( 32B ), chunk 的大小要对齐到 2*SIZE_SZ ( 16B )。当一个 chunk 处于使用状态时,它的下一个 chunk 的 prev_size 域肯定是无效的。所以实际上,这个空间也可以被当前 chunk 使用。这听起来有点不可思议,但确实是合理空间复用的例子。故而实际上,一个使用中的 chunk 的大小的计算公式应该是: in_use_size = ( 用户请求大小 + 16 - 8 ) align to 8B ,这里加 16 是因为需要存储 prev_size 和 size ,但又因为向下一个 chunk “借”了 8B ,所以要减去 8 ,每分配一个 chunk 的 overhead 为 8B ,即 SIZE_SZ 的大小 。 最后,因为空闲的 chunk 和使用中的 chunk 使用的是同一块空间。所以肯定要取其中最大者作为实际的分配空间。即最终的分配空间 chunk_size = max(in_use_size, 32) 。这就是当用户请求内存分配时, ptmalloc 实际需要分配的内存大小。
注意:如果 chunk 是由 mmap () 直接分配的,则该 chunk 不会有前一个 chunk 和后一个 chunk ,所有本 chunk 没有下一个 chunk 的 prev_size 的空间可以“借”,所以对于直接 mmap() 分配内存的 overhead 为 2*SIZE_SZ 。
/* size field is or'ed with PREV_INUSE when previous adjacent chunk in use */ #define PREV_INUSE 0x1 /* extract inuse bit of previous chunk */ #define prev_inuse(p) ((p)->size & PREV_INUSE) /* size field is or'ed with IS_MMAPPED if the chunk was obtained with mmap() */ #define IS_MMAPPED 0x2 /* check for mmap()'ed chunk */ #define chunk_is_mmapped(p) ((p)->size & IS_MMAPPED) /* size field is or'ed with NON_MAIN_ARENA if the chunk was obtained from a non-main arena. This is only set immediately before handing the chunk to the user, if necessary. */ #define NON_MAIN_ARENA 0x4 /* check for chunk from non-main arena */ #define chunk_non_main_arena(p) ((p)->size & NON_MAIN_ARENA)
chunk 在分割时总是以地址对齐(默认是 8 字节,可以自由设置,但是 8 字节是最小值并且设置的值必须是 2 为底的幂函数值,即是 alignment = 2^n , n 为整数且 n>=3 )的方式来进行的,所以用 chunk->size 来存储本 chunk 块大小字节数的话,其末 3bit 位总是 0 ,因此这三位可以用来存储其它信息,比如:
以第 0 位作为 P 状态位,标记前一 chunk 块是否在使用中,为 1 表示使用,为 0 表示空闲。
以第 1 位作为 M 状态位,标记本 chunk 块是否是使用 mmap() 直接从进程的 mmap 映射区域分配的,为 1 表示是,为 0 表示否。
以第 2 位作为 A 状态位,标记本 chunk 是否属于非主分配区,为 1 表示是,为 0 表示否。
/* Bits to mask off when extracting size Note: IS_MMAPPED is intentionally not masked off from size field in macros for which mmapped chunks should never be seen. This should cause helpful core dumps to occur if it is tried by accident by people extending or adapting this malloc. */ #define SIZE_BITS (PREV_INUSE|IS_MMAPPED|NON_MAIN_ARENA) /* Get size, ignoring use bits */ #define chunksize(p) ((p)->size & ~(SIZE_BITS)) /* Ptr to next physical malloc_chunk. */ #define next_chunk(p) ((mchunkptr)( ((char*)(p)) + ((p)->size & ~SIZE_BITS) )) /* Ptr to previous physical malloc_chunk */ #define prev_chunk(p) ((mchunkptr)( ((char*)(p)) - ((p)->prev_size) )) /* Treat space at ptr + offset as a chunk */ #define chunk_at_offset(p, s) ((mchunkptr)(((char*)(p)) + (s)))
prev_size 字段虽然在当前 chunk 块结构体内,记录的却是前一个邻接 chunk 块的信息,这样做的好处就是我们通过本块 chunk 结构体就可以直接获取到前一 chunk 块的信息,从而方便做进一步的处理操作。相对的,当前 chunk 块的 foot 信息就存在于下一个邻接 chunk 块的结构体内。字段 prev_size 记录的什么信息呢?有两种情况:
1 ) 如果前一个邻接 chunk 块空闲,那么当前 chunk 块结构体内的 prev_size 字段记录的是前一个邻接 chunk 块的大小。这就是由当前 chunk 指针获得前一个空闲 chunk 地址的依据。宏 prev_chunk(p) 就是依赖这个假设实现的。
2 )如果前一个邻接 chunk 在使用中,则当前 chunk 的 prev_size 的空间被前一个 chunk 借用中,其中的值是前一个 chunk 的内存内容,对当前 chunk 没有任何意义。
字段 size 记录了本 chunk 的大小,无论下一个 chunk 是空闲状态或是被使用状态,都可以通过本 chunk 的地址加上本 chunk 的大小,得到下一个 chunk 的地址,由于 size 的低 3 个 bit 记录了控制信息,需要屏蔽掉这些控制信息,取出实际的 size 在进行计算下一个 chunk 地址,这是 next_chunk (p) 的实现原理。
宏 chunksize (p) 用于获得 chunk 的实际大小,需要屏蔽掉 size 中的控制信息。
宏 chunk_at_offset (p, s) 将 p+s 的地址强制看作一个 chunk 。
注意:按照边界标记法,可以有多个连续的并且正在被使用中的 chunk 块,但是不会有多个连续的空闲 chunk 块,因为连续的多个空闲 chunk 块一定会合并成一个大的空闲 chunk 块。
/* extract p's inuse bit */ #define inuse(p)\ ((((mchunkptr)(((char*)(p))+((p)->size & ~SIZE_BITS)))->size) & PREV_INUSE) /* set/clear chunk as being inuse without otherwise disturbing */ #define set_inuse(p)\ ((mchunkptr)(((char*)(p)) + ((p)->size & ~SIZE_BITS)))->size |= PREV_INUSE #define clear_inuse(p)\ ((mchunkptr)(((char*)(p)) + ((p)->size & ~SIZE_BITS)))->size &= ~(PREV_INUSE)
上面的这一组宏用于 check/set/clear 当前 chunk 使用标志位,有当前 chunk 的使用标志位存储在下一个 chunk 的 size 的第 0 bit ( P 状态位),所以首先要获得下一个 chunk 的地址,然后 check/set/clear 下一个 chunk 的 size 域的第 0 bit 。
/* check/set/clear inuse bits in known places */ #define inuse_bit_at_offset(p, s)\ (((mchunkptr)(((char*)(p)) + (s)))->size & PREV_INUSE) #define set_inuse_bit_at_offset(p, s)\ (((mchunkptr)(((char*)(p)) + (s)))->size |= PREV_INUSE) #define clear_inuse_bit_at_offset(p, s)\ (((mchunkptr)(((char*)(p)) + (s)))->size &= ~(PREV_INUSE)) 上面的三个宏用于check/set/clear指定chunk的size域中的使用标志位。 /* Set size at head, without disturbing its use bit */ #define set_head_size(p, s) ((p)->size = (((p)->size & SIZE_BITS) | (s))) /* 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_head_size(p, s) 用于设置当前 chunk p 的 size 域并保留 size 域的控制信息。宏 set_head(p, s) 用于设置当前 chunk p 的 size 域并忽略已有的 size 域控制信息。宏 set_foot(p, s) 用于设置当前 chunk p 的下一个 chunk 的 prev_size 为 s , s 为当前 chunk 的 size ,只有当 chunk p 为空闲时才能使用这个宏,当前 chunk 的 foot 的内存空间存在于下一个 chunk ,即下一个 chunk 的 prev_size 。