更多"C/C++、PostgreSQL、编译原理、计算机原理、TCP/IP、数据结构&算法、Linux编程”等技术文章更新于公众号: 君子黎


1. postmaster.pid是什么文件?

     postmaster.pid文件是一个描述数据蔟目录的锁文件(lock file)。这里数据蔟目录即 PostgreSQL源码编译安装 中4.1节处使用initdb时候参数-D所指定的目录位置。

1.1 postmaster.pid文件位于哪?

     postmaster.pid锁文件位于指定数据蔟目录下。比如我的数据目录是/home/ssd/PG132, 那么postmaster.pid文件在/home/ssd/PG132/目录下。

1.2 postmaster.pid文件中都有什么数据?

     根据文件名后缀,大家第一时间想到的是,该文件存储了辅助进程postmaster的进程PID,没错,该文件确实存储了postmaster进程的PID, 但是实际上,该文件还存储有其他多项数据,下面一起来看看存储了什么项,每一项分别代表什么。
     在不去阅读源码时候,有一个文件可以快速帮你了解postmaster.pid锁文件中的各项数据的内容。该文件是pidfile.h(位于src/include/utils/pidfile.h)。在postmaster.pid文件中,每项数据存储为一行,从PostgreSQL 10.0开始,该锁文件中的内容共有8行(每行为一项),如下图所示:

图片说明

     其中每行的含义如下:
     LINE 1:postmaster守护进程的PID,或独立后端PID的负数。对于PostgreSQL数据库,其postgres的启动方式有两种,一种是多用户模式,即PostmasterMain()函数分支;另外一种是不经过postmaster的单用户模式。走PostgresMain()分支。

    if (argc > 1 && strcmp(argv[1], "--boot") == 0)
        AuxiliaryProcessMain(argc, argv);    /* does not return */
    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 */
    else
        PostmasterMain(argc, argv); /* does not return */

     LINE 2:数据蔟目录路径。

     LINE 3:postmaster守护进程的启动时间. 以秒(s)为单位。

     LINE 4:postmaster进程监听的端口。默认5432.

     LINE 5:第一个Unix套接字目录路径(如果没有则为空)。

     LINE 6:第一个监听地址(它可以是IP地址或"*", 如果没有TCP端口则为空)。

     LINE 7:共享内存键(在Windows上面为空)

     在类Unix系统上,可以使用ipcs命令看到共享内存健值信息如下图所示,其中0x0454ec24(十六进制)所对应的十进制数据刚好是72674340。和postmaster.pid文件中的第7行对应上。

在这里插入图片描述

     LINE 8:postmaster守护进程的状态。对于postmaster,可能出现的状态有以下几种:

#define PM_STATUS_STARTING        "starting"    /* 仍在启动*/
#define PM_STATUS_STOPPING        "stopping"    /* 在顺序关机汇总 */
#define PM_STATUS_READY            "ready   "    /* 准备好连接,只有postmaster启动成功,postmaster.pid文件中的第8行就是"read",参考上图 */
#define PM_STATUS_STANDBY        "standby "    /*  不接受连接*/

     注意:第6行及以上是在初始文件创建后通过AddToDataDirLockFile()添加的; 另外,第5行最初是空的(看下面的代码,最初socketDir初始化为空,CreateLockFile()函数第3个参数),并在第一个Unix套接字打开后更改。旁观者不应该认为第4行及以上是按照任何特定的顺序填写的。

void
CreateDataDirLockFile(bool amPostmaster)
{
    CreateLockFile(DIRECTORY_LOCK_FILE, amPostmaster, "", true, DataDir);
}

snprintf(buffer, sizeof(buffer), "%d\n%s\n%ld\n%d\n%s\n",
             amPostmaster ? (int) my_pid : -((int) my_pid),
             DataDir,
             (long) MyStartTime,
             PostPortNumber,
             socketDir);

1.2.1 套接字锁定文件目录路径

     在1.2节中详细描述了postmaster.pid文件中各行(项)内容及其含义,其中第5行是第一个Unix套接字目录路径(若有的前提下。不过一般都有,因为一个不能监听端口的数据库没有实际应用)。这里第一个套接字即为postmaster所监听的端口,默认是5432,我这里手动配置为了5566,对于端口的配置请阅读 【PostgreSQL教程】· postgresql.conf配置文件详解

     套接字目录路径默认在/tmp目录下,因此postmaster.pid锁文件中的第5行为"/tmp"。除此之外,postmaster还会在/tmp目录下创建一个“套接字锁文件(Socket lock files.)”。该文件中的内容共有5行,这5行数据与postmaster.pid锁文件中的前面5行内容是相同的。套接字锁文件是一个隐藏文件,其文件名的格式为 .s.PGSQL.5432和 .s.PGSQL.5432.lock(这里5432是指定的postmaster监听端口),其初始化代码如下:

#define UNIXSOCK_PATH(path, port, sockdir) \
       (AssertMacro(sockdir), \
        AssertMacro(*(sockdir) != '\0'), \
        snprintf(path, sizeof(path), "%s/.s.PGSQL.%d", \
                 (sockdir), (port)))

void CreateSocketLockFile(const char *socketfile, bool amPostmaster,
                     const char *socketDir)
{
    char        lockfile[MAXPGPATH];

    snprintf(lockfile, sizeof(lockfile), "%s.lock", socketfile);
    CreateLockFile(lockfile, amPostmaster, socketDir, false, socketfile);
}

     文件.s.PGSQL.5432.lock是Unix套接字锁文件,.s.PGSQL.5432是socket套接字文件。如下图所示:

在这里插入图片描述

2. postmaster.pid锁文件底层源码

2.1 postmaster.pid文件入口

     postmaster.pid文件的创建入口是函数CreateDataDirLockFile(true)。该函数的作为CreateLockFile()的一层封装,真正的文件创建逻辑是由CreateLockFile()函数完成。

static void
CreateLockFile(const char *filename, bool amPostmaster,
               const char *socketDir,
               bool isDDLock, const char *refName)
{
   //省略
}

void
CreateDataDirLockFile(bool amPostmaster)
{
    CreateLockFile(DIRECTORY_LOCK_FILE, amPostmaster, "", true, DataDir);
}

     对于CreateLockFile()函数,共有5个参数,分别是:filename、amPostmaster、socketDir、isDDLock、refName。 它们所代表的意义分别如下:

     · filename

     要创建的锁文件名, 即postmaster.pid。它由预定义宏DIRECTORY_LOCK_FILE表示,位于src/backend/utils/init/miscinit.c文件中,如下:

#define DIRECTORY_LOCK_FILE        "postmaster.pid"

     · amPostmaster
     用于确定如何对输出的PID进行编码,即如果它为true,则于postmaster.pid文件中写入当前守护进程postmaster的PID(getpid()),反之则向该文件写入“-PID”,即在进程的PID数字的前面追加一个“-”符号。

     实测:为了验证该结果,我们将CreateDataDirLockFile(true);函数中的参数置为false。 如下:

CreateDataDirLockFile(false); //该入口位于 src/backend/postmaster/postmaster.c line:982

     修改之后重新启动PostgreSQL服务进程,再来看postmaster.pid文件的第1行数据,可以看到在进程PID的前面多了一个横杆字符“-”(因此,这里的所谓编码并不是常见的例如MD5等算法编码)。如下图所示:

在这里插入图片描述

     · socketDir

     socketDir是Unix套接字目录路径,它可能为空,

     · isDDLock

     用于说明是否为创建postmaster.pid锁文件.

     · refName

     指明当前要创建的文件的绝对路径目录位置. 同时isDDLock和refName这两个参数也用于确定生成什么错误的日志消息。

2.2 postmaster.pid创建逻辑图

     postmaster.pid文件的底层创建逻辑流程图如下所示:

在这里插入图片描述

2.3.1 postmaster.pid创建过程代码分析

     首先获取当前运行环境中的进程PID、父进程PPID。一方面是因为postmaster.pid文件中需要写入进程PID值,另外一方面是在打开文件失败时候用获取到的PID、PPID进行一些列的业务逻辑处理判断。考虑到postgres服务在启动过程中,会有若干的外界环境因素(比如存在竞争条件, 同一时间多个用户同时启动等)影响,在打开文件的时候需要有一个循环,但是又不能永远循环下去(比如一个不可写的数据蔟目录可能会导致永远的失败),因此,当前策略是循环100次尝试。若100次都无法成功打开文件或是创建文件,则结束任务。

     以可读写的方式来打开指定的postmaster.pid文件,并附加上O_CREAT | O_EXCL标志,若文件不存在则,则创建该锁文件。反之若指定了O_CREAT标志且文件已经存在在报错。并对open返回的错误码进行相应的判断处理, 若错误码errno不为EEXIST(文件已经存在)且同时不为EACCES(对文件请求访问不允许), 又或者尝试次数(ntries > 100)大于指定的100次,则打印报错信息,继续向下处理。

     若文件打开且错误码不为上面两个,那么继续尝试打开该文件,若文件打开仍然失败,但错误码errno为ENOENT,那么继续打开文件。则继续往下走。

if (errno == ENOENT)
        continue;        /* race condition; try again */

     之后便读取该文件的所有内容,读取到文件的末尾之后则关闭该文件句柄fd。若读取出来的文件内容大小为0字节,那么有两种可能:

     (1) 另外一个服务器正在启动。

     (2) 该锁定文件是上一个服务器启动时候奔溃时留下的残余。

     若文件内容长度(len != 0)大于0,则获取该文件中的进程PID(正如前面所说,postmaster.pid文件的第一行为postmaster进程的PID)。

#define MAXPGPATH        1024
char        buffer[MAXPGPATH * 2 + 256];

buffer[len] = '\0';
encoded_pid = atoi(buffer);

     若果文件中的PID小于0, 那么这个进程PID值是postgres进程而非postmaster进程的。很显然这是一个不正常的现象,则打印错误提示信息。

/* if pid < 0, the pid is for postgres, not postmaster */
other_pid = (pid_t) (encoded_pid < 0 ? -encoded_pid : encoded_pid);

if (other_pid <= 0)
    elog(FATAL, "bogus data in lock file \"%s\": \"%s\"",
         filename, buffer);

     现在我们需要去检查另外一个进程是否仍然存在的情况,使用从文件中读取出来的PID(other_pid )和当前进程PID、父进程PPID的值进行比较,若other_pid同时不等于进程PID和父进程PPID,则kill掉other_pid进程。若kill信号发送成功,同时错误码不等于ESRCH和EPERM,那么表名当前的postmaster.pid锁文件属于以下进程之一。

if (other_pid != my_pid && other_pid != my_p_pid &&
            other_pid != my_gp_pid)
{
    if (kill(other_pid, 0) == 0 ||
        (errno != ESRCH && errno != EPERM))
    {
        /* lockfile belongs to a live process */
        ereport(FATAL,
                (errcode(ERRCODE_LOCK_FILE_EXISTS),
                 errmsg("lock file \"%s\" already exists",
                        filename),
                 isDDLock ?
                 (encoded_pid < 0 ?
                  errhint("Is another postgres (PID %d) running in data directory \"%s\"?",
                          (int) other_pid, refName) :
                  errhint("Is another postmaster (PID %d) running in data directory \"%s\"?",
                          (int) other_pid, refName)) :
                 (encoded_pid < 0 ?
                  errhint("Is another postgres (PID %d) using socket file \"%s\"?",
                          (int) other_pid, refName) :
                  errhint("Is another postmaster (PID %d) using socket file \"%s\"?",
                          (int) other_pid, refName))));
    }
}

EPERM与ESRCH信号所代表的含义分别如下:
EPERM: 进程没有向任何目标进程发送信息的权限。
ESRCH:进程PID或进程组PGID不存在。注意,现有的进程可能是僵尸进程,虽已经提交终止请求,但是还没有被(wait)回收。

     实测:lsof先查看监听端口为5432的postmaster进程情况,如下图所示,可看到进程已经存在且PID是4451,现在使用pg_ctl命令尝试新启动一个postmaster服务,可看到pg_ctl命令报错,并提示或许有另一个进程在跑。而指定的1.log文件中,则可以清楚地看到上面的代码中指定的信息打印。即

errmsg("lock file \"%s\" already exists", filename)  //filename这里是"postmaster.pid"

isDDLock ?
(encoded_pid < 0 ?
 errhint("Is another postgres (PID %d) running in data directory \"%s\"?",
      (int) other_pid, refName) :

//因为isDDLock = true 且 other_pid大于0, 所以走下面这条打印。和下图的截图能够相匹配。
 errhint("Is another postmaster (PID %d) running in data directory \"%s\"?",
      (int) other_pid, refName)) :
(encoded_pid < 0 ?
 errhint("Is another postgres (PID %d) using socket file \"%s\"?",
      (int) other_pid, refName) :
 errhint("Is another postmaster (PID %d) using socket file \"%s\"?",
      (int) other_pid, refName))));

     终端日志报错提示信息如下图所示:

在这里插入图片描述
     继续一些其他逻辑处理判断,若满足条件则跳转到写postmaster.pid文件的业务分支,反之,则结束本次处理流程。

     当postmaster.pid锁文件成功打开之后,返回对应的文件句柄fd,之后按照约定的格式构造5行数据,写入该文件句柄fd中。构造写入数据的完整代码如下:

snprintf(buffer, sizeof(buffer), "%d\n%s\n%ld\n%d\n%s\n",
         amPostmaster ? (int) my_pid : -((int) my_pid),   //getpid()
         DataDir,                                          //数据目录
         (long) MyStartTime,                              //postmaster启动时间
         PostPortNumber,                                  //postmaster监听端口,默认5432
         socketDir);                                      //第一个Unix套接字目录路径,这个一开始为空

     其中 MyStartTime(第3行)是一个全局变量,位于src/backend/utils/init/globals.c文件中,其类型是双长整型(long long int)。

#ifndef HAVE_INT64
typedef long long int int64;
#endif
typedef int64 pg_time_t;
pg_time_t    MyStartTime;  //

     MyStartTime变量是在PostmasterMain()函数入口处进行初始化的,这样能够更加精确地去表示postmaster守护进程的启动时间。

void InitProcessGlobals(void)
{
    unsigned int rseed;

    MyProcPid = getpid();
    MyStartTimestamp = GetCurrentTimestamp();
    MyStartTime = timestamptz_to_time_t(MyStartTimestamp);
    . . . . . . //省略若干
}
void PostmasterMain(int argc, char *argv[])
{
    int            opt;
    int            status;
    char       *userDoption = NULL;
    bool        listen_addr_saved = false;
    int            i;
    char       *output_config_variable = NULL;

    InitProcessGlobals();
    . . . . . . //省略若干
}

     当数据(buffer)格式化之后,便写入到postmaster.pid锁文件中,同时调用fsync同步刷新数据到磁盘文件中去。之后再对文件句柄fd设置一些其他高级属性,然后关闭该文件句柄fd。到这里时候,整个锁文件从创建到写数据等过程就已经详细讲解完成了。

3. 总结

     本文详细的讲解了PostgreSQL数据库中postmaster.pid文件的作用以及文件中的内容。比较深入地分析了该文件创建过程中的底层内核源码,图文并茂地进行了剖析。同时本文还附加说明了什么是套接字锁文件,以及套接字锁文件的位置和文件中的数据内容。