postgreSQL源码分析——存储管理——外存管理(1)

2021SC@SDUSC

概述

存储系统分为内存和外存两个部分,postgreSQL的存储管理的主要功能就是内存管理、外存管理、内存外存的交互。由于内存中的内容大都是从外存中取得(表),因此我打算先分析外存管理这一部分。
经由前面的综述可以知道,外存管理的主要部分位于storage这个文件夹中,因此我将先分析这个文件夹。
但这个文件夹中同样有许多文件夹,而在综述的分析中,可以得知storage中的smgr(Storage Managers)这个文件夹存放这存储/磁盘管理的相关代码,也就是外存管理的相关代码。
因此,本次分析将从smgr这个文件夹开始。

源码分析

在这里插入图片描述
上图为smgr文件夹中的所有内容,可以看到只有两个c文件。

README文件
首先阅读文件夹中的README文件,可知smgr.c这个文件实现的是存储管理器的功能,上层对smgr发送请求,再由smgr调用合适的存储管理器进行处理。md.c这个文件是磁盘管理器,是内核文件系统操作的接口。
所以,我们对于外存的所有分析都要建立在smgr.c这个文件的基础上,根据它的调用去分析各个功能的实现。

在这里插入图片描述
打开smgr.c文件,这两行足见其重要性。接下来开始分析smgr.c文件。

typedef struct f_smgr
{
	void		(*smgr_init) (void);	/* may be NULL */
	void		(*smgr_shutdown) (void);	/* may be NULL */
	void		(*smgr_close) (SMgrRelation reln, ForkNumber forknum);
	void		(*smgr_create) (SMgrRelation reln, ForkNumber forknum,
								bool isRedo);
	bool		(*smgr_exists) (SMgrRelation reln, ForkNumber forknum);
	void		(*smgr_unlink) (RelFileNodeBackend rnode, ForkNumber forknum,
								bool isRedo);
	void		(*smgr_extend) (SMgrRelation reln, ForkNumber forknum,
								BlockNumber blocknum, char *buffer, bool skipFsync);
	void		(*smgr_prefetch) (SMgrRelation reln, ForkNumber forknum,
								  BlockNumber blocknum);
	void		(*smgr_read) (SMgrRelation reln, ForkNumber forknum,
							  BlockNumber blocknum, char *buffer);
	void		(*smgr_write) (SMgrRelation reln, ForkNumber forknum,
							   BlockNumber blocknum, char *buffer, bool skipFsync);
	void		(*smgr_writeback) (SMgrRelation reln, ForkNumber forknum,
								   BlockNumber blocknum, BlockNumber nblocks);
	BlockNumber (*smgr_nblocks) (SMgrRelation reln, ForkNumber forknum);
	void		(*smgr_truncate) (SMgrRelation reln, ForkNumber forknum,
								  BlockNumber nblocks);
	void		(*smgr_immedsync) (SMgrRelation reln, ForkNumber forknum);
} f_smgr;

首先是一个结构体 f_smgr 包含了初始化、启动、停止、关闭、创建、读、写等方法,应该是为了实现各种介质的存储管理器而创建的。可以看到,结构体内的所有函数都采用了函数指针,因此具备了很高的扩展性
根据头部的引用寻找相应的头文件,找到了几个比较关键的数据结构。

typedef struct SMgrRelationData
{
	RelFileNodeBackend smgr_rnode;
	struct SMgrRelationData **smgr_owner;
	BlockNumber smgr_targblock; 
	BlockNumber smgr_fsm_nblocks;	
	BlockNumber smgr_vm_nblocks;	
	int			smgr_which;	
	int			md_num_open_segs[MAX_FORKNUM + 1];
	struct _MdfdVec *md_seg_fds[MAX_FORKNUM + 1];
	dlist_node	node;
} SMgrRelationData;

typedef SMgrRelationData *SMgrRelation;

typedef enum ForkNumber
{
	InvalidForkNumber = -1,
	MAIN_FORKNUM = 0,
	FSM_FORKNUM,
	VISIBILITYMAP_FORKNUM,
	INIT_FORKNUM
} ForkNumber;

typedef struct RelFileNode
{
	Oid			spcNode;		/* tablespace */
	Oid			dbNode;			/* database */
	Oid			relNode;		/* relation */
} RelFileNode;

typedef struct RelFileNodeBackend
{
	RelFileNode node;
	BackendId	backend;
} RelFileNodeBackend;

首先来看RelFileNode这个数据结构,顾名思义,就是关系文件节点,存放三个数据,分别是表空间节点(为关系所在的表空间),数据库空间节点(关系所在的数据库),关系节点(识别具体的关系)。有了这三个数据,就可以物理访问这个关系。
然后再看RelFileNodeBackend,就是在RelFileNode的基础上添加了一个BackendId用于标记后端的oid。
然后再看ForkNumber,是一个枚举类型的结构体(虽然后面几个没有赋值,但是由于枚举的特性,会自动递增赋值),用于定义文件的类型。而postgreSQL中数据表文件有以下三种类型:main负责存储数据、fsm存储main中空闲块的大小,vm则是方便vacuum回收已删除数据的空间。因此,MAIN_FORKNUM为存储实际的数据的文件类型,FSM_FORKNUM为存储空闲位置的文件类型,VISIBILITYMAP_FORKNUM为存储VM文件类型,INIT_FORKNUM用于数据库的初始化。
然后再看SMgrRelationData,为单个关系的文件结构。smgr_rnode是关系的物理标识,同时作为哈希表的key(方便快速查找),smgr_owner是指向表头的指针,暂时还不明白作用,smgr_targblock是写的块的数量,smgr_fsm_nblocks是最近的fsm块的数量,smgr_vm_nblocks是最近的vm块的数量,smgr_which是选择存储管理器(介质),但postgreSQL只实现了一种磁盘存储管理器,因此默认为0,md_num_open_segs[MAX_FORKNUM + 1]是每种类型文件的分段数量,*md_seg_fds[MAX_FORKNUM + 1]是每种类型文件对应的分段数组,node是链表的节点,会将没有属主的关系链接起来。

postgreSQL的表以表文件的形式存储,以表的OID命名。除此之外,每个表还拥有两个附属文件,分别是可见性映射表文件(VM)和空闲空间映射表文件(FSM)。VM用于加快VACUUM(清理)的速度,FSM用于表文件空闲空间的管理。

然后再看SMgrRelation,即为SMgrRelationData的指针,指向对应的关系的文件结构。

在下面使用了这个结构体的常数组来存储各种存储管理器(只实现了一种,磁盘管理器,具体方法都在md.c),以后分析磁盘的管理可以从此处下手。
然后就是一些基本的方法,smgr的初始化、启动和关闭。

	on_proc_exit(smgrshutdown, 0);

初始化函数中,有上面这样一行代码。作者给的注释是说在后端(postgres)进程终止时,这个函数会先hook住,直到smgr完成清理工作(终止函数)后,才继续后端的终止。

SMgrRelation
smgropen(RelFileNode rnode, BackendId backend)
{
	RelFileNodeBackend brnode;
	SMgrRelation reln;
	bool		found;

	if (SMgrRelationHash == NULL)
	{
		/* First time through: initialize the hash table */
		HASHCTL		ctl;

		MemSet(&ctl, 0, sizeof(ctl));
		ctl.keysize = sizeof(RelFileNodeBackend);
		ctl.entrysize = sizeof(SMgrRelationData);
		SMgrRelationHash = hash_create("smgr relation table", 400,
									   &ctl, HASH_ELEM | HASH_BLOBS);
		dlist_init(&unowned_relns);
	}

	/* Look up or create an entry */
	brnode.node = rnode;
	brnode.backend = backend;
	reln = (SMgrRelation) hash_search(SMgrRelationHash,
									  (void *) &brnode,
									  HASH_ENTER, &found);

	/* Initialize it if not present before */
	if (!found)
	{
		int			forknum;

		/* hash_search already filled in the lookup key */
		reln->smgr_owner = NULL;
		reln->smgr_targblock = InvalidBlockNumber;
		reln->smgr_fsm_nblocks = InvalidBlockNumber;
		reln->smgr_vm_nblocks = InvalidBlockNumber;
		reln->smgr_which = 0;	/* we only have md.c at present */

		/* mark it not open */
		for (forknum = 0; forknum <= MAX_FORKNUM; forknum++)
			reln->md_num_open_segs[forknum] = 0;

		/* it has no owner yet */
		dlist_push_tail(&unowned_relns, &reln->node);
	}

	return reln;
}

这个smgropen方法非常重要,能够返回一个SMgrRelation,也就是指向关系的指针,或者说表头。在第一次使用这个方法时,会创建一个SMgrRelationHash,即关系哈希表,能够存储所有已有的表头,然后初始化没有属主的链表,然后在哈希表中根据参数查找对应的关系,找到则直接返回表头;未找到则将reln初始化,并将各种类型文件的分段数量都设为0(意思是没有打开这个关系文件),并归入到没有属主的链表中,然后返回reln。
然后就是设置属主、清理属主的方法,检测关系对应文件类型是否存在的方法,关闭文件,关闭哈希表以及关闭节点的方法。
接下来的方法都和md.c这个文件相关,我会在以后分析md.c这个文件的时候顺带详细分析这些方法,现在先简要介绍一下。
smgrcreate方法能够创建关系文件。
smgrread方法能够读取文件的数据。
smgrwrite方法能够向文件写入数据。
smgrwriteback方法是回写,速度更快。

总结

  1. 虽然postgreSQL只实现了磁盘介质的存储管理器,但是仍然以多介质的情况编写代码,便于未来或者用户的扩展。这种可扩展的思想是很重要的。
  2. 了解了hook这种用法。
  3. 动态哈希表具有很好的提速作用。
  4. 大体上明白了smgr是如何实现存储管理的,其相当于一个接口,供上级进程调用,它也不对磁盘进行操作,通过调用下层的存储管理器对磁盘进行操作。但由于很多地方还没有涉及到,所以还存在很多细节上的疑问。这需要通过以后的分析贯通以后才能慢慢理解。
  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值