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

2021SC@SDUSC

概述

上两篇博客分析完了关于Cache的相关内容,这篇博客就来分析一下postgreSQL关于缓冲池的内容。

如果需要访问的系统表元组在Cache中无法找到或者需要访问普通表的元组,就需要对缓冲池进行访问。任何对于表、元组、索引表等的操作都在缓冲池进行,缓冲池的数据调度都以磁盘块为单位,需要访问的数据以磁盘块为单位,需要访问的数据以磁盘块为单位写入缓冲池,也有将缓冲池数据写回磁盘的操作。调入缓冲池中的磁盘块成为缓冲区、缓冲块或者页面,多个缓冲区组成缓冲池。

PostgreSQL有两种缓冲池——共享缓冲池和本地缓冲池。共享缓冲池主要用作普通可共享表的操作场所。本地缓冲池用作仅本地可见的临时表的操作场所。

源码分析

共享缓冲池

共享缓冲池位于共享内存区域,在系统启动时就要对其进行初始化操作。

共享缓冲池的初始化

InitBufferPool函数——初始化共享缓冲池

源码位于src/backend/storage/buffer/buf_init.c文件中。

void
InitBufferPool(void)
{
	bool		foundBufs,
				foundDescs,
				foundIOLocks,
				foundBufCkpt;
		
	//这一行不在这个函数里,我放进来只是方便看关于BufferDesciptors的数据类型
	BufferDescPadded *BufferDescriptors; 
	//这是一个BufferDescPadded类型的全局数组,用来管理缓冲池中的缓冲区,这个数据类型我会在下面分析
	//这个数组的元素个数就是缓冲池中缓冲区的总数,用于管理所有缓冲区的描述符,也可以认为这个数组就是共享缓冲池
	//建议先看完下面关于这个数据结构的分析再来看对函数的分析
	BufferDescriptors = (BufferDescPadded *)
		ShmemInitStruct("Buffer Descriptors",
						NBuffers * sizeof(BufferDescPadded),
						&foundDescs);
	//这也是一个全局指针变量,用于存储缓冲池的起始地址
	BufferBlocks = (char *)
		ShmemInitStruct("Buffer Blocks",
						NBuffers * (Size) BLCKSZ, &foundBufs);

	//这是IO相关的锁
	BufferIOLWLockArray = (LWLockMinimallyPadded *)
		ShmemInitStruct("Buffer IO Locks",
						NBuffers * (Size) sizeof(LWLockMinimallyPadded),
						&foundIOLocks);
	
	LWLockRegisterTranche(LWTRANCHE_BUFFER_IO_IN_PROGRESS, "buffer_io");
	LWLockRegisterTranche(LWTRANCHE_BUFFER_CONTENT, "buffer_content");

	 //用于对to-be-checkpointed缓冲区序号排序的数组位于共享内存中,这是为了避免在运行时分配大量的内存。
	CkptBufferIds = (CkptSortItem *)
		ShmemInitStruct("Checkpoint BufferIds",
						NBuffers * sizeof(CkptSortItem), &foundBufCkpt);
	//判断缓冲区是否已存在
	if (foundDescs || foundBufs || foundIOLocks || foundBufCkpt)
	{
		Assert(foundDescs && foundBufs && foundIOLocks && foundBufCkpt);
	}
	else
	{//缓冲区未存在
		//循环变量
		int			i;
		//初始化所有缓冲区的头
		for (i = 0; i < NBuffers; i++)
		{	
			//初始化缓冲区描述符的地址
			BufferDesc *buf = GetBufferDescriptor(i);
			//初始化缓冲区描述符的tag属性
			CLEAR_BUFFERTAG(buf->tag);
			//初始化缓冲区描述符的state属性
			pg_atomic_init_u32(&buf->state, 0);
			//初始化缓冲区描述符的wait_backend_pid属性
			buf->wait_backend_pid = 0;
			//初始化缓冲区描述符的buf_id属性
			buf->buf_id = i;
			//最初将所有缓冲区链接在一起都作为FreeList(因为都没有被使用)。 此列表的后续管理由 freelist.c 完成
			buf->freeNext = i + 1;
			//初始化content_lock,当进程访问缓冲块时,会在content_lock上加锁,读时加LW_SHARE锁,写时加LW_EXCLUSIVE锁。防止因多个进程对缓冲区访问的冲突而造成数据不一致。
			LWLockInitialize(BufferDescriptorGetContentLock(buf),
							 LWTRANCHE_BUFFER_CONTENT);
			//初始化io_in_progress_lock,该锁用于缓冲区和磁盘进行I/O操作时,请求这样的缓冲区需要等到I/O结束。
			LWLockInitialize(BufferDescriptorGetIOLock(buf),
							 LWTRANCHE_BUFFER_IO_IN_PROGRESS);
		}

		//更正FreeList链表的最后一个条目(即标记为链表的结尾)
		GetBufferDescriptor(NBuffers - 1)->freeNext = FREENEXT_END_OF_LIST;
	}

	//初始化其他共享缓冲区管理的条目
	StrategyInitialize(!foundDescs);

	//初始化每个后端文件的刷新上下文
	WritebackContextInit(&BackendWritebackContext,
						 &backend_flush_after);
}

关于缓冲区数组的长度

int			NBuffers = 1000;

可以在源码中找到,这是由postgreSQL定义的全局常量,为1000。
该函数的大致流程:

开始
分配一系列内存
缓冲区已存在?
初始化数组中的每个BufferDesc
调用StrategyInitialize函数初始化其他共享缓冲区管理的条目
结束
BufferDescPadded结构体——对齐的缓冲区描述符
typedef union BufferDescPadded
{
	//真正的缓冲区描述符
	BufferDesc	bufferdesc;
	char		pad[BUFFERDESC_PAD_TO_SIZE];
} BufferDescPadded;

可以看到这个结构体中有一个BufferDesc才是真正的缓冲区描述符。那这个结构体的意义是什么?
根据注释“事实证明,如果缓存行对齐,并发访问缓冲区头会更有效。 因此,强制 BufferDescriptors 数组的开头位于缓存行边界上,并强制元素为缓存行大小”。可以知道这个结构体的作用就是为了实现缓存行对齐。
再来看一下真正的缓冲区描述符的结构体:

typedef struct BufferDesc
{
	BufferTag	tag;			//缓冲区页面ID,指明了该缓冲区对应表块的物理信息
	int			buf_id;			//缓冲区的索引号(从0开始)
	//state这里是一个结构体,即当前缓冲区的状态,包含了标志位(缓冲区是否为脏),引用数,以及用于缓冲区替换的最近缓冲区的使用次数
	pg_atomic_uint32 state;
	int			wait_backend_pid;	//用于记录等待所有pin结束的进程PID
	//等待pin归0的目的就是要修改该缓冲区,这里可以理解为记录请求修改该缓冲区的进程号。
	
	int			freeNext;		//用于链接FreeList(和前面分析过的的FreeList类似)
	LWLock		content_lock;	//进程对缓冲区内容访问时加锁
} BufferDesc;

这里出现了两个操作,pin和lock,这是对于缓冲池中缓冲区的两种管理机制。lock就是读写锁,无需多言,我介绍一下pin。

pin
实际就是缓冲区的访问计数器。当进程要访问缓冲区前,对缓冲区加pin,pin的数目保存在上面的state里的引用数中。当引用数不为0时表明有进程正在访问缓冲区,此时缓冲区不能被替换。

这个pin的引用计数很像我最近在写的操作系统课设的文件系统中的引用计数,如果要删除的文件的引用计数不为0(即仍有进程打开该文件),则不能立刻从磁盘上删除该文件,等待引用数归零以后,会自动删除该文件。有异曲同工之妙。
举个例子,当有进程需要删除的元组所在的缓冲区有其他进程访问,即state中的引用数不为0,那现在该进程就不能修改缓冲区,即删除元组。因此系统将该进程的id记录下来,然后对该缓冲区加pin,并阻塞该进程,等待该缓冲区的引用数为1时(最后一个使用该缓冲区的进程释放缓冲区时),会向该缓冲区记录的进程的id发送消息,将其唤醒后修改缓冲区。

在共享缓冲池中查询缓冲区

前面提到过,一个缓冲池是由多个缓冲区组成的。为了在共享缓冲池中快速查找缓冲区,在初始化缓冲池阶段系统为其在共享内存中创建了Hash表。上面的缓冲区描述符中有BufferTag这样一个数据类型:

typedef struct buftag
{
	RelFileNode rnode;			//物理表的OID,由表所在的表空间OID、数据库OID以及表本身的OID构成
	ForkNumber	forkNum;		//枚举类型,标记缓冲区中是什么类型的文件块
	BlockNumber blockNum;		//块号
} BufferTag;

由于其包含一个缓冲块的物理信息,因此具有唯一性,可以当作Hash表的索引。

ReadBuffer_common函数——读取缓冲区

源码位于src/backend/storage/buffer/bufmgr.c文件中。

static Buffer
ReadBuffer_common(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
				  BlockNumber blockNum, ReadBufferMode mode,
				  BufferAccessStrategy strategy, bool *hit)
//smgr是物理表描述符
{	
	//存放找到的缓冲区描述符
	BufferDesc *bufHdr;
	//存放读取的缓冲区内容
	Block		bufBlock;
	//存放是否找到
	bool		found;
	//判断是否扩展
	bool		isExtend;
	//用于区分是共享缓冲池还是本地缓冲池
	bool		isLocalBuf = SmgrIsTemp(smgr);
	//传出是否命中
	*hit = false;

	//确保有足够的空间用来保存缓冲区的pin
	ResourceOwnerEnlargeBuffers(CurrentResourceOwner);
	
	isExtend = (blockNum == P_NEW);
	//表明对缓冲池的查找操作开始
	TRACE_POSTGRESQL_BUFFER_READ_START(forkNum, blockNum,
									   smgr->smgr_rnode.node.spcNode,
									   smgr->smgr_rnode.node.dbNode,
									   smgr->smgr_rnode.node.relNode,
									   smgr->smgr_rnode.backend,
									   isExtend);

	//如果需要扩展则替换正确的块号
	if (isExtend)
		blockNum = smgrnblocks(smgr, forkNum);
	//如果对本地缓冲池进行查找
	if (isLocalBuf)
	{
		//调用LocalBufferAlloc获得本地缓冲区
		bufHdr = LocalBufferAlloc(smgr, forkNum, blockNum, &found);
		if (found)
			pgBufferUsage.local_blks_hit++;
		else if (isExtend)
			pgBufferUsage.local_blks_written++;
		else if (mode == RBM_NORMAL || mode == RBM_NORMAL_NO_LOG ||
				 mode == RBM_ZERO_ON_ERROR)
			pgBufferUsage.local_blks_read++;
	}
	else
	{//如果对共享缓冲池进行查找
		
		 //调用BufferAlloc函数获得共享缓冲区
		bufHdr = BufferAlloc(smgr, relpersistence, forkNum, blockNum,
							 strategy, &found);
		if (found)
			pgBufferUsage.shared_blks_hit++;
		else if (isExtend)
			pgBufferUsage.shared_blks_written++;
		else if (mode == RBM_NORMAL || mode == RBM_NORMAL_NO_LOG ||
				 mode == RBM_ZERO_ON_ERROR)
			pgBufferUsage.shared_blks_read++;
	}

	//执行到这里不会持有任何锁

	//如果缓冲区已经在缓冲池中了,则表示查找到了
	if (found)
	{
		if (!isExtend)
		{//如果不需要扩展
			//函数退出前更新一下状态
			*hit = true;
			VacuumPageHit++;
			if (VacuumCostActive)
				VacuumCostBalance += VacuumCostPageHit;
			//表明在缓冲池中读取缓冲区已经完成
			TRACE_POSTGRESQL_BUFFER_READ_DONE(forkNum, blockNum,
											  smgr->smgr_rnode.node.spcNode,
											  smgr->smgr_rnode.node.dbNode,
											  smgr->smgr_rnode.node.relNode,
											  smgr->smgr_rnode.backend,
											  isExtend,
											  found);

			if (!isLocalBuf)
			{//如果查找的是共享缓冲池
				if (mode == RBM_ZERO_AND_LOCK)//并且处于RBM_ZERO_AND_LOCK模式
				//这个模式下,调用函数希望缓存区页面在返回时是被锁定的
					//给该缓冲区加上写锁
					LWLockAcquire(BufferDescriptorGetContentLock(bufHdr),
								  LW_EXCLUSIVE);
				else if (mode == RBM_ZERO_AND_CLEANUP_LOCK)
					LockBufferForCleanup(BufferDescriptorGetBuffer(bufHdr));
			}
			//返回找到的缓冲区头(描述符)
			return BufferDescriptorGetBuffer(bufHdr);
		}
	}
}
BufferAlloc函数——获取缓冲区

在上面的ReadBuffer_common函数中,会调用函数BufferAlloc函数来获得指定的共享缓存区,但该函数的功能不止于此,它还是执行缓存区替换策略的核心函数。
如果没有找到缓存区,它会选择一个替换牺牲的缓存区将其逐出缓冲池。

static BufferDesc *
BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
			BlockNumber blockNum,
			BufferAccessStrategy strategy,
			bool *foundPtr)
{
	BufferTag	newTag;			//请求的缓冲区的标签
	uint32		newHash;		//上面标签的哈希值
	LWLock	   *newPartitionLock;	//分区锁
	BufferTag	oldTag;			//被替换的缓冲区的标签
	uint32		oldHash;		//上面标签的哈希值
	LWLock	   *oldPartitionLock;	//被替换的缓冲区的分区锁
	uint32		oldFlags;	//被替换的缓冲区的标志位
	int			buf_id;		//缓冲区索引号
	BufferDesc *buf;	//缓冲区描述符
	bool		valid;		//缓冲区的数据是否正确加载
	uint32		buf_state;	//缓冲区状态

	//初始化请求缓冲区的标签,从而可以开始查找
	INIT_BUFFERTAG(newTag, smgr->smgr_rnode.node, forkNum, blockNum);

	//利用标签计算哈希值
	newHash = BufTableHashCode(&newTag);
	//利用哈希值获取分区锁
	newPartitionLock = BufMappingPartitionLock(newHash);

	//因为要读缓冲区,获取共享锁
	LWLockAcquire(newPartitionLock, LW_SHARED);
	//在缓冲池中查找该缓冲区,看是否找到
	buf_id = BufTableLookup(&newTag, newHash);
	if (buf_id >= 0)
	{//缓冲区的索引>=0,说明成功找到
		
		 //获取缓冲区的描述符
		buf = GetBufferDescriptor(buf_id);
		//判断该缓冲区的数据是否已经加载进来,并进行pin操作,防止其他进程从缓冲池中对其进行操作
		valid = PinBuffer(buf, strategy);

		//因为已经进行了pin操作,可以释放分区锁了
		LWLockRelease(newPartitionLock);
		//传出查找成功
		*foundPtr = true;

		if (!valid)
		{//如果数据没有加载进来
		//这可能是由两种情况导致的,可能有其他进程仍在读取该缓冲区;也可能上面的读取操作失败。

			if (StartBufferIO(buf, true))
			{
				//无论如何,必须等待任何活动的读取尝试完成,然后如果页面仍然不是 BM_VALID,则设置进程	
				//自己的读取尝试。 StartBufferIO函数会进行这些操作。
			
				 //如果执行到了这里,说明之前的读取缓冲区的尝试失败了,但是还要继续尝试读取
				 //传出没有找到
				*foundPtr = false;
			}
		}
		//返回缓冲区描述符
		return buf;
	}

	 //此时没有在缓冲池中找到要找的缓冲区,需要初始化该缓冲区,因此要释放分区锁。
	LWLockRelease(newPartitionLock);

	//无限循环,直到找到被替换的缓冲区
	for (;;)
	{
		 //确保在没有拿到自旋锁的时候,有空闲的引用计数器
		ReservePrivateRefCountEntry();

		 //选出被替换的缓冲区
		buf = StrategyGetBuffer(strategy, &buf_state);

		Assert(BUF_STATE_GET_REFCOUNT(buf_state) == 0);

		//持有自旋锁时,将其标志位获取
		oldFlags = buf_state & BUF_FLAG_MASK;

		//将该缓冲区pin住,并释放自旋锁
		PinBuffer_Locked(buf);

		 //如果缓冲区脏位为真(即被修改了),应将其写回到磁盘上。
		if (oldFlags & BM_DIRTY)
		{
//需要对缓冲区内容使用共享锁以将其写出(否则可能会写入无效数据,例如因为其他进程在该进程写入时正在压缩页面内容)。必须在这里使用
//条件锁获取来避免死锁。即使在 StrategyGetBuffer 返回缓冲区时没有固定(因此肯定没有锁定)缓冲区,但到执行到这里时,其他进程可能
//已经pin并获取了该缓存区的排他锁。如果尝试无条件获取锁,会阻塞等待其他进程;如果其他进程稍后阻塞等待该进程,就会发生死锁。

			//获取共享锁
			if (LWLockConditionalAcquire(BufferDescriptorGetContentLock(buf),
										 LW_SHARED))
			{
				//strategy为NULL表示默认的替换策略,不为NULL说明采用非默认的替换策略
				if (strategy != NULL)
				{
					XLogRecPtr	lsn;

					//获取缓存区头部信息的锁
					buf_state = LockBufHdr(buf);
					//读取LSN
					lsn = BufferGetLSN(buf);
					//释放缓存区头部信息的锁
					UnlockBufHdr(buf, buf_state);

					if (XLogNeedsFlush(lsn) &&
						StrategyRejectBuffer(strategy, buf))
					{//如果该策略决定该缓存区不合适,将继续选择下一个被替换的缓存区
						//释放锁
						LWLockRelease(BufferDescriptorGetContentLock(buf));
						//取消Pin
						UnpinBuffer(buf, true);
						//继续下一次循环
						continue;
					}
				}

				//如果该缓存区符合替换策略,将会开始写操作(如果为脏的话)
				TRACE_POSTGRESQL_BUFFER_WRITE_DIRTY_START(forkNum, blockNum,
														  smgr->smgr_rnode.node.spcNode,
														  smgr->smgr_rnode.node.dbNode,
														  smgr->smgr_rnode.node.relNode);
				//清空该缓存区的内容
				FlushBuffer(buf, NULL);
				//释放锁
				LWLockRelease(BufferDescriptorGetContentLock(buf));
				//将该缓存区写回
				ScheduleBufferTagForWriteback(&BackendWritebackContext,
											  &buf->tag);
				//表明缓存区的写操作完成
				TRACE_POSTGRESQL_BUFFER_WRITE_DIRTY_DONE(forkNum, blockNum,
														 smgr->smgr_rnode.node.spcNode,
														 smgr->smgr_rnode.node.dbNode,
														 smgr->smgr_rnode.node.relNode);
			}
			else
			{	//没有拿到共享锁
				 //进入这里说明缓存区被其他进程加锁了,所以放弃该缓存区,继续循环去找其他缓存区
				UnpinBuffer(buf, true);
				continue;
			}
		}

		//要更改有效缓冲区的关联,需要对旧映射分区和新映射分区都有排他锁。
		if (oldFlags & BM_TAG_VALID)
		{
			//获取被替换(旧,以后都用旧代替)缓存区的标签
			oldTag = buf->tag;
			//利用标签计算出哈希值
			oldHash = BufTableHashCode(&oldTag);
			//获取分区锁
			oldPartitionLock = BufMappingPartitionLock(oldHash);

			 //要先给编号较小的分区上锁,再给编号较大的分区上锁,避免死锁
			if (oldPartitionLock < newPartitionLock)
			{//如果旧分区编号较小
				//给旧分区上排他锁
				LWLockAcquire(oldPartitionLock, LW_EXCLUSIVE);
				//给新分区上排他锁
				LWLockAcquire(newPartitionLock, LW_EXCLUSIVE);
			}
			else if (oldPartitionLock > newPartitionLock)
			{//如果新分区编号较小
				//给新分区上排他锁
				LWLockAcquire(newPartitionLock, LW_EXCLUSIVE);
				//给旧分区上排他锁
				LWLockAcquire(oldPartitionLock, LW_EXCLUSIVE);
			}
			else
			{//这种情况下新旧分区相等,也就是只有一个分区
				//只上一个锁即可
				LWLockAcquire(newPartitionLock, LW_EXCLUSIVE);
			}
		}
		else
		{//如果旧缓存区的数据无效
			//只需要给新缓存区上锁
			LWLockAcquire(newPartitionLock, LW_EXCLUSIVE);
			//将旧分区锁标记为NULL
			oldPartitionLock = NULL;
			//将旧分区的Hash值置为0标记没有旧分区
			oldHash = 0;
		}

		//尝试在新标签条件下为缓冲区创建一个哈希表索引
		buf_id = BufTableInsert(&newTag, newHash, buf->buf_id);

		if (buf_id >= 0)
		{//如果索引创建失败(返回-1才表示索引创建成功)
			
			 //此时说明有其他进程已经将装入文件块的缓冲区加入到了Hash表中。
			 //这意味着请求的文件块已经被其他进程载入到缓冲池中(不在该缓冲区),因此没有必要重复载入该文件块。
			 //取消pin
			UnpinBuffer(buf, true);

			//现在可以放弃分区锁
			if (oldPartitionLock != NULL &&
				oldPartitionLock != newPartitionLock)//如果旧分区锁不为空且不为新分区锁
				LWLockRelease(oldPartitionLock);//释放旧分区锁

			//获取新分区的描述符
			buf = GetBufferDescriptor(buf_id);
			//pin住新分区
			valid = PinBuffer(buf, strategy);
			//由于已经对新分区pin了,因此可以释放分区锁
			LWLockRelease(newPartitionLock);
			//标记已经找到
			*foundPtr = true;

			if (!valid)
			{//数据无效
				
				//和前面一样,意味着有进程在读该分区或者上面的读尝试失败了
				if (StartBufferIO(buf, true))
				{
					//传出未找到
					*foundPtr = false;
				}
			}
			//返回缓冲区描述符
			return buf;
		}

		 //因要修改标签,需要对缓冲区头部上锁
		buf_state = LockBufHdr(buf);

		//在该进程进行 I/O 和创建新的哈希表条目时,其他进程可能已经固定或重新弄脏了缓冲区。如果是这样,该进程就不能回收这个缓冲区;必须撤消该进程所做的一切,并使用新的受害者缓冲区重新开始。
		oldFlags = buf_state & BUF_FLAG_MASK;
		if (BUF_STATE_GET_REFCOUNT(buf_state) == 1 && !(oldFlags & BM_DIRTY))
			break;

		UnlockBufHdr(buf, buf_state);
		BufTableDelete(&newTag, newHash);
		if (oldPartitionLock != NULL &&
			oldPartitionLock != newPartitionLock)
			LWLockRelease(oldPartitionLock);
		LWLockRelease(newPartitionLock);
		UnpinBuffer(buf, true);
	}

	 //执行到这里,说明旧缓存区已经成功替换掉了。
	 
	 //将缓存区的标签置为新缓存区的标签
	buf->tag = newTag;
	//设置状态
	buf_state &= ~(BM_VALID | BM_DIRTY | BM_JUST_DIRTIED |
				   BM_CHECKPOINT_NEEDED | BM_IO_ERROR | BM_PERMANENT |
				   BUF_USAGECOUNT_MASK);
	if (relpersistence == RELPERSISTENCE_PERMANENT || forkNum == INIT_FORKNUM)
		buf_state |= BM_TAG_VALID | BM_PERMANENT | BUF_USAGECOUNT_ONE;
	else
		buf_state |= BM_TAG_VALID | BUF_USAGECOUNT_ONE;
	//释放头部锁
	UnlockBufHdr(buf, buf_state);
	//如果旧缓存区的锁不为NULL
	if (oldPartitionLock != NULL)
	{
		//从哈希表中删除旧缓存区
		BufTableDelete(&oldTag, oldHash);
		//如果旧缓存区的锁不为新缓存区的锁
		if (oldPartitionLock != newPartitionLock)
			LWLockRelease(oldPartitionLock);//释放旧缓存区的锁
	}
	//释放新缓存区的锁
	LWLockRelease(newPartitionLock);

	//但当前缓冲区的内容还是无效的,需要读入
	//尝试获取 io_in_progress 锁
	if (StartBufferIO(buf, true))
		//传出未找到
		*foundPtr = false;
	else//此时说明其他进程已经读取了该缓冲区内容
		//直接传出找到了
		*foundPtr = true;
	//返回缓冲区描述符
	return buf;
}

这个函数比较复杂,细节部分已经在代码中分析完了,下面就用一个流程图来描述一下总的过程:

开始
查找Hash表
在Hash表中找到?
对缓冲区pin
缓冲区正在进行I/O?
等待I/O结束
返回缓冲区
使用替换机制得到一个缓冲区
缓冲区为脏?
该缓冲区没有排他锁?
将缓冲区写回磁盘
插入到Hash表中,并返回索引
索引是否大于等于0
将得到的缓冲区从Hash表中删除
申请的块被其他进程占用?
初始化得到的缓存区
返回缓冲区

共享缓冲区替换策略

上面的函数已经涵盖了缓冲区替换的过程,这里就分析一下缓冲区的替换策略,也就是如何选择被替换掉的缓冲区。
因为前面提到过,共享缓冲区的大小为1000,像物理内存一样是有限的,这就意味着会被用光,也就需要替换策略来将最近不使用的缓存区换下,从而载入新的文件块。

FreeList 空闲列表置换

前面源码分析过,缓冲池中其实维护了一个FreeList,记录空闲的缓冲区,因此如果能在FreeList中找到空闲缓冲区,那就无需执行其他替换策略,直接将该空闲缓冲区拿来使用即可。
当一个缓冲区变为空闲时(引用数为0),会被放入FreeList的链表尾部,而从链表中取空闲区时,只需要从链表头部获取即可。(这样做的时间复杂度最低,都是O(1))

Clock-sweep 算法

该算法的核心代码位于StrategyGetBuffer这个函数中,由于该函数比较长,我只截取算法的核心部分放在这里。

BufferDesc *
StrategyGetBuffer(BufferAccessStrategy strategy, uint32 *buf_state)
{
	//这里被略去的过程是在FreeList中寻找空闲缓冲区,没有找到的情况下才会进入到下面的clock-sweep算法	
	
	//初始化tryCounter为NBuffers即1000
	trycounter = NBuffers;//tryCounter就是一个计数器,用于防止无穷循环
	for (;;)//循环区域
	{
		//获取缓冲区描述符
		buf = GetBufferDescriptor(ClockSweepTick());

		 //如果当前缓冲区已经被其他进程pin了或者引用计数不为0,说明不能使用它,需要继续找下一个缓冲区
		local_buf_state = LockBufHdr(buf);

		if (BUF_STATE_GET_REFCOUNT(local_buf_state) == 0)
		{//如果引用计数为0
			if (BUF_STATE_GET_USAGECOUNT(local_buf_state) != 0)
			{//如果usage_count不为0
				//将usage_count减去1
				local_buf_state -= BUF_USAGECOUNT_ONE;
				//重置trycounter为NBuffers
				trycounter = NBuffers;
			}
			else//如果usage_count为0
			{
				//此时说明找到了缓冲区
				if (strategy != NULL)//如果不采用默认策略(即clock-sweep)
					AddBufferToRing(strategy, buf);//则使用环策略
				//设置状态
				*buf_state = local_buf_state;
				//返回找到的缓冲区
				return buf;
			}
		}
		else if (--trycounter == 0)
		{//引用计数不为0则继续循环,将循环计数器-1(如果减去1后为0,说明到达了循环上限)
			
			 //此时已经遍历了所有的缓冲区,所以所有的缓冲区都是被pin了的
			 //所以现在比起继续坚持无穷循环,等待其他进程释放缓冲区,不如返回一个替换失败来解放CPU
			UnlockBufHdr(buf, local_buf_state);
			elog(ERROR, "no unpinned buffers available");
		}
		//释放缓冲区头部锁
		UnlockBufHdr(buf, local_buf_state);
	}
}

这个算法并不意味着一定能找到可替换的缓冲区,就像函数最后部分那样,存在计数器归0,抛出错误的情况。但好在这种情况发生的改了是非常低的,因此这种替换策略可以有效地获取到一个最近未被使用的缓冲区。

引用自官网
该图就是对算法的一个演示:

  1. 在1)中,nextVictimBuffer 指向buffer_id=1的描述符;但是深蓝色表示该缓冲区被pin住了,因此跳过该缓冲区去找下一个。
  2. 在2)中,nextVictimBuffer 指向buffer_id=2的描述符;该缓冲区没有被pin住,但是上面的usage_count为2,因此将其减1后并将nextVictimBuffer 指向下一个。
  3. 在3)中,nextVictimBuffer 指向buffer_id=3的描述符;该缓冲区没有被pin住,并且usage_count为0,因此该缓冲区被选中为替换受害者。

总结

通过分析源码,了解了postgreSQL对缓冲池的管理。
总的来讲,这一次分析的内容和操作系统非常相似,共享缓冲区的替换就像物理内存页的替换,但相比我做的课设来说,这里考虑到的情况要复杂的多,包含各种锁操作,以及对死锁的处理,都是我之前没有考虑过的问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值