更多"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文件的作用以及文件中的内容。比较深入地分析了该文件创建过程中的底层内核源码,图文并茂地进行了剖析。同时本文还附加说明了什么是套接字锁文件,以及套接字锁文件的位置和文件中的数据内容。