5.6.7 grow_heap,shrink_heap,delete_heap,heap_trim
这几个函数实现 sub_heap 和增长和收缩, grow_heap() 函数主要将 sub_heap 中可读可写区域扩大; shrink_heap() 函数缩小 sub_heap 的虚拟内存区域,减小该 sub_heap 的虚拟内存占用量; delete_heap() 为一个宏,如果 sub_heap 中所有的内存都空闲,使用该宏函数将 sub_heap 的虚拟内存还回给操作系统; heap_trim() 函数根据 sub_heap 的 top chunk 大小调用 shrink_heap() 函数收缩 sub_heap 。
函数的实现代码如下:
static int #if __STD_C grow_heap(heap_info *h, long diff) #else grow_heap(h, diff) heap_info *h; long diff; #endif { size_t page_mask = malloc_getpagesize - 1; long new_size; diff = (diff + page_mask) & ~page_mask; new_size = (long)h->size + diff; if((unsigned long) new_size > (unsigned long) HEAP_MAX_SIZE) return -1; if((unsigned long) new_size > h->mprotect_size) { if (mprotect((char *)h + h->mprotect_size, (unsigned long) new_size - h->mprotect_size, PROT_READ|PROT_WRITE) != 0) return -2; h->mprotect_size = new_size; } h->size = new_size; return 0; }
Grow_heap() 函数的实现比较简单,首先将要增加的可读可写的内存大小按照页对齐,然后计算 sub_heap 总的可读可写的内存大小 new_size ,判断 new_size 是否大于 HEAP_MAX_SIZE ,如果是,返回,否则判断 new_size 是否大于当前 sub_heap 的可读可写区域大小,如果否,调用 mprotect() 设置新增的区域可读可写,并更新当前 sub_heap 的可读可写区域的大小为 new_size 。最后将当前 sub_heap 的字段 size 更新为 new_size 。
Shrink_heap() 函数的实现源代码如下:
static int #if __STD_C shrink_heap(heap_info *h, long diff) #else shrink_heap(h, diff) heap_info *h; long diff; #endif { long new_size; new_size = (long)h->size - diff; if(new_size < (long)sizeof(*h)) return -1; /* Try to re-map the extra heap space freshly to save memory, and make it inaccessible. */ #ifdef _LIBC if (__builtin_expect (__libc_enable_secure, 0)) #else if (1) #endif { if((char *)MMAP((char *)h + new_size, diff, PROT_NONE, MAP_PRIVATE|MAP_FIXED) == (char *) MAP_FAILED) return -2; h->mprotect_size = new_size; } #ifdef _LIBC else madvise ((char *)h + new_size, diff, MADV_DONTNEED); #endif /*fprintf(stderr, "shrink %p %08lx\n", h, new_size);*/ h->size = new_size; return 0; }
Shrink_heap() 函数的参数 diff 已经页对齐,同时 sub_heap 的 size 也是安装页对齐的,所以计算 sub_heap 的 new_size 时不用再处理页对齐。如果 new_size 比 sub_heap 的首地址还小,报错退出,如果该函数运行在非 Glibc 中,则从 sub_heap 中切割出 diff 大小的虚拟内存,创建一个新的不可读写的映射区域,注意 mmap() 函数这里使用了 MAP_FIXED 标志,然后更新 sub_heap 的可读可写内存大小。如果该函数运行在 Glibc 库中,则调用 madvise() 函数,实际上 madvise() 函数什么也不做,只是返回错误,这里并没有处理 madvise() 函数的返回值。
#define delete_heap(heap) \ do { \ if ((char *)(heap) + HEAP_MAX_SIZE == aligned_heap_area) \ aligned_heap_area = NULL; \ munmap((char*)(heap), HEAP_MAX_SIZE); \ } while (0)
Delete_heap() 宏函数首先判断当前删除的 sub_heap 的结束地址是否与全局变量 aligned_heap_area 指向的地址相同,如果相同,则将全局变量 aligned_heap_area 设置为 NULL ,因为当前 sub_heap 删除以后,就可以从当前 sub_heap 的起始地址或是更低的地址开始映射新的 sub_heap ,这样可以尽量从地地址映射内存。然后调用 munmap() 函数将整个 sub_heap 的虚拟内存区域释放掉。在调用 munmap() 函数时, heap_trim() 函数调用 shrink_heap() 函数可能已将 sub_heap 切分成多个子区域, munmap() 函数的第二个参数为 HEAP_MAX_SIZE ,无论该 sub_heap (大小为 HEAP_MAX_SIZE )的内存区域被切分成多少个子区域,将整个 sub_heap 都释放掉了。
Heap_trim() 函数的源代码如下:
static int internal_function #if __STD_C heap_trim(heap_info *heap, size_t pad) #else heap_trim(heap, pad) heap_info *heap; size_t pad; #endif { mstate ar_ptr = heap->ar_ptr; unsigned long pagesz = mp_.pagesize; mchunkptr top_chunk = top(ar_ptr), p, bck, fwd; heap_info *prev_heap; long new_size, top_size, extra; /* Can this heap go away completely? */ while(top_chunk == chunk_at_offset(heap, sizeof(*heap))) {
每个非主分配区至少有一个 sub_heap ,每个非主分配区的第一个 sub_heap 中包含了一个 heap_info 的实例和 malloc_state 的实例,分主分配区中的其它 sub_heap 中只有一个 heap_info 实例,紧跟 heap_info 实例后,为可以用于分配的内存块。当当前非主分配区的 top chunk 与当前 sub_heap 的 heap_info 实例的结束地址相同时,意味着当前 sub_heap 中只有一个空闲 chunk ,没有已分配的 chunk 。所以可以将当前整个 sub_heap 都释放掉。
prev_heap = heap->prev; p = chunk_at_offset(prev_heap, prev_heap->size - (MINSIZE-2*SIZE_SZ)); assert(p->size == (0|PREV_INUSE)); /* must be fencepost */ p = prev_chunk(p); new_size = chunksize(p) + (MINSIZE-2*SIZE_SZ); assert(new_size>0 && new_size<(long)(2*MINSIZE)); if(!prev_inuse(p)) new_size += p->prev_size; assert(new_size>0 && new_size<HEAP_MAX_SIZE); if(new_size + (HEAP_MAX_SIZE - prev_heap->size) < pad + MINSIZE + pagesz) break;
每个sub_heap 的可读可写区域的末尾都有两个 chunk 用于 fencepost ,以 64 位系统为例,最后一个 chunk 占用的空间为 MINSIZE-2 *SIZE_SZ ,为 16B ,最后一个 chuk 的 size 字段记录的前一个 chunk 为 inuse 状态,并标识当前 chunk 大小为 0 ,倒数第二个 chunk 为 inuse 状态,这个 chunk 也是 fencepost 的一部分,这个 chunk 的大小为 2 *SIZE_SZ ,为 16B ,所以用于 fencepost 的两个 chunk 的空间大小为 32B 。 fencepost 也有可能大于 32B ,第二个 chunk 仍然为 16B ,第一个 chunk 的大小大于 16B ,这种情况发生在 top chunk 的空间小于 2*MINSIZE ,大于 MINSIZE ,但对于一个完全空闲的 sub_heap 来说, top chunk 的空间肯定大于 2*MINSIZE ,所以在这里不考虑这种情况。用于 fencepost 的 chunk 空间其实都是被分配给应用层使用的, new_size 表示当前 sub_heap 中可读可写区域的可用空间,如果倒数第二个 chunk 的前一个 chunk 为空闲状态,当前 sub_heap 中可读可写区域的可用空间大小还需要加上这个空闲 chunk 的大小。如果 new_size 与 sub_heap 中剩余的不可读写的区域大小之和小于 32+4K ( 64 位系统),意味着前一个 sub_heap 的可用空间太少了,不能释放当前的 sub_heap 。
ar_ptr->system_mem -= heap->size; arena_mem -= heap->size; delete_heap(heap); heap = prev_heap; if(!prev_inuse(p)) { /* consolidate backward */ p = prev_chunk(p); unlink(p, bck, fwd); } assert(((unsigned long)((char*)p + new_size) & (pagesz-1)) == 0); assert( ((char*)p + new_size) == ((char*)heap + heap->size) ); top(ar_ptr) = top_chunk = p; set_head(top_chunk, new_size | PREV_INUSE); /*check_chunk(ar_ptr, top_chunk);*/
首先更新非主分配区的内存统计,然后调用 delete_heap() 宏函数释放该 sub_heap ,把当前 heap 设置为被释放 sub_heap 的前一个 sub_heap , p 指向的是被释放 sub_heap 的前一个 sub_heap 的倒数第二个 chunk ,如果 p 的前一个 chunk 为空闲状态,由于不可能出现多个连续的空闲 chunk ,所以将 p 设置为 p 的前一个 chunk ,也就是 p 指向空闲 chunk ,并将该空闲 chunk 从空闲 chunk 链表中移除,并将将该空闲 chunk 赋值给 sub_heap 的 top chunk ,并设置 top chunk 的 size ,标识 top chunk 的前一个 chunk 处于 inuse 状态。然后继续判断循环条件,如果循环条件不满足,退出循环,如果条件满足,继续对当前 sub_heap 进行收缩。
} top_size = chunksize(top_chunk); extra = ((top_size - pad - MINSIZE + (pagesz-1))/pagesz - 1) * pagesz; if(extra < (long)pagesz) return 0; /* Try to shrink. */ if(shrink_heap(heap, extra) != 0) return 0; ar_ptr->system_mem -= extra; arena_mem -= extra; /* Success. Adjust top accordingly. */ set_head(top_chunk, (top_size - extra) | PREV_INUSE); /*check_chunk(ar_ptr, top_chunk);*/
首先查看 top chunk 的大小,如果 top chunk 的大小减去 pad 和 MINSIZE 小于一页大小,返回退出,否则调用 shrink_heap() 函数对当前 sub_heap 进行收缩,将空闲的整数个页收缩掉,仅剩下不足一页的空闲内存,如果 shrink_heap() 失败,返回退出,否则,更新内存使用统计,更新 top chunk 的大小。
return 1; }