2021SC@SDUSC
概述
存储系统分为内存和外存两个部分,postgreSQL的存储管理的主要功能就是内存管理、外存管理、内存外存的交互。由于内存中的内容大都是从外存中取得(表),因此我打算先分析外存管理这一部分。 经由前面的综述可以知道,外存管理的主要部分位于storage这个文件夹中,因此我将先分析这个文件夹。 但这个文件夹中同样有许多文件夹,而在综述的分析中,可以得知storage中的smgr(Storage Managers)这个文件夹存放这存储/磁盘管理的相关代码,也就是外存管理的相关代码。 因此,本次分析将从smgr这个文件夹开始。
源码分析
上图为smgr文件夹中的所有内容,可以看到只有两个c文件。
首先阅读文件夹中的README文件,可知smgr.c这个文件实现的是存储管理器的功能,上层对smgr发送请求,再由smgr调用合适的存储管理器进行处理。md.c这个文件是磁盘管理器,是内核文件系统操作的接口。 所以,我们对于外存的所有分析都要建立在smgr.c这个文件的基础上,根据它的调用去分析各个功能的实现。
打开smgr.c文件,这两行足见其重要性。接下来开始分析smgr.c文件。
typedef struct f_smgr
{
void (*smgr_init) (void);
void (*smgr_shutdown) (void);
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;
Oid dbNode;
Oid relNode;
} 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是链表的节点,会将没有属主的关系链接起来。 然后再看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)
{
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);
}
brnode.node = rnode;
brnode.backend = backend;
reln = (SMgrRelation) hash_search(SMgrRelationHash,
(void *) &brnode,
HASH_ENTER, &found);
if (!found)
{
int forknum;
reln->smgr_owner = NULL;
reln->smgr_targblock = InvalidBlockNumber;
reln->smgr_fsm_nblocks = InvalidBlockNumber;
reln->smgr_vm_nblocks = InvalidBlockNumber;
reln->smgr_which = 0;
for (forknum = 0; forknum <= MAX_FORKNUM; forknum++)
reln->md_num_open_segs[forknum] = 0;
dlist_push_tail(&unowned_relns, &reln->node);
}
return reln;
}
这个smgropen方法非常重要,能够返回一个SMgrRelation,也就是指向关系的指针,或者说表头。在第一次使用这个方法时,会创建一个SMgrRelationHash,即存储所有已有的表头,然后初始化没有属主的链表。然后在哈希表中根据参数查找对应的关系,找到则直接返回表头;未找到则将reln初始化,并将各种类型文件的分段数量都设为0(意思是没有打开这个关系文件),并归入到没有属主的链表中,然后返回reln。 然后就是设置属主、清理属主的方法,检测关系对应文件类型是否存在的方法,关闭文件,关闭哈希表以及关闭节点的方法。 接下来的方法都和md.c这个文件相关,我会在以后分析md.c这个文件的时候顺带详细分析这些方法,现在先简要介绍一下。 smgrcreate方法能够创建关系文件。 smgrread方法能够读取文件的数据。 smgrwrite方法能够向文件写入数据。 smgrwriteback方法是回写,速度更快。
总结
- 虽然postgreSQL只实现了磁盘介质的存储管理器,但是仍然以多介质的情况编写代码,便于未来或者用户的扩展。这种可扩展的思想是很重要的。
- 了解了hook这种用法。
- 动态哈希表具有很好的提速作用。
- 大体上明白了smgr是如何实现存储管理的,其相当于一个接口,供上级进程调用,它也不对磁盘进行操作,通过调用下层的存储管理器对磁盘进行操作。但由于很多地方还没有涉及到,所以还存在很多细节上的疑问。这需要通过以后的分析贯通以后才能慢慢理解。
|