postgreSQL源码分析——存储管理——内存管理(2)

2021SC@SDUSC

概述

上篇博客分析了内存上下文的各种结构,以及实现内存上下文的结构。这次就继续上一篇的内容,来分析一下内存上下文的相关操作(即函数部分)。

源码分析

相关源码位于src/backend/utils/mmgr/aset.c
内存上下文的操作中,比较重要的函数就是以下几个:

内存上下文的初始化

任何一个postgreSQL的进程在使用内存上下文之前都要进行初始化,通过调用MemoryContextInit函数来进行初始化。

void
MemoryContextInit(void)
{
	AssertState(TopMemoryContext == NULL);//如果TopMemoryContext不为空,说明已经初始化过了,不需要再继续初始化了

	 //初始化TopMemoryContext,即初始化内存上下文树,并初始化根节点。
	TopMemoryContext = AllocSetContextCreate((MemoryContext) NULL,
											 "TopMemoryContext",
											 ALLOCSET_DEFAULT_SIZES);

	 //由于当前内存上下文并没有其他可以指向的对象,因此将其指向刚创建的TopMemoryContext。
	CurrentMemoryContext = TopMemoryContext;

	 //初始化ErrorContext,作为TopMemoryContext的第一个子节点。我在上一篇博客有提到过,这里会始终保留最少8KB的空闲空间,作为内存用完时的备用空间。这也是唯一一个需要保留空闲空间的内存上下文。
	ErrorContext = AllocSetContextCreate(TopMemoryContext,
										 "ErrorContext",
										 8 * 1024,
										 8 * 1024,
										 8 * 1024);
	MemoryContextAllowInCriticalSection(ErrorContext, true);
}

通过内存上下文初始化,为进程初始化了内存上下文树,即建立了TopMemoryContext、ErrorContext这两个核心的节点。

内存上下文的创建

上面的初始化完成后,进程就可以创建其他类型的内存上下文了。通过调用AllocSetContextCreate函数来实现。
AllocSetContextCreate函数

#define AllocSetContextCreate \
	AllocSetContextCreateInternal
#endif

这个函数是一个预定义函数,本质是调用AllocSetContextCreateInternal函数。
AllocSetContextCreateInternal函数

MemoryContext
AllocSetContextCreateInternal(MemoryContext parent,//该节点的父节点
							  const char *name,//该节点的名称
							  Size minContextSize,//最小的内存上下文大小
							  Size initBlockSize,//初始化内存块大小
							  Size maxBlockSize)//最大的内存块大小
{
	int			freeListIndex;//即该上下文在context_freelists中的序号,如果未放入则为-1。
	Size		firstBlockSize;//第一个内存块的大小
	AllocSet	set;//要初始化的内存上下文头部数据结构(上篇博客分析过)
	AllocBlock	block;//内存块头部信息

	 //检查参数是否匹配可用的freelist
	if (minContextSize == ALLOCSET_DEFAULT_MINSIZE &&
		initBlockSize == ALLOCSET_DEFAULT_INITSIZE)//如果最小大小和初始化大小均符合默认
		freeListIndex = 0;
	else if (minContextSize == ALLOCSET_SMALL_MINSIZE &&
			 initBlockSize == ALLOCSET_SMALL_INITSIZE)//如果最小大小和初始化大小均为最小
		freeListIndex = 1;
	else
		freeListIndex = -1;//未找到可用的freelist

	//如果存在合适的freelist,回收一个空闲的上下文空间利用即可
	if (freeListIndex >= 0)
	{
		AllocSetFreeList *freelist = &context_freelists[freeListIndex];//获取对应的freelist

		if (freelist->first_free != NULL)//freelist不为空,即存在符合条件的空闲空间
		{
			set = freelist->first_free;//拿到空闲空间指针,赋给set
			freelist->first_free = (AllocSet) set->header.nextchild;//将在freelist中取得的空闲空间指针
			freelist->num_free--;//将freelist中的空闲空间数减去1

			//更新最大块大小
			set->maxBlockSize = maxBlockSize;

			//调用MemoryContextCreate函数创建一个MemoryContext结构(内存上下文头部信息)赋给set
			MemoryContextCreate((MemoryContext) set,
								T_AllocSetContext,
								&AllocSetMethods,
								parent,
								name);

			return (MemoryContext) set;
		}
	}
	//不存在合适的freelist
	//设置要初始化的内存块(内存上下文也要存放在内存块中)的大小
	firstBlockSize = MAXALIGN(sizeof(AllocSetContext)) +
		ALLOC_BLOCKHDRSZ + ALLOC_CHUNKHDRSZ;
		
	if (minContextSize != 0)//如果最小上下文大小不为0
		firstBlockSize = Max(firstBlockSize, minContextSize);//则取firstBlockSize和minContextSize两者中更大的值
	else//否则
		firstBlockSize = Max(firstBlockSize, initBlockSize);//则取firstBlockSize和initBlockSize两者中更大的值

	set = (AllocSet) malloc(firstBlockSize);//set即内存上下文分配足够的内存空间,并强制转换为AllocSet结构体,填充AllocSet其他结构体元素的信息,比如初始化内存块的大小以及最大内存块的大小,并根据最大内存块的大小设置要分配内存片的尺寸阈值。
	if (set == NULL)//如果分配失败
	{
		if (TopMemoryContext)//如果TopMemoryContext已经存在
			MemoryContextStats(TopMemoryContext);//检查TopMemoryContext的状态
		ereport(ERROR,
				(errcode(ERRCODE_OUT_OF_MEMORY),
				 errmsg("out of memory"),
				 errdetail("Failed while creating memory context \"%s\".",
						   name)));//否则可能是内存空间不够用导致错误
	}

	//填充初始化块的头部信息
	block = (AllocBlock) (((char *) set) + MAXALIGN(sizeof(AllocSetContext)));//将该内存块转换为AllocBlock结构
	block->aset = set;//初始化该内存块所位于的AllocSet
	block->freeptr = ((char *) block) + ALLOC_BLOCKHDRSZ;//初始化该内存块中空闲区域的首地址,即块头后
	block->endptr = ((char *) set) + firstBlockSize;//指向该内存块的尾地址
	block->prev = NULL;//初始化链表中上一个内存块的指针为NULL
	block->next = NULL;//初始化链表中下一个内存块的指针为NULL

	//将为分配的空间标记为不可访问
	VALGRIND_MAKE_MEM_NOACCESS(block->freeptr, block->endptr - block->freeptr);

	//将该块放入该内存上下文中存放所有内存块的链表
	set->blocks = block;
	//将该块放入keeper,即下一次内存上下文重置时内存空间会被保留
	set->keeper = block;

	//填充上下文中AllocSet数据结构的一些头部信息
	MemSetAligned(set->freelist, 0, sizeof(set->freelist));
	set->initBlockSize = initBlockSize;
	set->maxBlockSize = maxBlockSize;
	set->nextBlockSize = initBlockSize;
	set->freeListIndex = freeListIndex;

	set->allocChunkLimit = ALLOC_CHUNK_LIMIT;//按照上篇博客分析,内存片的大小不能超出ALLOC_CHUNK_LIMIT的限制。
	//只要内存片的阈值和内存片的头部数据大小大于(最大块大小减去块头,再除以分配的内存片数量)即每个内存片可分配的平均大小
	while ((Size) (set->allocChunkLimit + ALLOC_CHUNKHDRSZ) >
		   (Size) ((maxBlockSize - ALLOC_BLOCKHDRSZ) / ALLOC_CHUNK_FRACTION))
		set->allocChunkLimit >>= 1;//右移一位,即/2,来缩小内存片的阈值(因为内存片的大小为2的次幂,只能通过这种方式来扩大或者缩小)
	
	//调用MemoryContextCreate函数进行上下文的创建
	MemoryContextCreate((MemoryContext) set,
						T_AllocSetContext,
						&AllocSetMethods,
						parent,
						name);

	return (MemoryContext) set;//返回创建的上下文
}

整个流程大致如下:

  1. 判断是否有可用的空闲内存上下文空间,如果有则直接在对应的空间中调用MemoryContextCreate函数创建上下文。
  2. 如果没有,则需要计算所需的内存空间,然后申请内存空间。然后再填充一些头部信息,上下文的头部信息、第一个内存块的头部信息、内存片的头部信息(这里要对内存片的最大阈值进行判断)。最终调用MemoryContextCreate函数创建上下文并返回。

内存上下文关于内存的操作

在前面我提到过,postgreSQL实现了自己的palloc、repalloc、pfree三个函数来替代C语言标准库中的malloc、realloc、free三个函数来进行内存上下文中对于内存的分配、重分配、释放三个操作。

内存上下文中的内存分配

palloc函数
palloc本质上是一个宏定义,它会被转为当前上下文中对MemoryContextAlloc函数的调用。

void *
MemoryContextAlloc(MemoryContext context, Size size)//要分配的内存上下文,以及大小
{
	void	   *ret;

	context->isReset = false;//新分配的,设置为未重置

	ret = context->methods->alloc(context, size);//核心:调用methods变量中的alloc——即AllocSetAlloc函数
	if (unlikely(ret == NULL))//如果调用失败
	{
		MemoryContextStats(TopMemoryContext);
		ereport(ERROR,
				(errcode(ERRCODE_OUT_OF_MEMORY),
				 errmsg("out of memory"),
				 errdetail("Failed on request of size %zu in memory context \"%s\".",
						   size, context->name)));//报错
	}
	return ret;//返回分配好的内存片指针
}

可以看到,MemoryContextAlloc实际上是调用了当前内存上下文的methods变量中所指定的alloc函数指针。而postgreSQL只实现了AllocSetAlloc函数,所以调用的就是AllocSetAlloc函数。而使用palloc分配的内存空间中的内容是随机的,此外,还定义了一个palloc0,会将分配的内存中的内容全部设置为0。
AllocSetAlloc函数

static void *
AllocSetAlloc(MemoryContext context, Size size)//要分配的内存上下文,以及要分配的大小
{
	AllocSet	set = (AllocSet) context;
	AllocBlock	block;
	AllocChunk	chunk;
	int			fidx;
	Size		chunk_size;
	Size		blksize;

	 //如果要分配的大小超过了内存片的阈值
	if (size > set->allocChunkLimit)
	{
		chunk_size = MAXALIGN(size);//通过size计算字节对齐后的内存片大小
		blksize = chunk_size + ALLOC_BLOCKHDRSZ + ALLOC_CHUNKHDRSZ;//块大小设置为内存片数据大小+块头大小+内存片头大小
		block = (AllocBlock) malloc(blksize);//分配内存块
		if (block == NULL)//分配失败,返回NULL
			return NULL;
		//初始化相关信息
		block->aset = set;
		block->freeptr = block->endptr = ((char *) block) + blksize;//没有空闲区域(全被利用)
		chunk = (AllocChunk) (((char *) block) + ALLOC_BLOCKHDRSZ);//从块头后面设置为内存片
		//初始化内存片
		chunk->aset = set;
		chunk->size = chunk_size;

		 //如果上下文中已经有分配的内存块(链表头为活跃内存块,也只有链表头中的空闲空间会被使用)
		if (set->blocks != NULL)
		{
			//由于新块已经没有空闲空间,所以放在链表头会导致原链表头的空闲空间被浪费,因此放在链表头后
			//链表中间插入的基本操作(在下面的源码中会再次见到,会在下面详细分析)
			block->prev = set->blocks;
			block->next = set->blocks->next;
			if (block->next)
				block->next->prev = block;
			set->blocks->next = block;
		}
		else//没有已经分配的内存块,则将该块作为链表头
		{
			block->prev = NULL;
			block->next = NULL;
			set->blocks = block;
		}

		AllocAllocInfo(set, chunk);
		return AllocChunkGetPointer(chunk);//返回设置好的内存片
	}
	
	//这里说明要分配的内存大小在内存片的阈值内
	fidx = AllocSetFreeIndex(size);//获取freelist数组序号
	chunk = set->freelist[fidx];//获取空闲内存片
	if (chunk != NULL)//如果找到合适的空闲内存片
	{
		//从freelist中取出合适的内存片并修改头信息,然后从freelist中删去该内存片
		set->freelist[fidx] = (AllocChunk) chunk->aset;
		chunk->aset = (void *) set;
		AllocAllocInfo(set, chunk);
		return AllocChunkGetPointer(chunk);//返回内存片
	}
	//没找到合适的空闲内存片
	chunk_size = (1 << ALLOC_MINBITS) << fidx;//根据数组序号还原所需的内存片的大小

	if ((block = set->blocks) != NULL)//如果存在已分配的内存块
	{
		Size		availspace = block->endptr - block->freeptr;//获取可用(空闲)大小

		if (availspace < (chunk_size + ALLOC_CHUNKHDRSZ))//如果空闲大小不满足条件,会将该块的空闲空间转为内存片放入freelist中
		{
			//这部分在重分配会遇到,所以这里就不详细分析了,详细分析写在下面的重分配中。
			while (availspace >= ((1 << ALLOC_MINBITS) + ALLOC_CHUNKHDRSZ))
			{
				Size		availchunk = availspace - ALLOC_CHUNKHDRSZ;
				int			a_fidx = AllocSetFreeIndex(availchunk);


				if (availchunk != ((Size) 1 << (a_fidx + ALLOC_MINBITS)))
				{
					a_fidx--;
					Assert(a_fidx >= 0);
					availchunk = ((Size) 1 << (a_fidx + ALLOC_MINBITS));
				}

				chunk = (AllocChunk) (block->freeptr);

		

				block->freeptr += (availchunk + ALLOC_CHUNKHDRSZ);
				availspace -= (availchunk + ALLOC_CHUNKHDRSZ);

				chunk->size = availchunk;

			block = NULL;//设置为需要新内存块
		}
	}

	if (block == NULL)//如果需要新建内存块(这部分在重分配中也有,就不详细分析了,详细分析在重分配部分)
	{
		Size		required_size;

		blksize = set->nextBlockSize;
		set->nextBlockSize <<= 1;
		if (set->nextBlockSize > set->maxBlockSize)
			set->nextBlockSize = set->maxBlockSize;
		required_size = chunk_size + ALLOC_BLOCKHDRSZ + ALLOC_CHUNKHDRSZ;
		while (blksize < required_size)
			blksize <<= 1;

		block = (AllocBlock) malloc(blksize);

		/*
		 * We could be asking for pretty big blocks here, so cope if malloc
		 * fails.  But give up if there's less than a meg or so available...
		 */
		while (block == NULL && blksize > 1024 * 1024)
		{
			blksize >>= 1;
			if (blksize < required_size)
				break;
			block = (AllocBlock) malloc(blksize);
		}

		if (block == NULL)
			return NULL;

		block->aset = set;
		block->freeptr = ((char *) block) + ALLOC_BLOCKHDRSZ;
		block->endptr = ((char *) block) + blksize;

		/* Mark unallocated space NOACCESS. */
		VALGRIND_MAKE_MEM_NOACCESS(block->freeptr,
								   blksize - ALLOC_BLOCKHDRSZ);

		block->prev = NULL;
		block->next = set->blocks;
		if (block->next)
			block->next->prev = block;
		set->blocks = block;
	}

	chunk = (AllocChunk) (block->freeptr);//分配内存片

	
	//初始化要分配的内存片的头部信息
	block->freeptr += (chunk_size + ALLOC_CHUNKHDRSZ);
	Assert(block->freeptr <= block->endptr);
	chunk->aset = (void *) set;
	chunk->size = chunk_size;

	AllocAllocInfo(set, chunk);

	return AllocChunkGetPointer(chunk);
}

这里的很多操作在下面的内存重分配中都有,因此就不详细分析了,详细分析放在了下面的内存重分配中(因为我先分析的内存重分配)。

内存上下文中的内存重分配

AllocSetAlloc函数
将在指定的内存上下文中对context指向的上下文的内存空间进行重新分配,而新分配的内存大小由另一个参数size来决定。context中的内容将会被复制到新分配的内存空间中,并且释放context原来的内存空间,最终返回一个在新内存空间的上下文的指针。

static void *
AllocSetAlloc(MemoryContext context, Size size)//两个参数,要重分配的上下文以及新的内存大小
{
	AllocSet	set = (AllocSet) context;//将上下文强转为AllocSet类型
	AllocBlock	block;//内存块
	AllocChunk	chunk;//内存片
	int			fidx;
	Size		chunk_size;//内存片大小
	Size		blksize;//内存块大小

	AssertArg(AllocSetIsValid(set));//判断set是否是正确的AllocSet类型


	 //如果请求的大小超过了最大的内存片的大小(这种情况我在上篇博客提到过),则会分配一整个内存块给这个请求。
	if (size > set->allocChunkLimit)
	{
		chunk_size = MAXALIGN(size);
		blksize = chunk_size + ALLOC_BLOCKHDRSZ + ALLOC_CHUNKHDRSZ;//要获取的内存块的大小,即内存片数据大小加上内存块头部大小加内存片头部大小组成
		block = (AllocBlock) malloc(blksize);//给内存块分配需要的大小
		if (block == NULL)//内存块分配失败则返回NULL
			return NULL;
		block->aset = set;//内存块分配成功,放置内存上下文
		block->freeptr = block->endptr = ((char *) block) + blksize;//内存块的空闲位置的起始指针

		chunk = (AllocChunk) (((char *) block) + ALLOC_BLOCKHDRSZ);//进行内存片化
		chunk->aset = set;
		chunk->size = chunk_size;//重置内存片大小
		 
		if (set->blocks != NULL)//如果当前内存上下文的链表中已经有了分配的内存块
		{
			block->prev = set->blocks;//设置当前内存块的前驱
			block->next = set->blocks->next;//设置当前内存块的next
			if (block->next)//如果原本的内存块不止一个
				block->next->prev = block;//将原本内存块的下一个内存块的前驱设置为当前内存块
			set->blocks->next = block;//原本内存块的next设置为当前内存块
			//为什么要将新的内存块放在之前已内存块的后面?
			//这样做能够继续使用已分配的内存块中的空闲空间
		}
		else//没有已分配的内存块
		{
			block->prev = NULL;//前驱设置为NULL
			block->next = NULL;//next设置为NULL
			set->blocks = block;//当前上下文已分配的内存块集合中放入block
		}

		return AllocChunkGetPointer(chunk);//返回新分配的“内存片”
	}

	 //函数执行到这里,说明请求的内存大小使用内存片来存放足以。
	fidx = AllocSetFreeIndex(size);//根据大小确定freelist的序号(具体原理上篇博客讲过,这里不赘述)
	chunk = set->freelist[fidx];//直接将对应freelist中对应大小的空闲内存片分配出去
	if (chunk != NULL)//如果成功获得了内存片
	{
		Assert(chunk->size >= size);//内存片小于所求大小则报错

		set->freelist[fidx] = (AllocChunk) chunk->aset;//关于aset在这里其实是next作用(上篇博客讲过),所以这个赋值是为了将当前使用的内存片从freelist中去除,并将next放回去,维护freelist。

		chunk->aset = (void *) set;//设置当前内存的上下文为set
		return AllocChunkGetPointer(chunk);//返回分配的内存片
	}

	 //没有成功获得内存片,即freelist中没有适合的空闲内存片
	chunk_size = (1 << ALLOC_MINBITS) << fidx;//这个计算将1左移3+fidx位,即通过freelist序号还原所求大小(这里的计算在上篇博客也有详细讲解,这里不多赘述)。

这里你可能会疑惑,已经有了原本的size了为什么还要通过这个序号来还原请求大小?
因为原本的size可能不是2的幂次,而内存片都是2的幂次大小的。而通过这个freelist序号所还原出的大小一定的2的幂次,因此通过还原出的大小来分配内存空间更为合适!

	Assert(chunk_size >= size);//如果还原后的大小小于请求的大小(size)则报错

	 //如果当前上下文存在已分配的内存块
	if ((block = set->blocks) != NULL)
	{
		Size		availspace = block->endptr - block->freeptr;//计算内存块中可用(空闲)空间大小,即空闲空间尾地址减去首地址。

		if (availspace < (chunk_size + ALLOC_CHUNKHDRSZ))//如果内存块中空闲空间大小不符合所需的内存片大小(内存片数据大小加上内存片的头部大小)
		{
			 //即使这种情况,在内存块中仍然可能有一定数量的空闲空间。
			while (availspace >= ((1 << ALLOC_MINBITS) + ALLOC_CHUNKHDRSZ))//只要空闲空间大小大于等于(内存片数据的最小大小和内存片头部信息的和),即可认为这种空闲空间会有作用。
			{
				Size		availchunk = availspace - ALLOC_CHUNKHDRSZ;//将空闲空间大小减去内存片头部信息大小,从而获取可存放内存片数据的大小
				int			a_fidx = AllocSetFreeIndex(availchunk);//将上面计算出的大小转换为freelist序号

但上面这个转换其实是有问题的,在上面的分配内存片中也用到了这个函数,它返回的序号实际上代表大于等于所给参数(大小)的内存片大小,而这里的availchunk是当前内存片中能给出内存片数据的最大大小,因此不能直接使用这个序号。

				
				if (availchunk != ((Size) 1 << (a_fidx + ALLOC_MINBITS)))//如果计算出的大小根据刚才计算出的序号反推出的大小(或者说2的幂次),则需要对序号进行处理
				{
					a_fidx--;//将序号减1即可
					Assert(a_fidx >= 0);//如果序号减去1后小于零,则说明序号出现问题,报错
					availchunk = ((Size) 1 << (a_fidx + ALLOC_MINBITS));//将availchunk进行2的幂次化
				}

				chunk = (AllocChunk) (block->freeptr);//将空闲空间首地址指针指向的内存进行内存片化

				block->freeptr += (availchunk + ALLOC_CHUNKHDRSZ);//空闲指针指向真正空闲的区域(原空闲指针加上分配的内存片大小(二次幂化后)加上内存片的头的大小)
				availspace -= (availchunk + ALLOC_CHUNKHDRSZ);//可用空间减去分配了的空间大小

				chunk->size = availchunk;//将内存片的大小设置为分配的大小
				//将该内存片添加到freelist中
				chunk->aset = (void *) set->freelist[a_fidx];//将内存片的aset(由于空闲等价于next)赋值为freelist中对应序号的空闲内存片
				set->freelist[a_fidx] = chunk;//将freelist对应序号指向该内存片
			}

			//由于没有足够的空间分配,需要创建一个新的内存块
			block = NULL;
		}
	}

	if (block == NULL)//如果需要创建一个新的内存块
	{
		Size		required_size;//需要的大小

		 //就像上篇博客提到的,blksize的赋值为当前上下文nextBlockSize,这个值的初始值为initBlockSize
		blksize = set->nextBlockSize;
		set->nextBlockSize <<= 1;//由于上个内存块不够用,因此这次的内存块分配大小翻倍
		if (set->nextBlockSize > set->maxBlockSize)//如果大小翻倍后超过限制
			set->nextBlockSize = set->maxBlockSize;//设置为最大块大小

		required_size = chunk_size + ALLOC_BLOCKHDRSZ + ALLOC_CHUNKHDRSZ;//计算加上块头部后需求大小
		while (blksize < required_size)//如果块大小小于需要的大小
			blksize <<= 1;//将块大小翻倍(为了保持2的次幂)

		block = (AllocBlock) malloc(blksize);//尝试分配blksize大小的内存
		//之所以是尝试,是因为需求的内存可能会比较大。

		              
		while (block == NULL && blksize > 1024 * 1024)//如果分配内存失败并且块的大小大于1MB
		{
			blksize >>= 1;//将块的大小右移一位,即变为原大小二分之一
			if (blksize < required_size)//如果块大小小于需求的大小
				break;//跳出循环(不可能满足条件)
			block = (AllocBlock) malloc(blksize);//尝试分配内存
		}

		if (block == NULL)//如果分配内存仍然失败,说明条件无法满足
			return NULL;//返回分配失败
		
		//分配成功
		block->aset = set;//设置内存块所属上下文
		block->freeptr = ((char *) block) + ALLOC_BLOCKHDRSZ;//设置空闲空间起始位置为内存块头后面
		block->endptr = ((char *) block) + blksize;//设置空闲空间尾部为指针加块大小

		block->prev = NULL;//由于这是新的块,将放在blocks链表的头部
		block->next = set->blocks;//旧的块将放置在当前块的next
		if (block->next)//如果旧的块存在
			block->next->prev = block;//将旧块的前驱设置为当前块
		set->blocks = block;//将上下文的链表更换为新链表
	}

	chunk = (AllocChunk) (block->freeptr);//直接在空闲位置分配一个内存片(所需的内存片)

	block->freeptr += (chunk_size + ALLOC_CHUNKHDRSZ);//空闲空间起始位置后移(内存片数据大小+内存片头部大小)个字节
	Assert(block->freeptr <= block->endptr);//如果空闲空间起始位置超过尾部,则报错。

	chunk->aset = (void *) set;//设置内存片所属的上下文
	chunk->size = chunk_size;//设置内存片的大小

	return AllocChunkGetPointer(chunk);//返回分配的内存片
}

主要流程:

  1. 首先判断请求的大小是否超过了内存片的阈值,未超过则分配内存片。超过则分配内存块。
  2. 超过了,则会申请一个新的内存块,进行各种初始化操作后,返回新分配的内存上下文的指针。
  3. 未超过,则会继续判断,如果可以从freelist中取得合适的内存片,则设置上下文,并维护freelist后返回该内存片。如果不能从freelist中取得合适的内存片,则要创建一个新的内存块(在这之前,会将队列头部的内存块的空闲空间尽可能的转化成内存片,因为只有队列头部的内存块的空闲空间会被利用,而新建的内存块会被放在头部,导致这个内存块的空闲空间再也不会被使用),然后在新的内存块中取得符合大小的内存片。

内存上下文的释放

关于内存上下文的释放,我在前面提到过三种情况:

  • 释放内存上下文中的所有内存块
  • 重置内存上下文
  • 释放一个内存上下文中指定的内存片

释放内存上下文中所有内存块——AllocSetDelete

static void
AllocSetDelete(MemoryContext context)//要释放的内存上下文
{
	AllocSet	set = (AllocSet) context;//转换为AllocSet类型
	AllocBlock	block = set->blocks;//获取该上下文内存块链表

	AssertArg(AllocSetIsValid(set));//判断是否为正确的AllocSet

	if (set->freeListIndex >= 0)//这说明该块可以放入freelist中,而不必释放
	{
		AllocSetFreeList *freelist = &context_freelists[set->freeListIndex];//取得符合条件的freelist

		 
		if (!context->isReset)//如果不需要重置
			MemoryContextResetOnly(context);


		if (freelist->num_free >= MAX_FREE_CONTEXTS)//如果freelist已经满了
		{
			while (freelist->first_free != NULL)//只要freelist的头节点不为NULL
			{
				AllocSetContext *oldset = freelist->first_free;//获取头节点

				freelist->first_free = (AllocSetContext *) oldset->header.nextchild;//将头节点设置为头节点的next
				freelist->num_free--;//freelist的数量减一

				free(oldset);//释放头节点
			}
		}

		//将freelist清空后,再将该上下文放入freelist中
		set->header.nextchild = (MemoryContext) freelist->first_free;
		freelist->first_free = set;
		freelist->num_free++;

		return;
	}

	while (block != NULL)//如果内存块链表不为空,就一直循环
	{
		AllocBlock	next = block->next;//获取链表头的next,用于下一次循环

		if (block != set->keeper)//如果该块未在keeper中
			free(block);//释放该块

		block = next;//将next赋值给block
	}

	free(set);//释放上下文头部信息
}

分为两种情况,如果可以放入freelist中,则放入freelist而不必释放。否则,将释放当前上下文中的所有内存块,不包括keeper中保留的内存块,并释放内存上下文节点。

释放一个内存上下文中指定的内存片——AllocSetFree

static void
AllocSetFree(MemoryContext context, void *pointer)//传参为内存上下文和要删除的内存片的指针
{
	AllocSet	set = (AllocSet) context;//将context转换为AllocSet类型
	AllocChunk	chunk = AllocPointerGetChunk(pointer);//根据pointer取得内存片

	AllocFreeInfo(set, chunk);

	if (chunk->size > set->allocChunkLimit)//如果内存片的大小大于内存片的阈值,说明该内存片是一个内存块
	{
		AllocBlock	block = (AllocBlock) (((char *) chunk) - ALLOC_BLOCKHDRSZ);//获取内存块头部信息

		 //如果内存块所属的上下文不为传入的上下文
		if (block->aset != set ||
			block->freeptr != block->endptr ||
			block->freeptr != ((char *) block) +
			(chunk->size + ALLOC_BLOCKHDRSZ + ALLOC_CHUNKHDRSZ))
			elog(ERROR, "could not find block containing chunk %p", chunk);//则报错
		
		//由于还有链表,释放前需要先对链表进行删除
		if (block->prev)//如果该块有前驱
			block->prev->next = block->next;//从链表中移除该块,即将前驱的next设置为该块的next(删除中间节点)
		else//如果该块没有前驱
			set->blocks = block->next;//则该块为链表头,将链表头设置为该块的next(删除头节点)
		if (block->next)//如果该块的next不为NULL
			block->next->prev = block->prev;//则将该该块的next的前驱设置为该块的前驱

		free(block);//释放该块内存
	}
	else//否则,为正常的内存片
	{
		int			fidx = AllocSetFreeIndex(chunk->size);//将该内存片放入对应大小的freelist链表中

		chunk->aset = (void *) set->freelist[fidx];//将该内存片的next设置为原链表的头部

		set->freelist[fidx] = chunk;//链表头设置为该节点
	}
}

主要是判断两种情况,第一种是要删除的内存片其实是一个内存块时,直接释放内存块。第二种是正常的内存片,直接装入到freelist链表中,便于下次分配。

重置内存上下文——AllocSetReset
这个函数已经在上面分析过了,这里就不再分析。在进行重置时,内存上下文中除了keeper变量中指定要保留的内存块,其他的内存块全部都要释放,包括空闲链表中的内存。而keeper中指定保留的内存块会被清空内容,使得内存上下文重置后就立刻有一块内存可供使用。

总结

大致了解了postgreSQL对内存上下文的相关操作。

这里有一个容易忽略的点,字节对齐。

字节对齐
计算机中内存大小的基本单位是字节(byte),理论上来讲,可以从任意地址访问某种基本数据类型,但是实际上,计算机并非逐字节大小读写内存,而是以2,4,或8的 倍数的字节块来读写内存,如此一来就会对基本数据类型的合法地址作出一些限制,即它的地址必须是2,4或8的倍数。那么就要求各种数据类型按照一定的规则在空间上排列,这就是对齐。

为什么要字节对齐
最重要的考虑是提高内存系统性能。
计算机每次读写一个字节块,例如,假设计算机总是从内存中取8个字节,如果一个double数据的地址对齐成8的倍数,那么一个内存操作就可以读或者写,但是如果这个double数据的地址没有对齐,数据就可能被放在两个8字节块中,那么我们可能需要执行两次内存访问,才能读写完成。显然在这样的情况下,是低效的。所以需要字节对齐来提高内存系统性能。
在有些处理器中,如果需要未对齐的数据,可能不能够正确工作甚至crash,这里我们不多讨论。

postgreSQL是有自己的字节对齐机制的,以后有时间的话我会去研究一下。

还有比较收益的地方就是关于postgreSQL对空闲内存空间的使用,freelist中只有在队列头的内存块的空闲内存会被分配,而我认为这种机制十分巧妙。下面我讲一下我自己的理解(当然上面已经有详细的分析了):
在freelist头部的内存块一定是最新加入进去的,所以可以认为是当前最活跃的,并且,只访问头部会缩短访问链表的时间,提升分配效率。这时你可能会问了,那剩下的内存块的空闲空间怎么办?前面提到过,当头部的空闲块被放入链表的第二块时,会将该空闲块的空闲空间以内存片的形式转存。而在链表中的所有内存块必然都是从头部挤到后面的,因此空闲空间可以说是得到了比较好的利用,只有少量不能转成内存片的内存会变成碎片。

  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值