musl 知:内存管理

文章目录

1. 前言

musl libc 是一个专门为嵌入式系统开发的轻量级 libc 库,以简单、轻量和高效率为特色。有不少 Linux 发行版将其设为默认的 libc 库,用来代替体积臃肿的 glibc ,如 Alpine Linux(做过 Docker 镜像的应该很熟悉)、OpenWrt(常用于路由器)和 Gentoo 等。

作者在写该篇文章的时候,musl最新发布版本是1.2.2。据作者所知 musl-1.2.0musl-1.2.1 中malloc的算法是不同的,由于历史malloc算法(1.2.0及之前)存在缺陷,所以1.2.1版本中进行了算法重构,新版malloc命名为mallocng,老版malloc命名为oldmalloc。本文是对musl-1.2.0的内存管理进行分析,关于新版内存管理算法将来会在另外文章中介绍。

本文所说的内存管理,主要是指堆内存管理,所以,如无特殊说明,下文出现的内存即表示堆内存。

2. 设计思想

malloc的设计思想如下:

In principle, this memory allocator is roughly equivalent to Doug
Lea's dlmalloc with fine-grained locking.

malloc:

Uses a freelist binned by chunk size, with a bitmap to optimize
searching for the smallest non-empty bin which can satisfy an
allocation. If no free chunks are available, it creates a new chunk of
the requested size and attempts to merge it with any existing free
chunk immediately below the newly created chunk.

Whether the chunk was obtained from a bin or newly created, it's
likely to be larger than the requested allocation. malloc always
finishes its work by passing the new chunk to realloc, which will
split it into two chunks and free the tail portion.

原则上,这个内存分配器大致相当于 Doug Lea 的带有细粒度锁定的 dlmalloc

malloc:

  • 使用按块(chunk)大小分箱(bin)的空闲列表,并使用位图(bitmap)来优化搜索可以满足分配的最小非空分箱(bin)。 如果没有可用的空闲块,它会创建一个具有请求大小的新块,并尝试将其与紧邻新创建的块下方的任何现有空闲块合并。
  • 无论块是从 bin 获得还是新创建的,它都可能大于请求的分配。malloc 总是通过将新块传递给 realloc 来完成其工作,realloc 会将其拆分为两个块并释放尾部。

这里出现了两个术语:chunk 和 bin。想要更好的理解这两个术语,可能要对堆内存管理知识有一定的了解才行。这两个术语被用在很多堆内存管理算法中。

  • chunk
    区块,表示内存块,是内存管理中分配的基本单位。内存空间会被分成连续的、大小不一的chunk。内存管理器的核心目的就是能够高效地分配和回收这些内存块(chunk)。

    这些chunk可以大致分为两类:

    • allocated chunk:已分配的chunk,即被使用的chunk
    • free chunk:空闲的chunk,即未被使用的chunk
  • bin
    bin是一种记录free chunk的链表数据结构,不同内存管理中可能也会对bin进行分类,主要是按free chunk的大小进行不同程度的分组。

3. malloc本质

在linux平台上,malloc本质上都是通过系统调用brk或者mmap从操作系统获取内存,可以参考 Syscalls used by malloc

1、brk是将数据段(.data)的最高地址指针_edata往高地址推;
2、mmap是在进程的虚拟地址空间中(堆和栈中间,称为文件映射区域的地方)找一块空闲的虚拟内存。

这两种方式分配的都是虚拟内存,没有分配物理内存。在第一次访问已分配的虚拟地址空间的时候,发生缺页中断,操作系统负责分配物理内存,然后建立虚拟内存和物理内存之间的映射关系。

musl中的内存是分别通过这两种方式来获取的。

3.1. brk

brk 通过增加程序中断位置 (brk) 从内核获取(非零初始化的)内存。 最初,堆段的起始 (start_brk) 和结束 (brk) 将指向相同的位置。

  • 当 ASLR 关闭时,start_brk 和 brk 将指向 data/bss 段的结尾(end_data)。
  • 当 ASLR 打开时,start_brk 和 brk 将等于 data/bss 段的结尾(end_data)加上随机 brk 偏移量。

在这里插入图片描述
上图“进程虚拟内存布局”图中start_brk是堆段的开始,brk(程序中断)是堆段的结束

3.1.1. 举例

系统调用brk通过直接推高堆顶指针来实现虚拟内存的分配,如下图所示:
在这里插入图片描述
第一次访问该内存时触发缺页异常再分配物理内存。操作系统回收内存的时候需要按照顺序回收,上图中需要先回收B,再回收A,brk适于分配小块内存。

3.2. mmap

malloc 使用 mmap 创建私有匿名映射段。私有匿名映射的主要目的是分配(零填充的)新内存,而这个新内存将专门由调用进程使用。

3.2.1. 举例

系统调用mmap直接在堆栈之间分配一大块内存,可以单独释放,适用于分配大块的内存,如下图中的C:
在这里插入图片描述

4. 数据结构

4.1. chuck 和 bin

4.1.1. chunk 结构

struct chunk {
   
   
	size_t psize, csize;
	struct chunk *next, *prev;
};
  • psize:表示前一个chunk的大小,最后一位是flag位。
  • csize:表示当前chunk的大小,最后一位是flag位。
  • *next:当chunk在bin中时,表示指向下一个chunk;当chunk不在bin中时,暂未使用。
  • *prev:当chunk在bin中时,表示指向上一个chunk;当chunk不在bin中时,暂未使用。

4.1.2. bin 结构

struct bin {
   
   
	volatile int lock[2];
	struct chunk *head;
	struct chunk *tail;
};
  • lock[2]:用于锁
  • *head:指向第一个chunk
  • *tail:指向最后一个chunk

4.1.3. 小结

1)chunk特点

chunk是连续的内存块。

已分配的chunk之间是通过psize和csize进行关联,当前chunk首地址向前移动上一个chunk的大小,可以找到上一个chunk的首地址;当前chunk首地址向后移动当前chunk大小,可以找到下一个chunk的首地址。

空闲的chunk之间是通过next和prev进行关联,通过当前chunk的next指针可以找到下一个chunk的首地址;通过当前chunk的prev指针可以找到上一个chunk的首地址。

flag位是1表示该chunk被使用,flag位是0表示该chunk未使用,未使用的chunk统一由bin表管理。

备注:这里说的chunk是指小内存chunk,大内存chunk不需要进行管理,大内存的chunk的flag位一直是0,所以对于使用中的chunk,flag是0表示大内存,flag是1表示小内存。关于大内存和小内存后续章节会介绍。

2)bin是一种特殊的chunk

bin和chunk结构体的前两个成员类型不同,int用于表示有符号整型数,size_t用于表示无符号整型数,但int和size_t两种类型的大小都等于机器的字长,这就说明两个结构体的前两个成员在空间上是相等的,所以bin可以看成是一种特殊的chunk。可bin又不是真的chunk,所以不需要表示chunk大小,因此前两个成员被用于锁。

3)空bin的特点
bin是一个管理空闲chunk的链表结构,成员head指向第一个chunk,成员tail指向最后一个chunk。由于bin是一种特殊的chunk,当bin链表中无空闲chunk时,head和tail将指向bin结构体的首地址。bin实际就是空闲chunk链表的表头,链表空时,head和tail指针都指向表头。

4.2. 宏

#define SIZE_ALIGN (4*sizeof(size_t)) // 最小的chunk大小
#define SIZE_MASK (-SIZE_ALIGN)
#define OVERHEAD (2*sizeof(size_t)) // chunk的前导大小,psize和csize占用的字节
#define MMAP_THRESHOLD (0x1c00*SIZE_ALIGN) // MMAP阈值,0x1c00*4*sizeof(size_t),32位上是112KB,64位上是224KB
#define DONTCARE 16
#define RECLAIM 163840  // 160KB

#define CHUNK_SIZE(c) ((c)->csize & -2)	// 清空flag位
#define CHUNK_PSIZE(c) ((c)->psize & -2) // 清空flag位
#define PREV_CHUNK(c) ((struct chunk *)((char *)(c) - CHUNK_PSIZE(c))) // chunk是连续的内存块
#define NEXT_CHUNK(c) ((struct chunk *)((char *)(c) + CHUNK_SIZE(c)))
#define MEM_TO_CHUNK(p) (struct chunk *)((char *)(p) - OVERHEAD)  // 用户使用的memory地址和chunk首地址之间相差OVERHEAD长度
#define CHUNK_TO_MEM(c) (void *)((char *)(c) + OVERHEAD)
#define BIN_TO_CHUNK(i) (MEM_TO_CHUNK(&mal.bins[i].head)) // 取head的地址,然后从内存转换到chunk,因为bin是一种特殊的chunk,所以等于获取bin结构的首地址

#define C_INUSE  ((size_t)1)  // flag位为1,表示申请的内存是小内存,释放时,通过bin链表管理,并不真正释放

#define IS_MMAPPED(c) !((c)->csize & (C_INUSE))  // flag位为0,表示内存是通过mmap分配的大内存,释放时,直接调用munmap释放

在这里插入图片描述

4.3. mal 结构

static struct {
   
   
	volatile uint64_t binmap;
	struct bin bins[64];
	volatile int free_lock[2];
} mal;
  • bitmap:位图,64位每位对应一个bin,1 表示对应bin非空(有空闲chunk),0 表示对应bin空(无空闲chunk)
  • bins[64]:bin数组,固定64个bin,表示可以有64个空闲chunk链表
  • free_lock[2]:用于锁

5. malloc/free 函数

5.1. 初识 malloc/free 函数

5.1.1. malloc 函数

void *malloc(size_t n)
{
   
   
	struct chunk *c;
	int i, j;

	if (adjust_size(&n) < 0) return 0;

	if (n > MMAP_THRESHOLD) {
   
   
		size_t len = n + OVERHEAD + PAGE_SIZE - 1 & -PAGE_SIZE;
		char *base = __mmap(0, len, PROT_READ|PROT_WRITE,
			MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
		if (base == (void *)-1) return 0;
		c = (void *)(base + SIZE_ALIGN - OVERHEAD);
		c->csize = len - (SIZE_ALIGN - OVERHEAD);
		c->psize = SIZE_ALIGN - OVERHEAD;
		return CHUNK_TO_MEM(c);
	}

	i = bin_index_up(n);
	for (;;) {
   
   
		uint64_t mask = mal.binmap & -(1ULL<<i);
		if (!mask) {
   
   
			c = expand_heap(n);
			if (!c) return 0;
			if (alloc_rev(c)) {
   
   
				struct chunk *x = c;
				c = PREV_CHUNK(c);
				NEXT_CHUNK(x)->psize = c->csize =
					x->csize + CHUNK_SIZE(c);
			}
			break;
		}
		j = first_set(mask);
		lock_bin(j);
		c = mal.bins[j].head;
		if (c != BIN_TO_CHUNK(j)) {
   
   
			if (!pretrim(c, n, i, j)) unbin(c, j);
			unlock_bin(j);
			break;
		}
		unlock_bin(j);
	}

	/* Now patch up in case we over-allocated */
	trim(c, n);

	return CHUNK_TO_MEM(c);
}

malloc的大致逻辑为:
1)如果 n 大小超过了MMAP_THRESHOLD,就直接通过__mmap(内部调用系统调用mmap)进行申请;
2)否则,试图从bin中找一个合适的chunk来分配,如果找不到合适的,就通过expand_heap延展堆空间,然后生成新的chunk;(expand_heap的逻辑是:如果brk可用就用brk,否则继续使用mmap)
3)chunk可能大于用户申请的大小,所以还会涉及到分割/合并等操作。

5.1.2. free 函数

void free(void *p)
{
   
   
	if (!p) return;

	struct chunk *self = MEM_TO_CHUNK(p);

	if (IS_MMAPPED(self))
		unmap_chunk(self);
	else
		__bin_chunk(self);
}

free的大致逻辑为:
1)如果内存是通过mmap申请的(较大的内存),就通过unmap_chunk(内部通过系统调用munmap)进行释放。
2)否则,通过__bin_chunk管理这些空闲chunk(较小的内存),并不进行释放。

5.1.3. 小结

1)大内存,小内存

  • 大内存:申请大于MMAP_THRESHOLD的内存称为大内存。用户申请大内存时,直接通过系统调用mmap获取;释放时,直接通过系统调用munmap释放。每次都要向操作系统申请。

  • 小内存:申请不大于MMAP_THRESHOLD的内存称为小内存。用户申请小内存时,如果bin链表中有合适的空闲chunk,则直接通过空闲chunk获取,否则,通过brk或mmap获取新的内存;释放时,将chunk返还到合适的bin链表中,而不是返还给操作系统。并非每次都要向操作系统申请,已申请到的内存可以重复使用。(若管理不好,可能会导致内存持续增长)

5.2. 解析 malloc 函数

malloc函数的处理逻辑分如下几步:
1)调整用户指定的申请大小
2)若申请大内存,通过mmap进行申请
3)若申请小内存,通过bin链表查找可用空闲chunk,或重新分配新的chunk,然后做chunk分割/合并操作

下面是每一步的详细介绍。

5.2.1. adjust_size 函数

static int adjust_size(size_t *n)
{
   
   
	/* Result of pointer difference must fit in ptrdiff_t. */
	if (*n-1 > PTRDIFF_MAX - SIZE_ALIGN - PAGE_SIZE) {
   
   
		if (*n) {
   
   
			errno = ENOMEM;
			return -1;
		} else {
   
   
			*n = SIZE_ALIGN;
			return 0;
		}
	}
	*n = (*n + OVERHEAD + SIZE_ALIGN - 1) & SIZE_MASK;
	return 0;
}

adjust_size用于对申请的大小做调整,如果调整失败,说明本次申请内存失败;如果调整成功,说明可以继续进行内存申请。另外,调整成功后的大小一般都会比用户指定的申请大小大。

5.2.1.1. 数据结构
  • ptrdiff_t:是两个指针相减的结果的带符号整数类型,PTRDIFF_MAX定义该类型最大值。
  • SIZE_ALIGN:用于chunk的内存大小对齐
  • PAGE_SIZE:用于通过mmap申请的内存大小对齐
5.2.1.2. 函数解析

用户调用malloc申请内存,指定的大小有三种情况:0,适中,超大。
1)0:要么申请失败,要么按默认的最小内存申请;
2)适中:一般都会在申请的大小上加一些管理信息大小,再做内存对齐,然后根据调整后的大小进行内存申请;
3)超大:当进行超大内存申请时,将申请失败。

	/* Result of pointer difference must fit in ptrdiff_t. */
	if (*n-1 > PTRDIFF_MAX - SIZE_ALIGN - PAGE_SIZE) {
   
   
		if (*n) {
   
   
			errno = ENOMEM;
			return -1;
		} else {
   
   
			*n = SIZE_ALIGN;
			return 0;
		}
	}

1)如果申请超大内存,提示无可用内存错误
2)如果申请0大小内存,重新调整大小为默认的最小内存大小

	*n = (*n + OVERHEAD + SIZE_ALIGN - 1) & SIZE_MASK;

3)如果申请适中内存,加上chunk中管理信息大小OVERHEAD,再做 + SIZE_ALIGN - 1) & SIZE_MASK 内存对齐,得到新的申请大小。

理解:
假设用户申请的内存大小为n1,调整后的大小为n,此时 n = (n1 + OVERHEAD + SIZE_ALIGN - 1) & SIZE_MASK。

  • 例子1:n1 + OVERHEAD 正好是 SIZE_ALIGN 整数倍,那么 n = n1 + OVERHEAD。
  • 例子2:n1 + OVERHEAD 不是 SIZE_ALGIN 整数倍,那么 n > n1 + OVERHEAD 且 n < n1 + OVERHEAD + SIZE_ALIGN。

5.2.2. 申请大内存

	if (n > MMAP_THRESHOLD) {
   
   
		size_t len = n + OVERHEAD + PAGE_SIZE - 1 & -PAGE_SIZE;
		char *base = __mmap(0, len, PROT_READ|PROT_WRITE,
			MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
		if (base == (void *)-1) return 0;
		c = (void *)(base + SIZE_ALIGN - OVERHEAD);
		c->csize = len - (SIZE_ALIGN - OVERHEAD);
		c->psize = SIZE_ALIGN - OVERHEAD;
		return CHUNK_TO_MEM(c);
	}
5.2.2.1. 哨兵chunk

每次分配chunk的时候,如果新chunk与之前分配的chunk地址不连续,都会多分配一个零大小的哨兵chunk。哨兵chunk和后面chunk间的内存是连续的,作为chunk的前导,像站岗的哨兵一样。哨兵chunk不关心可用内存,至少预留psize和csize两个成员的空间,因此占用的大小不低于OVERHEAD。

5.2.2.2. 逻辑简析
		size_t len = n + OVERHEAD + PAGE_SIZE - 1 & -PAGE_SIZE;

n 在 adjust_size 函数中已经调整过了,且按SIZE_ALIGN进行了对齐,此处 + OVERHEAD 预留了哨兵chunk。PAGE_SIZE - 1 & -PAGE_SIZE 按页大小对齐。

        c = (void *)(base + SIZE_ALIGN - OVERHEAD);
        c->csize = len - (SIZE_ALIGN - OVERHEAD);
        c->psize = SIZE_ALIGN - OVERHEAD;

上面提到内存的大小按SIZE_ALGIN对齐,因为分配的内存需要预留一个哨兵chunk,又因为SIZE_ALGIN等于2倍OVERHEAD,所以将内存地址向后偏移SIZE_ALGIN,既能能保证内存按SIZE_ALGIN对齐,又能保证预留足够的空间给哨兵chunk。内存地址向前偏移OVERHEAD可以得到chunk首地址,所以c = (void *)(base + SIZE_ALIGN - OVERHEAD)。

SIZE_ALGIN长度内,chunk又占用了OVERHEAD,所以哨兵chunk的大小为 SIZE_ALIGN - OVERHEAD,根据哨兵chunk的大小,初始化当前chunk信息,其中当前chunk的psize等于哨兵chunk的大小,由于csize是偶数,且chunk是通过mmap分配的,所以不需要单独清空flag位,默认flag位就是0。哨兵chunk不关心内存大小,所以不需要初始化哨兵chunk。

思考:内存地址向后偏移了SIZE_AGLIN,那么能保证剩下的内存大小满足用户申请的内存大小吗?
解:假设用户申请的大小是n1,剩下的内存大小是n2,论证过程如下:
1)n 在adjust_size中经过了调整,那么n >= n1 + OVERHEAD 且 n < n1 + OVERHEAD + SIZE_ALIGN,n = x * SIZE_ALGIN。
2)因为len经过了PAGE_SIZE对齐,所以len >= n + OVERHEAD 且 len < n + OVERHEAD + PAGE_SIZE。
3)当n和len都取最小值时,即n = n1 + OVERHEAD,len = n + OVERHEAD = n1 + OVERHEAD + OVERHEAD = n1 + 2 * OVERHEAD,内存地址向后偏移了SIZE_AGLIN,相当于len减去SIZE_AGLIN,所以n2 = len - SIZE_AGLIN = n1 + 2 * OVERHEAD - SIZE_AGLIN。
4)只要SIZE_AGLIN <= 2 * OVERHEAD,就能保证剩下的内存大小满足用户申请的内存大小。目前SIZE_AGLIN = 2 * OVERHEAD,所以剩下的内存大小满足用户申请的内存大小,不会出现内存越界操作。

5.2.2.3. __mmap 函数
#define UNIT SYSCALL_MMAP2_UNIT
#define OFF_MASK ((-0x2000ULL << (8*sizeof(syscall_arg_t)-1)) | (UNIT-1))

void *__mmap(void *start, size_t len, int prot, int flags, int fd, off_t off)
{
   
   
	long ret;
	if (off & OFF_MASK) {
   
   
		errno = EINVAL;
		return MAP_FAILED;
	}
	if (len >= PTRDIFF_MAX) {
   
   
		errno = ENOMEM;
		return MAP_FAILED;
	}
	if (flags & MAP_FIXED) {
   
   
		__vm_wait();
	}
#ifdef SYS_mmap2
	ret = __syscall(SYS_mmap2, start, len, prot, flags, fd, off/UNIT);
#else
	ret = __syscall(SYS_mmap, start, len, prot, flags, fd, off);
#endif
	/* Fixup incorrect EPERM from kernel. */
	if (ret == -EPERM && !start && (flags&MAP_ANON) && !(flags&MAP_FIXED))
		ret = -ENOMEM;
	return (void *)__syscall_ret(ret);
}

weak_alias(__mmap, mmap);

weak_alias(mmap, mmap64);

__mmap对外接口是mmap,通过调用系统调用mmap实现内存分配。关于mmap的原理读者可以自行研究。

5.2.3. 申请小内存

	i = bin_index_up(n);
	for (;;) {
   
   
		uint64_t mask = mal.binmap & -(1ULL<<i);
		if (!mask) {
   
   
			c = expand_heap(n);
			if (!c) return 0;
			if (alloc_rev(c)) {
   
   
				struct chunk *x = c;
				c = PREV_CHUNK(c);
				NEXT_CHUNK(x)->psize = c->csize =
					x->csize + CHUNK_SIZE(c);
			}
			break;
		}
		j = first_set(mask);
		lock_bin(j);
		c = mal.bins[j].head;
		if (c != BIN_TO_CHUNK(j)) {
   
   
			if (!pretrim(c, n, i, j)) unbin(c, j);
			unlock_bin(j);
			break;
		}
		unlock_bin(j);
	}

	/* Now patch up in case we over-allocated */
	trim(c, n);
5.2.3.1. 逻辑简析
	i = bin_index_up(n);

根据内存大小n,通过bin_index_up查找对应的bin的索引值。

		uint64_t mask = mal.binmap & -(1ULL<<i);

将位图binmap低于i的位清零,大于等于i对应的bin链表上的空闲chunk都能满足内存分配。

		if (!mask) {
   
   
			c = expand_heap(n);
			if (!c) return 0;
			if (alloc_rev(c)) {
   
   
				struct chunk *x = c;
				c = PREV_CHUNK(c);
				NEXT_CHUNK(x)->psize = c->csize =
					x->csize + CHUNK_SIZE(c);
			}
			break;
		}

mask为0,表示大于等于i对应的bin链表都是空链表,没有空闲chunk可用于分配,所以需要expand_heap扩展堆内存分配新的chunk。
alloc_rev©用于判断前一个chunk是否空闲,如果前一个chunk是空闲的,会将其从bin链表上取下,用于同新chunk进行合并。
合并操作很简单,就是将新chunk的大小和新chunk的下一个chunk的psize进行更新,加上前一个chunk的大小。

思考:新chunk怎么会有下一个chunk呢?
解:新的chunk都是通过expand_heap分配的,expand_heap内部会保证chunk的内存连续性,如果第一次扩展堆,即分配第一个chunk,这时就会多分配一个哨兵chunk。除了哨兵chunk,实际上还有一个尾chunk,它是一个连续chunk的最后一个chunk,和哨兵chunk是一对,一前一后标识着连续内存块的起始和结尾。大内存中没有尾chunk,而小内存中在分配chunk的时候,会预留尾chunk的空间。所以新chunk的下一个chunk就是尾chunk。

		j = first_set(mask);
		lock_bin(j);
		c = mal.bins[j].head;
		if (c != BIN_TO_CHUNK(j)) {
   
   
			if (!pretrim(c, n, i, j)) unbin(c, j);
			unlock_bin(j);
			break;
		}
		unlock_bin(j);

大于等于i对应的bin链表可能会有多个是非空的链表,由于bin是分组的,下标越大对应的空闲chunk就越大,所以取最低下标对应的bin上的chunk是最合适的。first_set(mask)获取mask二进制位从低到高第一次出现1的位置,该位置就对应着bins的索引。

bin链表属于共享资源,从bin表上取下合适的chunk需要加锁操作,lock_bin函数除了加锁,如果bin链表未初始化,还会初始化bin链表,即bin结构的head和tail指针指向bin结构体的首地址,标识bin链表是一个空表。

c = mal.bins[j].head 表示c是bin表上的第一个chunk,BIN_TO_CHUNK(j)表示bin结构的首地址,即bin链表的表头,如果第一个chunk和表头相等,说明bin链表是一个空表,所以只有c != BIN_TO_CHUNK(j)时,才有可用的空闲chunk。

找到可用的空闲chunk之后,如果该chunk对应的内存比用户申请的内存大一些,可能需要稍微裁剪一下,如果预裁剪pretrim成功,将会完成裁剪;如果不成功,将该空闲chunk从bin中取下来,等待后续的处理。

	/* Now patch up in case we over-allocated */
	trim(c, n);

裁剪即将被用户使用的chunk,新分配的chunk或未预裁剪成功(某些chunk裁剪下来的chunk需要重新找到合适的bin链表进行挂载,所以不在pretrim中处理)的空闲chunk对应的内存都可能比用户申请的内存大一些,裁剪trim除了保留够用户使用的内存,还会将裁剪下来的chunk重新找到合适的bin链表进行挂载。

5.2.3.2. bin 下标

static const unsigned char bin_tab[60] = {
   
   
	            32,33,34,35,36,36,37,37,38,38,39,39,
	40,40,40,40,41,41,41,41,42,42,42,42,43,43,43,43,
	44,44,44,44,44,44,44,44,45,45,45,45,45,45,45,45,
	46,46,46,46,46,46,46,46,47,47,47,47,47,47,47,47,
};

static int bin_index(size_t x)
{
   
   
	x = x / SIZE_ALIGN - 1;
	if (x <= 32) return x;
	if (x < 512) return bin_tab[x/8-4];
	if (x > 0x1c00) return 63;
	return bin_tab[x/128-4] + 16;
}

static int bin_index_up(size_t x)
{
   
   
	x = x / SIZE_ALIGN - 1;
	if (x <= 32) return x;
	x--;
	if (x < 512) return bin_tab[x/8-4] + 1;
	return bin_tab[x/128-4] + 17;
}
5.2.3.2.1. bin_tab 表

bin_tab中数据比较有意思,如:
32~35 每个出现1次
36~39 每个出现2次
40~43 每个出现4次
44~47 每个出现8次

bins数组根据不同空闲chunk的大小做了分组,不同bin上面的chunk的大小有的是固定的,有的是一个范围,而范围的宽度也不全是一样的。

bin_tab中出现的数值其实就是bins数组的下标,下标出现的次数代表范围的宽度,比如:[1, 4) 范围宽度为4,[4, 12) 范围宽度为8。

其中,下标0~31和48 ~63都不在bin_tab表中,实际上,下标0 ~31对应的是固定的chunk,下标32 ~47对应的是一些范围的chunk,而下标48 ~63同下标32 ~47出现的次数规律是一样的,但是次数是指数递增的,通过在下标32 ~47的基础上加16,规律如下:
48~51 每个出现16次,即范围宽度是16
52~55 每个出现32次,即范围宽度是32
56~59 每个出现64次,即范围宽度是64
60~62 每个出现128次,即范围宽度是128
63 范围宽度不限制

5.2.3.2.2. bin_index 函数

x 是经过 SIZE_ALIGN对齐的,所以 (x = x / SIZE_ALIGN - 1) >= 0。
假设下标为i,N = SIZE_ALIGN,下面列出x和i的对应关系:
1)当 x = [0, 31] 时,i = x = [0, 31],bin[i]对应的chunk大小为:(i + 1) * N
2)当 x >= 32 时,i不再对应固定的x,而是对应x的范围,如下:
当 x = [32, 40) 时,i = 32,bin[i]对应的chunk大小为:[32+(i-32)*8+1, 40+(i-32)*8] * N
当 x = [40, 48) 时,i = 33,bin[i]对应的chunk大小为:[32+(i-32)*8+1, 40+(i-32)*8] * N
当 x = [48, 56) 时,i = 34,bin[i]对应的chunk大小为:[32+(i-32)*8+1, 40+(i-32)*8] * N
当 x = [56, 64) 时,i = 35,bin[i]对应的chunk大小为:[32+(i-32)*8+1, 40+(i-32)*8] * N
当 x = [64, 80) 时,i = 36,bin[i]对应的chunk大小为:[64+(i-36)*16+1, 80+(i-36)*16] * N
当 x = [80, 96) 时,i = 37,bin[i]对应的chunk大小为:[64+(i-36)*16+1, 80+(i-36)*16] * N
当 x = [96, 112) 时,i = 38,bin[i]对应的chunk大小为:[64+(i-36)*16+1, 80+(i-36)*16] * N
当 x = [112, 128) 时,i = 39,bin[i]对应的chunk大小为:[64+(i-36)*16+1, 80+(i-36)*16] * N
当 x = [128, 160) 时,i = 40,bin[i]对应的chunk大小为:[128+(i-40)*32+1, 160+(i-40)*32] * N
当 x = [160, 192) 时,i = 41,bin[i]对应的chunk大小为:[128+(i-40)*32+1, 160+(i-40)*32] * N
当 x = [192, 224) 时,i = 42,bin[i]对应的chunk大小为:[128+(i-40)*32+1, 160+(i-40)*32] * N
当 x = [224, 256) 时,i = 43,bin[i]对应的chunk大小为:[128+(i-40)*32+1, 160+(i-40)*32] * N
当 x = [256, 320) 时,i = 44,bin[i]对应的chunk大小为:[256+(i-44)*64+1, 320+(i-44)*64] * N
当 x = [320, 384) 时,i = 45,bin[i]对应的chunk大小为:[256+(i-44)*64+1, 320+(i-44)*64] * N
当 x = [384, 448) 时,i = 46,bin[i]对应的chunk大小为:[256+(i-44)*64+1, 320+(i-44)*64] * N
当 x = [448, 512) 时,i = 47,bin[i]对应的chunk大小为:[256+(i-44)*64+1, 320+(i-44)*64] * N

当 x = [512, 640) 时,i = 48,bin[i]对应的chunk大小为:[512+(i-48)*128+1, 640+(i-48)*128] * N
当 x = [640, 768) 时,i = 49,bin[i]对应的chunk大小为:[512+(i-48)*128+1, 640+(i-48)*128] * N

当 x = [1024, 1280) 时,i = 52,bin[i]对应的chunk大小为:[1024+(i-52)*256+1, 1280+(i-52)*256] * N
当 x = [1280, 1536) 时,i = 53,bin[i]对应的chunk大小为:[1024+(i-52)*256+1, 1280+(i-52)*256] * N

当 x = [2048, 2560) 时,i = 56,bin[i]对应的chunk大小为:[2048+(i-56)*512+1, 2560+(i-56)*512] * N
当 x = [2560, 3072) 时,i = 57,bin[i]对应的chunk大小为:[2048+(i-56)*512+1, 2560+(i-56)*512] * N

当 x = [4096, 5120) 时,i = 60,bin[i]对应的chunk大小为:[4096+(i-60)*1024+1, 5120+(i-60)*1024] * N

当 x = [6144, 7168) 时,i = 62,bin[i]对应的chunk大小为:[4096+(i-60)*1024+1, 5120+(i-60)*1024] * N
当 x >= 7168 时,i = 63,bin[i]对应的chunk大小为:> (5120+(62-60)*1024) * N

下面列出不同bin的下标和管理的chunk大小的关系:(64位上:N=32)

bin的下标i chunk大小公式 chunk大小范围 下标i于chunk大小的关系
0~31 (i+1) * N 0x20~0x400(1k) (i+1)*0x20
32~35 [32+(i-32)*8+1, 40+(i-32)*8] * N 0x420~0x800(2k) [0x420+(i-32)*0x100, 0x500+(i-32)*0x100]
36~39 [64+(i-36)*16+1, 80+(i-36)*16] * N 0x820~0x1000(4k) [0x820+(i-36)*0x200, 0xA00+(i-36)*0x200]
40~43 [128+(i-40)*32+1, 160+(i-40)*32] * N 0x1020~0x2000(8k) [0x1020+(i-40)*0x400, 0x1400+(i-40)*0x400]
44~47 [256+(i-44)*64+1, 320+(i-44)*64] * N 0x2020~0x4000(16k) [0x2020+(i-44)*0x800, 0x2800+(i-44)*0x800]
48~51 [512+(i-48)*128+1, 640+(i-48)*128] * N 0x4020~0x8000(32k) [0x4020+(i-48)*0x1000, 0x5000+(i-48)*0x1000]
52~55 [1024+(i-52)*256+1, 1280+(i-52)*256] * N 0x8020~0x10000(64k) [0x8020+(i-52)*0x2000, 0xA000+(i-52)*0x2000]
56~59 [2048+(i-56)*512+1, 2560+(
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

canpool

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值