Postgres中postmaster代码解析(--boot和--single)


难度 中等

之前我的一些文章都是在说Postgres的一些查询相关的代码。但是对于Postgres服务端是如何启动,后台进程是如何加载,服务端在哪里以及如何监听客户端的连接都没有一个清晰的逻辑。那么今天我来说说Postgres中的postmaster模块的代码,试着解答这些问题。

在正式讨论之前,我先说一下,代码主要涉及的是postgres源码的src/backend目录下的main,postmaster以及tcop模块。

关于postmaster这个命令,熟悉postgres的一定不会陌生。在Linux上它是postgres命令的一个软连接,而在Windows上,它直接就是postgres命令的别名。因此,话题就转换为:postgres命令的处理细节。而postgres命令,从官方手册上我们可以知道,它是启动后端服务器的命令(当然前提是你要用initdb命令先生成一个database cluster)。无论是是直接使用postgres命令启动还是用pg_ctl命令,其本质都是调用postgres命令来启动数据库的。

下面进入代码。


1.命令的入口处理

命令的入口在src/backend/main/main.c。这个main()函数所做的工作不多:

做一下基本的初始化(主要是调用MemoryContextInit函数启动error和memory management子系统,还有其他的locale设置等);
根据命令行的第一个参数分派不同的函数去处理。

我们用postgres命令来启动一个数据库的时候,虽然参数很多。最简单的是对于”–help, –version”这两个参数的处理。对于这两个参数我们只需要简单的返回一下帮助信息即可返回。对于剩下的参数,我们不急着处理,因为我们首先要根据第一个参数确定的是我们希望database工作在何种模式?

要回答这个问题的话,我们看一下src/backend/main/main.c,在main函数里,有以下代码:

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 */--->后台主进程

我们来一一分析。

首先是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()函数,进入正常启动后端的过程。这个也是我们今天讨论的重点了。


2.postmater的处理

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的其他细节。


文章作者: growdu
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 growdu !
  目录
分类导航
随笔3 AI29 计算机基础13 算法1 博客搭建7 ChatGPT2 集群63 计算机通信1 数据库59 数据库深入93 数据库93 Docker11 DPDK26 编辑工具4 Elasticsearch4 FAQ1 Go Web1 hometown2 编程语言16 人生11 网络9 Linux38 OPC1 openGauss4 页面12 PostgreSQL54 程序员自我修养1 协议11 stock1 存储5 成长之路1 工具20 视频作品1 VPP18 Vue13 Web1 代码示例11 BenchmarkSQL1 PostgreSQL 源码修炼之路14
最热文章
1
PostgreSQL 逻辑复制全链路:以一个真实事务为例,从 `INSERT` 到 `apply` 的源码级旅程
数据库🔥 2162
2
PostgreSQL 逻辑复制 Stream 执行逻辑源码级拆解:从 reorderbuffer 触发到订阅端落盘回放
数据库🔥 1765
3
PostgreSQL 行锁机制源码级深度拆解:4 种锁模式 + xmax + MultiXact 的完整链路
数据库🔥 1656
4
PostgreSQL reorderbuffer.c 函数级源码全集:5615 行 / 95 个函数的逐函数解析
数据库深入🔥 1650
5
PostgreSQL 目录快照源码级深度拆解:系统表读取的 MVCC 真相与失效机制
数据库🔥 1604
6
13 逻辑复制深入
数据库深入🔥 1570
7
0 Postgresql存储、索引及系统优化、主备切换
PostgreSQL🔥 1495
8
PostgreSQL 读一行数据源码级深度拆解:MVCC 在每个环节的落点
数据库🔥 1464
9
PostgreSQL WAL 记录类型完全指南:21 个 RMGR × 80+ 种记录的源码级拆解
数据库🔥 1431
10
一文读懂openguass dcf网络模块
集群🔥 1420
11
PostgreSQL 元数据存储机制:从磁盘文件到内存缓存,`pg_class` 撑起的整个系统表体系
数据库🔥 1411
12
openGauss 并行解码设计原理深度拆解:reader + 多 worker + CSN 三层架构的源码级剖析
数据库深入🔥 1344
13
逻辑复制源码分析
数据库深入🔥 1327
14
PostgreSQL 逻辑复制:catalog 改动、可见性判定与 xmin/xmax 推进全景
数据库深入🔥 1317
15
自托管服务全家桶:从原文方案到可复制实操手册
人生🔥 1278
16
PostgreSQL CREATE_REPLICATION_SLOT 端到端源码分析:从 SQL 到 snapbuild 状态机的完整链路(每个函数每个参数)
数据库深入🔥 1277
17
PostgreSQL syscache 模块源码深度拆解:CatCache 数据结构、LRU 哈希表、cache invalidation 全链路
数据库深入🔥 1216
18
PostgreSQL 18 内核数据结构全景:RangeVar 与 20+ 个核心数据结构源码级拆解
数据库🔥 1158
19
PostgreSQL 逻辑复制里 `RUNNING_XACTS` 写入机制与快照 `xmin / xmax / xip` 的演化:源码级深度拆解
数据库🔥 1119
20
PostgreSQL 分区表:从一行 `PARTITION BY` 到路由热路径的全链路拆解
数据库🔥 1094