以前个人一些文章都是在说Postgres的一些查询相关的代码。可是对于Postgres服务端是如何启动,后台进程是如何加载,服务端在哪里以及如何监听客户端的链接都没有一个清晰的逻辑。那么今天我来讲说Postgres中的postmaster模块的代码,试着解答这些问题。数据库
在正式讨论以前,我先说一下,代码主要涉及的是postgres源码的src/backend目录下的main,postmaster以及tcop模块。bootstrap
关于postmaster这个命令,熟悉postgres的必定不会陌生。在Linux上它是postgres命令的一个软链接,而在Windows上,它直接就是postgres命令的别名。所以,话题就转换为:postgres命令的处理细节。而postgres命令,从官方手册上咱们能够知道,它是启动后端服务器的命令(固然前提是你要用initdb命令先生成一个database cluster)。不管是是直接使用postgres命令启动仍是用pg_ctl命令,其本质都是调用postgres命令来启动数据库的。后端
下面进入代码。服务器
命令的入口在src/backend/main/main.c。这个main()函数所作的工做很少:数据结构
作一下基本的初始化(主要是调用MemoryContextInit函数启动error和memory management子系统,还有其余的locale设置等); 根据命令行的第一个参数分派不一样的函数去处理。
咱们用postgres命令来启动一个数据库的时候,虽然参数不少。最简单的是对于"–help, --version"这两个参数的处理。对于这两个参数咱们只须要简单的返回一下帮助信息便可返回。对于剩下的参数,咱们不急着处理,由于咱们首先要根据第一个参数肯定的是咱们但愿database工做在何种模式?dom
要回答这个问题的话,咱们看一下src/backend/main/main.c,在main函数里,有如下代码:socket
if (argc > 1 && strcmp(argv[1], "--boot") == 0) AuxiliaryProcessMain(argc, argv); /* does not return */ --->后端子进程,bootstrap else if (argc > 1 && strcmp(argv[1], "--describe-config") == 0) GucInfoMain(); /* does not return */ else if (argc > 1 && strcmp(argv[1], "--single") == 0) PostgresMain(argc, argv, NULL, /* no dbname */ strdup(get_user_name_or_exit(progname))); /* does not return */ --->backend进程 else PostmasterMain(argc, argv); /* does not return */ --->后台主进程
咱们来一一分析。ide
首先是bootstrap("--boot"参数指定)模式。函数
这个模式你们可能比较陌生,在postgres的官方手册上也没有说明这个参数,这个参数目前只在postgres程序内部调用。这个模式下,数据库工做在启动模式下,此时数据库还不能接受外部的数据库链接,大概就是说此时数据库只是内核启动了,尚未对外部提供访问接口。那么这个模式的用处何在呢?工具
那就让咱们把目光放远一点,看看initdb这个命令吧(代码在src/bin/initdb下)。initdb命令咱们很熟悉,这个命令用来初始化一个数据库集群。而在这个过程当中,咱们知道会创建template1,template0和postgres这个三个初始的数据库。那么问题是,咱们这三个数据库是怎么创建的,数据库的表,视图索引之类的是怎么建立的?
答案就是在调用工做在"bootstrap"模式下的postgres命令,启动一个"standalone bootstrap process"。也就是说,以"内核"模式启动postgres服务器,从而进行这一系列的数据库操做。证据何在?咱们看看initdb.c:
代码调用栈以下:
main() ->initialize_data_directory() ->bootstrap_template1()
在bootstrap_template1中,有如下代码:
snprintf(cmd, sizeof(cmd), "\"%s\" --boot -x1 %s %s %s", backend_exec, data_checksums ? "-k" : "", boot_options, talkargs);
能够说是很是清晰了。
接下来是"--describe-config"参数。
这个参数在官方手册是有定义说明的,我直接抄下来吧:
这个选项会用制表符分隔的COPY格式导出服务器的内部配置变量、描述以及默认值。设计它的目的是用于管理工具。
因此仍是打印信息,只不过是打印数据库的内部的配置参数信息的,其实仍是蛮实用的。
single("--single"参数指定)模式
这个很简单,选择数据库进入单用户模式。这个模式和正常的启动的差异是你是以单独的用户方式进入数据库的,不会作任何后台处理,例如自动检查点。通常是用来debug用。这里也不细说了。
不指定以上参数的话,咱们进入Postmater进程。
到这里,咱们终于进入正题,进入PostmasterMain()函数,进入正常启动后端的过程。这个也是咱们今天讨论的重点了。
postmaster部分的处理主函数是PostmasterMain()。这个函数的处理内容较多,咱们按照步骤来解释:
1.memorycontext的创建与切换(切换到PostmasterContext)
咱们知道在main模块里咱们创建了顶层上下文TopMemoryContext和其子上下文ErrorContext。此处调用AllocSetContextCreate()函数创建内存上下文PostmasterContext,并调用函数MemoryContextSwitchTo()将当前上下文切换到PostmasterContext。这样若是在Postmaster模块若是出现内存相关的问题,不会影响到其余模块(这也是内存上下文模块引入的缘由吧)。
2.信号处理函数(singal handler)的设置
做为一个后端主进程,其拥有不少相关的子进程。咱们也知道信号(singal)是进程间通讯的一种比较方便的方式。这里Postmaster也利用了这一点,注册了不少信号处理函数来处理信号。有关这部分的信号以及相关的handler我列在下面了:
3.处理GUC和一些命令行参数
这部分就比较常规了。注意这里调用InitializeGUCOptions()函数初始化一些GUC,咱们还并不能从config文件中读取,由于咱们尚未处理命令行参数(命令行参数能够指定一些GUC参数以及config文件)。而后咱们用getopt读取命令行参数,这样之后,咱们就能够读取config文件进行GUC的设置和验证参数的合法性了。
4.数据库集群(database cluster)的锁定(lock)
调用CreateDataDirLockFile()函数在数据库集群所在目录建立数据库集群的lock文件postmaster.pid。这样就能保证咱们不会对同一个数据库集群"启动两次"。虽然咱们也会建立socket lock文件,可是咱们仍是以为数据库集群所在的目录更加可信和保险一点。
5.共享库的预加载(process_shared_preload_libraries())
咱们喜欢用Postgres的一大缘由就是Postgres的丰富的插件,其中不少就是经过共享库来实现的。这里就是调用process_shared_preload_libraries()函数来导入你在shared_preload_libraries参数中指定的共享库的。
6.socket的初始化
这里初始化TCP/IP socket和UNIX socket。初始化UNIX socket会在/tmp下建立socket文件。默认状况下,TCP/IP socket是禁用的,咱们能够经过修改配置文件来开启。
7.共享内存和信号量的初始化(reset_shared(PostPortNumber))
这里调用函数reset_shared(PostPortNumber)来处理共享内存和信号量。详细的说,它调用各模块的共享内存的使用量估计函数,计算总共所需的共享内存的量,并申请。详细的咱们能够看CreateSharedMemoryAndSemaphores()函数。
8.初始化(并未启动)数据库相关后台进程
调用SysLogger_Start()函数启动syslogger后台子进程;
分别调用pgstat_init()和autovac_init()函数初始化状态收集子进程(stats collection process)和自动清理子进程(autovacuum process)。
9.读取客户端认证的配置文件()
调用load_hba()函数和load_ident()函数读取客户端认证文件pg_hba.conf和ident.conf。
10.启动数据库(StartupDataBase()函数)
作好了上面这些配置和设置,这里终于能够进行数据库的启动操做了。这里咱们调用StartupDataBase()函数(其实就是一个宏)来启动数据库集群,这里主要发挥做用的是StartupProcessMain(void)函数,这个函数至关于启动数据库的Main函数。详细的调用栈以下,有兴趣的读者能够看看:
PostmasterMain() ->StartupDataBase() ->StartChildProcess() ->AuxiliaryProcessMain() ->StartupProcessMain(void) ->StartupXLOG()
11.服务端主循环(ServerLoop())
既然数据库终于启动起来了,咱们终于能够接受客户端发起的链接请求了,这里的ServerLoop()函数就是一个死循环。循环读取客户端的请求并进行相关处理。
这里有一点说明,我提个问题,是否是进入ServerLoop()以后,咱们就真的能够立刻接受客户端的链接了呢?或者换句话说,咱们到底到何时才能接受客户端链接呢?标志是什么呢?
咱们看PostmasterMain()函数里面关于上面的10和11的代码以下:
StartupPID = StartupDataBase(); Assert(StartupPID != 0); StartupStatus = STARTUP_RUNNING; pmState = PM_STARTUP; /* Some workers may be scheduled to start now */ maybe_start_bgworker(); status = ServerLoop();
关于上面的代码,咱们发现,正是由StartupDataBase()函数启动了数据库,而后在ServerLoop()函数里面接受链接。
可是,咱们看到在进入ServerLoop()函数以前,pmState的值仍是PM_STARTUP,而只有在PM_RUN状态,数据库才是真正的启动起来了。
咱们在看下函数reaper(),这个函数是postmaster函数的一个信号处理函数,当它捕获到Startup进程正常死亡(也就是说,数据库正常启动完毕了)后,会设置pmState为PM_RUN。
所以,咱们获得结论:在进入ServerLoop()函数后,只有在reaper函数捕获到Startup的正常死亡并设置pmState为PM_RUN以后,数据库才能真正的意义上接受链接。
接下来,咱们再看ServerLoop()里面到底作了什么。
PostmasterMain() |->ServerLoop() |->initMasks() |->for(;;) |->select() <--监听端口 |->ConnCreate() <--建立connection相关的数据结构 |->BackendStartup() <--创建后端进程backend process |->PostmasterRandom() |->fork_process() |->InitPostmasterChild() |->ClosePostmasterPorts() |->BackendInitialize() |->ProcessStartupPacket() |->BackendRun() |->PostgresMain() |->ConnFree() <--释放connection相关的数据结构
咱们再看看关于ServerLoop()中的关于socket和信号处理:
好的,今天就到这里,剩下的会继续讨论Postmaster的其余细节。