PostgreSQL Bootstrap Explanation. -Bootstrap & BKI 详解

通常,多数的文章对于PostgreSQL系统在初始化过程中的了解多为:其使用bki类型的文件作为模版并将pg_xxx.h的头文件作为相应的catalog。但是文章中并未详细阐述系统在初始化过程中是如和完成bki文件到具体catalog的创建,初始数据的插入以及当我们在系统初始化时候,此时对于bki脚本文件这种类似于sql的脚本文件是对其进行解析和执行的。因为在执行initdb时候,存储引擎和查询引擎并未准备就绪,即便是存储引擎和查询引擎准备就绪,存储引擎和查询引擎也属于postgres进程,我们亦无法在initdb进程中直接使用postgres进程中的相关功能,那么我们又是以什么样的方式来使得initdb与postgres两进程之间的通信呢?

从PostgreSQL初始化的过程中,我们可以知道,此时的我们的存储引擎和查询引擎并未完成初始化和启动,那么在这bootstrap过程中我们又是完成对于bki脚本文件的解析和执行的呢?下面我们就来详细分析一下PostgreSQL的bootstrap机制。

我们知道,系统表是各个数据库中的最重要的元数据信息,系统表数据的丢失或者损害将可能导致整个系统不能正常启动或者正常的工作。通常,我们在使用initdb程序来创建一个初始的数据库库模版。在initdb的执行过程中将会创建PostgreSQL在执行过程中所需要的元数据表:pg_class, pg_am等元数据表。 那么很少人会思考,在initdb的过程中,我们需要做些什么样的工作?需要初始化并启动存储引擎和查询引擎吗?是否需要完成对于WAL以及buffer的模块的初始化和启动工作呢?

我们知道在src/include/catalog文件中存在大量的pg_xxxx.h形式的头文件,同时我们由这些头文件我们可以看出,这些.h的头文件正是我们在PostgreSQL中的catalog。例如:对于PostgreSQL中的保存系统中所有对象的系统表pg_class来说,我们从pg_class.h文件中可以看出, 其不但给出类该系统表的oid值,且给出了pg_class该系统表的结构信息,例如:系统表中每个属性的名称及类型,以及相应的初始数据信息。 下面我们就来详细的看看pg_class.h中的具体代码。

由pg_class.h中,我们可以看出,首先系统给出了pg_class系统表所具有的oid值,1259. 此时,可能读者就会想,我是否可以随便设置一个数字来表述pg_class的oid值,答案是,可以的。理论上,我们可以无原则的随便设置一个数字作为pg_class的oid值,但是理论比较是理论上的,在实际的操作过程并非无原则的。因为对于oid值,理论上也是有限的。我们无原则和无规律的使用最终会导致oid的枯竭,为了减少这种情况的发生。假使我们需要创建一个新的系统元数据表(在下面我们将给出详细介绍),我们可以使用 src/include/catalog目录下 unused_oids 来获取下一个未使用的oid或者使用 duplicate_oids 来测试是否存在着重复的oid值。

#define RelationRelationId 1259
#define RelationRelation_Rowtype_Id 83

在获得一有效的oid值后, 下面由CATALOG宏定义来给出关于该系统表的结构描述。 例如对于本例pg_class来说:

CATALOG(pg_class,1259) BKI_BOOTSTRAP BKI_ROWTYPE_OID(83) BKI_SCHEMA_MACRO
CATALOG(pg_class,1259) BKI_BOOTSTRAP BKI_ROWTYPE_OID(83) BKI_SCHEMA_MACRO
{
NameData relname; /* class name */
Oid relnamespace; /* OID of namespace containing this class */
Oid reltype; /* OID of entry in pg_type for table’s * implicit row type */
Oid reloftype; /* OID of entry in pg_type for underlying * composite type */
Oid relowner; /* class owner */
Oid relam; /* index access method; 0 if not an index */
Oid relfilenode; /* identifier of physical storage file */
/* relfilenode == 0 means it is a “mapped” relation, see relmapper.c */
Oid reltablespace; /* identifier of table space for relation */
int32 relpages; /* # of blocks (not always up-to-date) */
float4 reltuples; /* # of tuples (not always up-to-date) */
int32 relallvisible; /* # of all-visible blocks (not always * up-to-date) */
Oid reltoastrelid; /* OID of toast table; 0 if none */
bool relhasindex; /* T if has (or has had) any indexes */
bool relisshared; /* T if shared across databases */
char relpersistence; /* see RELPERSISTENCE_xxx constants below */
char relkind; /* see RELKIND_xxx constants below */
int16 relnatts; /* number of user attributes *//*
* Class pg_attribute must contain exactly “relnatts” user attributes
* (with attnums ranging from 1 to relnatts) for this class. It may also
* contain entries with negative attnums for system attributes.
*/
int16 relchecks; /* # of CHECK constraints for class */
bool relhasoids; /* T if we generate OIDs for rows of rel */
bool relhaspkey; /* has (or has had) PRIMARY KEY index */
bool relhasrules; /* has (or has had) any rules */
bool relhastriggers; /* has (or has had) any TRIGGERs */
bool relhassubclass; /* has (or has had) derived classes */
bool relispopulated; /* matview currently holds query results */
char relreplident; /* see REPLICA_IDENTITY_xxx constants */
TransactionId relfrozenxid; /* all Xids < this are frozen in this rel */
TransactionId relminmxid; /* all multixacts in this rel are >= this.
* this is really a MultiXactId */

#ifdef CATALOG_VARLEN /* variable-length fields start here */
/* NOTE: These fields are not present in a relcache entry’s rd_rel field. */
aclitem relacl[1]; /* access permissions */
text reloptions[1]; /* access-method-specific options */
#endif
} FormData_pg_class;

在给出相应的系统表的定义后,接下来就是所给出的用于bki脚本中,该元数据表中的初始数据。 而这些在pg_xxx.h中给出的系统表的原始数据将会被一系统工具正PostgrSQL编译和可执行程序的构建过程中被复制到bki文件中。

从上一段介绍中我们知道,bki文件将在PostgreSQL编译和可执行程序构建过程中由相应的系统工具构建。而这一工具就是:genbki.pl。其中, genbki.pl位于src/backend/catalog目标中,其为一个perl脚本文件。同样我们可以由该目录下的Makefile中所给出的描述来看出。

genbki
genbkicode
从genbki.pl中可以看出,其又会引用Catalog.pem文件中所给出的相应具体操作函数。 而Catalog.pem大致的功能为公共搜索指定目录下的所有pg_xxx.h类型的头文件并处理这些头文件:以这些头文件中所描述的元数据表中的信息来构建相应的bki脚本文件。 在正确的执行genbki.pl文件后,将会在src/backend/catalog中产生如下三个文件:(1)postgres.bki;(2)postgres.description;(3)postgres.shdescription。下面我们就来看一看postgres.bki中又包含了哪些东西。
bkidesc
在文章的开始部分,我们曾说在我们执行initdb进行初始化数据库时,将通过解析该bki文件来创建相应的初始数据库。当我们正确的完成Postgres的编译并在构建可执行程序时候,会将src/backend/catalog中的postgres.bki,postgres.descritpion以及postgres.shdescritpion复制到 share目录下。该目标下保存了我们在bootstrap时候所使用的相关文件。
bkishare
bkisharefolder

例如:除上述的三个文件外,还有相应的sql文件。 bki文件的相应形式如下,我们以pg_class为例,其经过genbki.pl处理后,在postgres.bki文件中所存在的形式如下:

create pg_class 1259 bootstrap rowtype_oid 83
(
relname = name ,
relnamespace = oid ,
reltype = oid ,
reloftype = oid ,
relowner = oid ,
relam = oid ,
relfilenode = oid ,
reltablespace = oid ,
relpages = int4 ,
reltuples = float4 ,
relallvisible = int4 ,
reltoastrelid = oid ,
relhasindex = bool ,
relisshared = bool ,
relpersistence = char ,
relkind = char ,
relnatts = int2 ,
relchecks = int2 ,
relhasoids = bool ,
relhaspkey = bool ,
relhasrules = bool ,
relhastriggers = bool ,
relhassubclass = bool ,
relrowsecurity = bool ,
relforcerowsecurity = bool ,
relispopulated = bool ,
relreplident = char ,
relfrozenxid = xid ,
relminmxid = xid ,
relacl = aclitem[] ,
reloptions = text[]
)
insert OID = 1247 ( pg_type 11 71 0 10 0 0 0 0 0 0 0 f f p r 30 0 t f f f f f f t n 3 1 _null_ _null_ )
insert OID = 1249 ( pg_attribute 11 75 0 10 0 0 0 0 0 0 0 f f p r 21 0 f f f f f f f t n 3 1 _null_ _null_ )
insert OID = 1255 ( pg_proc 11 81 0 10 0 0 0 0 0 0 0 f f p r 29 0 t f f f f f f t n 3 1 _null_ _null_ )
insert OID = 1259 ( pg_class 11 83 0 10 0 0 0 0 0 0 0 f f p r 31 0 t f f f f f f t n 3 1 _null_ _null_ )
close pg_class

从上述的代码片段中我们可以,其非常类似于我们在创建关系表和进行数据插入时的sql脚本。这里需要请诸位读者特别注意 bootstrap 关键字,该关键用来描述该系统表在创建过程中所表现出行为。 一个系统表的定义带有该选项与不带有该选项表现出不同的行为特征,对于这点我们将在后续的文章中给出讨论。

上述文中,我们曾提及PostgreSQL是通过解析该bki并执行相应的操作来完成对于bootstrap过程中完成对于初始数据库的的构建。 在进一步的介绍PostgreSQL对该bki文件是如何解析和执行之前。 我们首先许多读者所关心的一个问题:如何创建一个定制化的系统表。下面,我们就来展示一下如果构建一个自定义的系统表。

首先,由上述的讨论可知,在src/include/backend/catalog目标中保存了相应的以pg_xxx.h为形式的头文件,而这些头文件正是我们说需要创建的系统表的描述。并通过src/backend/catalog目录中的genbki.pl来创新相应的bki文件,而该bki文件则用来在boostrap时候供initdb使用,用来构建相应的初始数据库。 假如我们需要创建一个名为pg_ext的系统表(为了方便起见,pg_ext具有与pg_class相同的表结构)。在完成在src/include/backend/catalog目录下添加一个名为pg_ext.h的头文件并使用unused_oid获的一个有效未使用的OID数值后,我们按照pg_class的形式,给出pg_ext系统表的定义如下:

CATALOG(pg_ext,3259) BKI_BOOTSTRAP BKI_ROWTYPE_OID(83) BKI_SCHEMA_MACRO {    NameData relname; /* class name */
Oid relnamespace; /* OID of namespace containing this class */
Oid reltype; /* OID of entry in pg_type for table’s * implicit row type */
Oid reloftype; /* OID of entry in pg_type for underlying * composite type */
Oid relowner; /* class owner */
Oid relam; /* index access method; 0 if not an index */
Oid relfilenode; /* identifier of physical storage file */
/* relfilenode == 0 means it is a “mapped” relation, see relmapper.c */
Oid reltablespace; /* identifier of table space for relation */
int32 relpages; /* # of blocks (not always up-to-date) */
float4 reltuples; /* # of tuples (not always up-to-date) */
int32 relallvisible; /* # of all-visible blocks (not always * up-to-date) */
Oid reltoastrelid; /* OID of toast table; 0 if none */
bool relhasindex; /* T if has (or has had) any indexes */
bool relisshared; /* T if shared across databases */
char relpersistence; /* see RELPERSISTENCE_xxx constants below */
char relkind; /* see RELKIND_xxx constants below */
int16 relnatts; /* number of user attributes */

/*
* Class pg_attribute must contain exactly “relnatts” user attributes
* (with attnums ranging from 1 to relnatts) for this class. It may also
* contain entries with negative attnums for system attributes.
*/
int16 relchecks; /* # of CHECK constraints for class */
bool relhasoids; /* T if we generate OIDs for rows of rel */
bool relhaspkey; /* has (or has had) PRIMARY KEY index */
bool relhasrules; /* has (or has had) any rules */
bool relhastriggers; /* has (or has had) any TRIGGERs */
bool relhassubclass; /* has (or has had) derived classes */
bool relispopulated; /* matview currently holds query results */
char relreplident; /* see REPLICA_IDENTITY_xxx constants */
TransactionId relfrozenxid; /* all Xids < this are frozen in this rel */
TransactionId relminmxid; /* all multixacts in this rel are >= this.
* this is really a MultiXactId */

#ifdef CATALOG_VARLEN /* variable-length fields start here */
/* NOTE: These fields are not present in a relcache entry’s rd_rel field. */
aclitem relacl[1]; /* access permissions */
text reloptions[1]; /* access-method-specific options */
#endif
} FormData_pg_ext;

这里BKI_BOOTSTRAP 参数说了,该系统表是否为bootstrap类型,当使用该参数后,我们需要明确的告知系统,需要在pg_class中插入一条关于该系统表的说明数据。当该系统表的类型为非bootstrap时,则无需告知系统而是由postgres自动的在创建该系统表之后,在pg_class中插入一条描述该系统表的数据。 同时,如果需要在创建该系统表后,指定该系统表中所含有的初始数据,则我们可以使用由DATA宏来进行描述。例如在我们创建pg_ext系统表之后,我们想使得该系统表具有初始数据,pg_type, pg_attribute 等,则我们可以按照pg_ext的表结构使用DATA宏定义来描述该数据。genbki.pl程序在生成postgres.

bki文件时候,将DATA宏所描述的信息转为为相应的语句操作,而DESCR则是相应的描述信息。

DATA(insert OID = 1247 ( pg_type PGNSP 71 0 PGUID 0 0 0 0 0 0 0 f f p r 30 0 t f f f f f f t n 3 1 _null_ _null_ ));
DESCR(“”);
DATA(insert OID = 1249 ( pg_attribute PGNSP 75 0 PGUID 0 0 0 0 0 0 0 f f p r 21 0 f f f f f f f t n 3 1 _null_ _null_ ));
DESCR(“”);
DATA(insert OID = 1255 ( pg_proc PGNSP 81 0 PGUID 0 0 0 0 0 0 0 f f p r 29 0 t f f f f f f t n 3 1 _null_ _null_ ));
DESCR(“”);
DATA(insert OID = 1259 ( pg_ext PGNSP 83 0 PGUID 0 0 0 0 0 0 0 f f p r 31 0 t f f f f f f t n 3 1 _null_ _null_ ));
DESCR(“”);

在pg_ext.h 文件中完成对于该系统表的定义后,我们使用genbki.pl来重新生成新的postgre.bki文件。 该命令的格式为:

genbki.pl Catalog.pm pg_xxx1.h pg_xxx2.h, …, pg_xxxN.h, pg_ext.h -I $(postgres_base_dir)/src/include/backend/catalog –set-version=9.6
其中,pg_xxxx.h 为 src/include/backend/catalog目录下所需要创建的系统表的头文件, 参数I用来表明路径信息,
而set-version则是用来指定版本信息,而该版本信息则会在initdb执行时候进行验证。 当该命令正确执行后,会在src/backend/catalog目录下创建postgres.bki等文件。

genbkimakefile

下面就是由genbki.pl处理过后postgres.bki文件中关于pg_ext的相关说明

create pg_ext 3259 bootstrap rowtype_oid 83
(
relname = name ,
relnamespace = oid ,
reltype = oid ,
reloftype = oid ,
relowner = oid ,
relam = oid ,
relfilenode = oid ,
reltablespace = oid ,
relpages = int4 ,
reltuples = float4 ,
relallvisible = int4 ,
reltoastrelid = oid ,
relhasindex = bool ,
relisshared = bool ,
relpersistence = char ,
relkind = char ,
relnatts = int2 ,
relchecks = int2 ,
relhasoids = bool ,
relhaspkey = bool ,
relhasrules = bool ,
relhastriggers = bool ,
relhassubclass = bool ,
relrowsecurity = bool ,
relforcerowsecurity = bool ,
relispopulated = bool ,
relreplident = char ,
relfrozenxid = xid ,
relminmxid = xid ,
relacl = aclitem[] ,
reloptions = text[]
)
insert OID = 1247 ( pg_type 11 71 0 10 0 0 0 0 0 0 0 f f p r 30 0 t f f f f f f t n 3 1 _null_ _null_ )
insert OID = 1249 ( pg_attribute 11 75 0 10 0 0 0 0 0 0 0 f f p r 21 0 f f f f f f f t n 3 1 _null_ _null_ )
insert OID = 1255 ( pg_proc 11 81 0 10 0 0 0 0 0 0 0 f f p r 29 0 t f f f f f f t n 3 1 _null_ _null_ )
insert OID = 1259 ( pg_ext 11 83 0 10 0 0 0 0 0 0 0 f f p r 31 0 t f f f f f f t n 3 1 _null_ _null_ )
close pg_ext

当我们所构建的系统表为非BKI_BOOTSTRAP , 则无需在pg_class中指定该关于该系统表的详细信息。

在我们正确的创建完bki文件后,将share目录中的bki文件替换为该文件,则我们在使用initdb进行数据库初始化时,我们会创建一个新的系统表pg_ext。

selectext

在介绍完如果创建自定义的系统表后,下面我们来分析一下postgres是如何依据该bki文件来构建初始数据库及其相应的元数据。

由initdb工程中的initdb.c中由main函数我们可以看出,在完成相关参数的设置后,将会执行initialize_data_directory函数,而在该函数中,又会进一步的执行函数bootstrap_template1来创建相应的初始数据库。那么bootstrap_template1函数中又完成那些工作呢? 由该函数中的bki_lines = readfile(bki_file); 代码可以看出,其首先会将bki文件中的内容进行读取,并将bki文件中的某些变量进行替换。此时,在完成对于这些变量的替换后,在bootstrap_template1函数中,我们将执行 PG_CMD_OPEN 操作,而由该宏定义可以知,其会进入到函数 popen_check中执行相应的操作。

我们由popen_check函数的定义可知,其第一个参数为一const char* 型命令行,系统将通过_popen函数来创建一个postgres进程。由popen函数的如下说明我们可以知道,正是通过调用该函数来完成启动一个postgres进程的操作。

“popen() 函数通过创建一个管道,调用 fork 产生一个子进程,执行一个 shell 以运行命令来 开启一个进程。这个进程必须由 pclose() 函数关闭,而不是 fclose() 函数。pclose() 函数关闭标准 I/O 流,等待命令执行结束,然后返回 shell 的终止状态。如果 shell 不能被执行,则 pclose() 返回的终止状态与 shell 已执行 exit 一样。”

该函数的原型如下:
FILE * popen ( const char * command , const char * type ); int pclose ( FILE * stream );

而该函数的返回值为FILE* 类型,表明我们可以向使用文件系统一样,通过向该句柄中写入相应的数据来执行相应的操作。 bootstrap_template1函数中的如下代码就说明了一切:

PG_CMD_OPEN;
for (line = bki_lines; *line != NULL; line++)
{
PG_CMD_PUTS(*line);
free(*line);
}

PG_CMD_CLOSE;

而此时的变量cmd值为:”E:/postgres/bin/postgres.exe” –boot -x1 -F

bkidebug

经过该函数执行后,我们说创建的postgres进程将像一个文件系统一样接收我们通过FILE*所传递过来的命令来执行。

那么此时, 相应的问题又回到了postgres进程中:postgres以 –boot -x1 -F模式所启动的进程将被用来执行bki文件中所描述的bootstrap脚本。

我们在src/backend/main/main.c文件中的main函数中可以看出,当postgres启动的参数为boot时候,postgres进程将创建一个所谓的辅助进程来接手相应的处理工作。

if (argc > 1 && strcmp(argv[1], “–boot”) == 0)
AuxiliaryProcessMain(argc, argv); /* does not return */

在函数AuxiliaryProcessMain中,将依据上述相应的postgres进程创建时候所指定的参数来进行相应的处理。 在进行参数分解和辨识后,AuxiliaryProcessMain函数中将使用BaseInit 来进行基表参数的初始化工作。因为在某些参数需要在InitPostgres操作之前进行初始化工作。 在完成BaseInit操作后,AuxiliaryProcessMain函数继续依据-X所指定的类型来进行分类处理.
-X参数指明了该辅助进程所处于何种状态下,本例中 -X1表明了,当前的postgres进程是处于bootstrap状态下,因此AuxiliaryProcessMain 函数将进入到 bootstrap分支中。

bkisubmain

然后将具体的处理过程交由 BootstrapModeMain 函数来完成。 那么此时的问题又归结为BootstrapModeMain函数。下面我们就来看一看BootstrapModeMain函数,看看它又是如何完成执行bki文件呢?(由上述PG_CMD_PUTS(*line);代码可以,其是以一条一条的方式进行执行), 在完成 InitProcess(); 及InitPostgres(NULL, InvalidOid, NULL, InvalidOid, NULL); 操作后,函数来到 boot_yyparse, 这里对于lex/yacc比较熟悉的读者一眼就可以看出yyparse的作用。 对其就是对bki文件中的命令进行解析的工具,其实质上也是一个yacc。 到这里可能读者又会想到有一个这个boot_yyparse,那么肯定会有个套.l和.y文件与其相对应,用来描述相应的词法和语法规则。没错,在src\backend\bootstrap目录的bootscanner.l和bootparse.y文件正是描述bki文件中所出现的词法和语法规则。

下面,我们就以bki文件中create命令来分析一下,bootparse.y中描述的语法规则。由bootparse.y文件中我们找到 create语句的语法规则描述:

Boot_CreateStmt:
XCREATE boot_ident oidspec optbootstrap optsharedrelation optwithoutoids optrowtypeoid LPAREN
{
do_start();
numattr = 0;
elog(DEBUG4, “creating%s%s relation %s %u”,
$4 ? ” bootstrap” : “”,
$5 ? ” shared” : “”,
$2,
$3);
}

boot_column_list
{
do_end();
}

RPAREN
{
TupleDesc tupdesc;
bool shared_relation;
bool mapped_relation;
do_start();
tupdesc = CreateTupleDesc(numattr, !($6), attrtypes);
shared_relation = $5;
mapped_relation = ($4 || shared_relation);
if ($4)
{
if (boot_reldesc)
{
elog(DEBUG4, “create bootstrap: warning, open relation exists,
closing first”);
closerel(NULL);
}
boot_reldesc = heap_create($2,
PG_CATALOG_NAMESPACE,
shared_relation ? GLOBALTABLESPACE_OID : 0,
$3,
InvalidOid,
tupdesc,
RELKIND_RELATION,
RELPERSISTENCE_PERMANENT,
shared_relation,
mapped_relation,
true);
elog(DEBUG4, “bootstrap relation created”);
}
else
{
Oid id;
id = heap_create_with_catalog($2,
PG_CATALOG_NAMESPACE,
shared_relation ? GLOBALTABLESPACE_OID : 0,
$3,
$7,
InvalidOid,
BOOTSTRAP_SUPERUSERID,
tupdesc,
NIL,
RELKIND_RELATION,
RELPERSISTENCE_PERMANENT,
shared_relation,
mapped_relation,
true,
0,
ONCOMMIT_NOOP,
(Datum) 0,
false,
true,
false,
NULL);
elog(DEBUG4, “relation created with OID %u”, id);
}
do_end();
}
;

上述的XCREATE为对应的create关键字,而boot_ident则为我们所需要创建的系统表的名称,oidspec 为相应所指定系统表的oid值, optbootstrap则是我们boostrap类型,即在上述文中我们所提及的 BKI_BOOTSTRAP 参数。 从上述规则所对于的action中可以看出当 if ($4) 条件为true时,执行 heap_create 操作; 而当条件为false时,执行heap_create_with_catalog。 而$4正式我们在pg_ext.h和pg_class.h中所给出系统表的定义的相关参数,BKI_BOOTSTRAP。 当未给出BKI_BOOTSTRAP参数是,由函数heap_create_with_catalog创建相应的表,然后并将该基表信息插入到pg_class中;当给出BKI_BOOTSTRAP参数时,需要我们明确的在bki文件中给出相应的将基表信息插入到pg_class的语句。

到这里,我们相信诸位读者已经对于postgres的initdb执行由一个清晰而明确的认识了,上文我们详细的剖析了postgres在初始化过程中所发生的一切一切。