PostgreSQL数据库安装好以后,要执行initdb命令对数据库进行初始化才可以正常使用。那么,initdb命令都做了哪些事情呢?

1. initdb介绍

initdb是PostgreSQL的一个独立的程序。前面的文章已经介绍过,initdb的主要作用就是创建(初始化)数据库集簇(database cluster),几乎数据库所有相关的东西都在这个集簇之中(除了程序文件)。创建数据库集簇其实就是创建一些目录用来存储数据库数据,以及创建一些控制文件。同时会创建3个数据库:template0 、template1 、postgres 。具体介绍见《PostgreSQL常用命令》一文。

下面是我执行initdb时的输出:

allan@ubuntu:~$ initdb -E UTF-8
The files belonging to this database system will be owned by user "allan".
This user must also own the server process.

The database cluster will be initialized with locale "en_US.UTF-8".
The default text search configuration will be set to "english".

creating directory /home/allan/pgdata ... ok
creating subdirectories ... ok
selecting default max_connections ... 100
selecting default shared_buffers ... 24MB
creating configuration files ... ok
creating template1 database in /home/allan/pgdata/base/1 ... ok
initializing pg_authid ... ok
initializing dependencies ... ok
creating system views ... ok
loading system objects' descriptions ... ok
creating collations ... ok
creating conversions ... ok
creating dictionaries ... ok
setting privileges on built-in objects ... ok
creating information schema ... ok
loading PL/pgSQL server-side language ... ok
vacuuming database template1 ... ok
copying template1 to template0 ... ok
copying template1 to postgres ... ok

WARNING: enabling "trust" authentication for local connections
You can change this by editing pg_hba.conf or using the option -A, or
--auth-local and --auth-host, the next time you run initdb.

Success. You can now start the database server using:

    postgres -D /home/allan/pgdata
or
    pg_ctl -D /home/allan/pgdata -l logfile start

我系统的环境变量设置如下(先设置环境变量,再执行initdb):

allan@ubuntu:~$ tail -n3 .bashrc 
export PATH=/home/allan/pgsql/bin:$PATH
export PGDATA=/home/allan/pgdata
export PGLIB=/home/allan/pgsql/lib

2. initdb流程分析

initdb对应的源码为src/bin/initdb/initdb.c,入口函数是该文件中的main函数。initdb可以加参数,所以main函数也有入参(argc, argv[]),而argv[0]一般都是initdb命令的全路径字符串,比如在我的环境上面为:/home/allan/pgsql/bin/initdb. 下面我们看一下initdb的主要流程。

说明:

(1)本系列文章使用的PostgreSQL版本为9.2.4.

(2)使用的操作系统为Ubuntu 14.04 64bit

(3)贴代码时,可能省去了一些兼容windows或其他平台的代码。

(4)贴代码时,连续的函数或语句不一定在一起或者同一个源文件中,只是会将相关性比较大的函数或语句贴在一起。

2.1 初始化变量&&获取程序名

首先,程序会定义两个变量long_options和subdirs供后面使用。然后就会会调用get_progname函数获取程序名,即initdb。该函数主要是调用last_dir_separator函数获取字符串argv[0]的最后一个目录分隔符,比如对于argv[0]="/home/allan/pgsql/bin/initdb",执行该函数后得到"/initdb",然后再get_progname函数中去掉最后一个目录分隔符,得到initdb。

progname = get_progname(argv[0]);

/*
 * Extracts the actual name of the program as called -
 * stripped of .exe suffix if any
 */
const char *
get_progname(const char *argv0)
{
	const char *nodir_name;
	char	   *progname;

	nodir_name = last_dir_separator(argv0);
	if (nodir_name)
		nodir_name++;
	else
		nodir_name = skip_drive(argv0);

	/*
	 * Make a copy in case argv[0] is modified by ps_status. Leaks memory, but
	 * called only once.
	 */
	progname = strdup(nodir_name);
	if (progname == NULL)
	{
		fprintf(stderr, "%s: out of memoryn", nodir_name);
		abort();				/* This could exit the postmaster */
	}

	return progname;
}


/*
 *	last_dir_separator
 *
 * Find the location of the last directory separator, return
 * NULL if not found.
 */
char *
last_dir_separator(const char *filename)
{
	const char *p,
			   *ret = NULL;

	for (p = skip_drive(filename); *p; p++)
		if (IS_DIR_SEP(*p))
			ret = p;
	return (char *) ret;
}

#define skip_drive(path)	(path)

需要注意的是,在postgresql源码中,很多地方都没有直接使用获得的初始字符串,而是拷贝一份副本(使用strdup函数)用于操作。

2.2 初始化系统编码和配置目录(service directory)

调用set_pglocale_pgservice函数设置一些环境变量。其中主要的函数说明如下:

  • PG_TEXTDOMAIN 宏的变量是将其参数和PostgreSQL的主版本号使用-拼接在一起,比如PG_TEXTDOMAIN("initdb");的结果为"initdb-9.2";
  • 使用setlocale(LC_ALL,""); 语句设置系统编码为LC_ALL;
  • 使用find_my_exec函数判断输入的可执行文件是否可读可执行;
  • 设置环境变量PGSYSCONFDIR ,用于后面对系统的配置,一般不使用。在我的环境中,该环境变量最后为“PGSYSCONFDIR=/home/allan/pgsql/etc”。

2.3 处理输入选项

该部分主要是对输入的命令行参数(不一定有)进行处理,虽然PG(后面简称PostgreSQL为PG)自己实现了getopt_long函数,但其原理和系统提供的API函数getopt_long相同。关于该函数的使用见我的博客《命令行参数解析函数getopt及getopt_long介绍》,这里不再赘述。

在这一步除了解析输入选项,还有几项比较重要的工作:

  • 数据集簇目录的指定,程序先会从命理行参数获取(-D /path/pg_data_dir),如果获取不到,就尝试读取环境变量PGDATA。如果二者都没有得到一个路径,就报错退出;
  • 检验postgres的版本是否正确(使用postgres -V 的输出和PG_BACKEND_VERSIONSTR比较);
  • 找到postgres所在目录(backend_exec变量的值)和share目录(share_path);
  • 指定数据库用户。

2.4 文件检查和语系设置

调用set_input设置share目录下面的一些文件名,check_input对文件可用性进行检查。

之前已经初始化过语系,这里进行了一系列语系、字符集等检查和设置。

2.5 中断信号设置

设置中断信号处理函数,对终端命令行SIGHUP、程序中断SIGINT、程序退出SIGQUIT、软件中断SIGTERM和管道中断SIGPIPE等信号进行屏蔽,保证初始化工作顺利进行。主要调用的是pqsignal函数:

pqsigfunc
pqsignal(int signo, pqsigfunc func)
{
#if !defined(HAVE_POSIX_SIGNALS)
	return signal(signo, func);
#else
	struct sigaction act,
				oact;

	act.sa_handler = func;
	sigemptyset(&act.sa_mask);
	act.sa_flags = 0;
	if (signo != SIGALRM)
		act.sa_flags |= SA_RESTART;
#ifdef SA_NOCLDSTOP
	if (signo == SIGCHLD)
		act.sa_flags |= SA_NOCLDSTOP;
#endif
	if (sigaction(signo, &act, &oact) < 0)
		return SIG_ERR;
	return oact.sa_handler;
#endif   /* !HAVE_POSIX_SIGNALS */

2.6 创建数据目录及其子目录

该步主要是创建pg_data目录及其子目录,包括创建PG_VERSION文件。

涉及的函数主要是创建目录的函数mkdatadir,但实质去创建目录的是pg_mkdir_p函数,该函数实现了递归创建目录:

/*
 * pg_mkdir_p --- create a directory and, if necessary, parent directories
 *
 * This is equivalent to "mkdir -p" except we don't complain if the target
 * directory already exists.
 *
 * We assume the path is in canonical form, i.e., uses / as the separator.
 *
 * omode is the file permissions bits for the target directory.  Note that any
 * parent directories that have to be created get permissions according to the
 * prevailing umask, but with u+wx forced on to ensure we can create there.
 * (We declare omode as int, not mode_t, to minimize dependencies for port.h.)
 *
 * Returns 0 on success, -1 (with errno set) on failure.
 *
 * Note that on failure, the path arg has been modified to show the particular
 * directory level we had problems with.
 */
int
pg_mkdir_p(char *path, int omode)
{
	struct stat sb;
	mode_t		numask,
				oumask;
	int			last,
				retval;
	char	   *p;

	retval = 0;
	p = path;

#ifdef WIN32
	/* skip network and drive specifiers for win32 */
	if (strlen(p) >= 2)
	{
		if (p[0] == '/' && p[1] == '/')
		{
			/* network drive */
			p = strstr(p + 2, "/");
			if (p == NULL)
			{
				errno = EINVAL;
				return -1;
			}
		}
		else if (p[1] == ':' &&
				 ((p[0] >= 'a' && p[0] <= 'z') ||
				  (p[0] >= 'A' && p[0] <= 'Z')))
		{
			/* local drive */
			p += 2;
		}
	}
#endif

	/*
	 * POSIX 1003.2: For each dir operand that does not name an existing
	 * directory, effects equivalent to those caused by the following command
	 * shall occcur:
	 *
	 * mkdir -p -m $(umask -S),u+wx $(dirname dir) && mkdir [-m mode] dir
	 *
	 * We change the user's umask and then restore it, instead of doing
	 * chmod's.  Note we assume umask() can't change errno.
	 */
	oumask = umask(0);
	numask = oumask & ~(S_IWUSR | S_IXUSR);
	(void) umask(numask);

	if (p[0] == '/')			/* Skip leading '/'. */
		++p;
	for (last = 0; !last; ++p)
	{
		if (p[0] == '')
			last = 1;
		else if (p[0] != '/')
			continue;
		*p = '';
		if (!last && p[1] == '')
			last = 1;

		if (last)
			(void) umask(oumask);

		/* check for pre-existing directory */
		if (stat(path, &sb) == 0)
		{
			if (!S_ISDIR(sb.st_mode))
			{
				if (last)
					errno = EEXIST;
				else
					errno = ENOTDIR;
				retval = -1;
				break;
			}
		}
		else if (mkdir(path, last ? omode : S_IRWXU | S_IRWXG | S_IRWXO) < 0)
		{
			retval = -1;
			break;
		}
		if (!last)
			*p = '/';
	}

	/* ensure we restored umask */
	(void) umask(oumask);

	return retval;
}

2.7 设置三个配置文件

测试当前服务器系统性能,由测试结果创建配置文件postgresql.conf、pg_hba.conf、pg_ident.conf文件,并对其中定义的参数做一些设置。这些功能主要是由以下三个函数做的:

  • set_null_conf:创建空的postgresql.conf文件
  • test_config_settings:测试服务器性能,得到一些参数合适的值,主要是shared_buffers的值
  • setup_config:创建并配置postgresql.conf、pg_hba.conf、pg_ident.conf文件

2.8 创建数据库template1

在bootstrap模式下允许BKI脚本(postgres.bki),生成template1模板数据库,它的数据存储在base/1目录下。

postgres.bki文件是在编译的过程中由src/backend/catalog目录下的脚本程序文件genbki.pl(旧的版本里面可能是genbki.sh)读取src/include/catalog目录下的以.h结尾的系统表定义文件(包括系统表索引和TOAST表定义文件)创建的,并且通常存放在安装树的share子目录下。在pg_*.h的头文件中包含如下内容定义:

  • 定义CATALOG宏,用于以统一的模式去定义系统表的结构以及用以描述系统表的数据结构,如系统表pg_class的定义通过CATALOG(pg_class,1259)来表现。
  • 通过宏DATA(x)和DESCR(x)来定义insert操作,这样的insert操作可能会有多个,用于定义系统表中的初始数据。

模板数据库template1就是通过运行在bootstrap模式下的postgres程序读取postgres.bki文件创建的。BKI文件是一些用特殊语言写的脚本,这些脚本使PostgreSQL后端能够理解,且以特殊的bootstrap模式来执行之,这种模式允许在不存在系统表的零初始条件下执行数据库函数,而普通的SQL命令要求系统表必须存在。因此BKI文件仅用于初始化数据集簇。

2.9 创建其他数据库及表等

在bootstrap模式下创建好template1以后,就可以使用SQL语句创建系统视图、系统表、TOAST表、template0数据库、postgres数据库等。等这些都成功执行后,就打印成功信息,然后退出。

至此,initdb执行结束,如果没有出错的话,数据库就可以正常使用了。下面附一张图:

initdb

本文总结自《PostgreSQL内核源码分析》一书。