信号这篇实际上是要说说配置文件的加载问题,有些标题党了。准确说是如何在线加载配置,用户通过向postgres进程发送信号SIGHUP触发服务端完成配置的刷新。这部分看下来代码还是有点多,挑选几段我认为比较重要的片段和大家一起学习一下。
信号也是进程间通信的一种手段,之前在《postgresql 15源码浅析(1)—— postgres中的1号数据库》介绍了通过管道完成向psql客户端进程传递BKI命令的进程间通信方式,同样,信号是另一种进程间传递信息的方式,相对传统的共享内存和消息队列方式,信号要简单的多。
信号简介信号机制是一种使用信号来进行进程之间的传递消息的方法,其中信号的全称为软中断信号,简称软中断。
我们最常用的一个就是 9) SIGKILL ,kill -9 ,我想这个大家应该都用过。实际上,Linux内核支持64种不同的信号,这些信号种的大部分都有了预先定义好的意义,但是都支持自定义动作,并且还提供了类似SIGUSR1这样由应用程序来定义的信号。
同样在C./C++中也可以使用类似方法,使用kill函数,传入进程号pid和信号值sig,即可以向指定进程发送特定的信号。
信号的处理我们处理信号的方式大体有3种:
- 捕获信号,并忽略它,常用于在某些处理过程中不希望被终端打断的场景,如某些初始化过程。
- 不做任何捕获操作,使用信号的默认行为,信号的默认行为大部分是要终止进程。
- 捕获信号,并重新赋予被捕获信号新的使命。这里点题了。“信号1的使命”。
信号实际上在核心场景下并不是一个好的选择,它涉及中断,同时需要内核态和用户态的切换,效率不高,开销太大,且与其它进程通信方式相比,能够传递的信息有限。
再推荐一本书吧,有兴趣转行的同学可以看看《Linux C程序设计大全》,是一本贼厚的蓝皮书,也是我入行时的第一本工具书。
修改PostgreSQL的配置 配置文件在哪?在初始化之后,数据库集簇目录下回创建4个配置文件。
也可以通过命令查看配置文件所在位置
配置文件生效的方法修改配置文件后,配置项生效的方法共有四种:
- 重新启动数据库服务,这是网吧管理员的做法,除非是一些参数要求重启服务,否则不需要这么干;
- 使用超级用户执行 select pg_reload_conf(); 留个坑,后续补充这个函数的执行流程;
- bash下执行 kill -HUP 或 kill -1 ,这个就是今天要撸的代码;
- 使用pg_ctl工具执行 pg_ctl reload 这个命令实际上就是触发了SIGHUP信号;
上文提到,不是所有的配置都可以动态的加载生效的,有些配置需要重新启动数据库服务。
通过select distinct(context) from pg_settings;命令列出所有配置的类型(共7种)。
postgres@/tmp:postgres> select distinct(context) from pg_settings; +-------------------+ | context | |-------------------| | postmaster | | superuser-backend | | user | | internal | | backend | | sighup | | superuser | +-------------------+ SELECt 7 Time: 0.005s
对应的正是代码中GucContext的枚举类型(共7种)。
typedef enum
{
PGC_INTERNAL,
PGC_POSTMASTER,
PGC_SIGHUP,
PGC_SU_BACKEND,
PGC_BACKEND,
PGC_SUSET,
PGC_USERSET
} GucContext;
注释中对每个选项的表述都比较良心,等级是从严到松的顺序。
如果配置项的context小于PGC_SIGHUP,那么需要重新启动生效,当然PGC_INTERNAL是只读的,当然PGC_INTERNAL的配置也不会出现在postgresql.conf文件中,所以只要context属性是PGC_POSTMASTER的配置项需要重启后会生效。
看一下这些配置在PostgreSQL 15中的分布情况:
postgres@/tmp:postgres> select context,count(*) from pg_settings group by context; +-------------------+-------+ | context | count | |-------------------+-------| | postmaster | 55 | | superuser-backend | 4 | | user | 136 | | internal | 20 | | backend | 2 | | sighup | 92 | | superuser | 44 | +-------------------+-------+ SELECT 7 Time: 0.006s postgres@/tmp:postgres>
可以通过查询pg_settings表的context字段来确定,修改的配置是否允许动态加载。
信号处理流程整理流程如上图,1.进程启动时,注册信号捕获处理函数;2.用户发送信号,并被进程捕获,交由SIGHUP_handler处理;3.主进程先处理配置文件重加载流程,并把信号传递个各个子进程;4.分别对hba和ident两个配置文件进程加载处理;
注册信号处理函数void PostmasterMain(int argc, char *argv[])
在父进程(主进程)postmaster的入口函数PostmasterMain中注册信号处理函数。
pqsignal_pm(SIGHUP, SIGHUP_handler); pqsignal_pm(SIGINT, pmdie); pqsignal_pm(SIGQUIT, pmdie); pqsignal_pm(SIGTERM, pmdie); pqsignal_pm(SIGALRM, SIG_IGN); pqsignal_pm(SIGPIPE, SIG_IGN); pqsignal_pm(SIGUSR1, sigusr1_handler); pqsignal_pm(SIGUSR2, dummy_handler); pqsignal_pm(SIGCHLD, reaper);
其中pqsignal_pm(SIGHUP, SIGHUP_handler);注册了对新SIGHUP的处理函数SIGHUP_handler,
SIGHUP_handler核心代码:
ereport(LOG,(errmsg("received SIGHUP, reloading configuration files")));
ProcessConfigFile(PGC_SIGHUP);
SignalChildren(SIGHUP);
if (StartupPID != 0)
signal_child(StartupPID, SIGHUP);
if (BgWriterPID != 0)
signal_child(BgWriterPID, SIGHUP);
if (CheckpointerPID != 0)
signal_child(CheckpointerPID, SIGHUP);
if (WalWriterPID != 0)
signal_child(WalWriterPID, SIGHUP);
if (WalReceiverPID != 0)
signal_child(WalReceiverPID, SIGHUP);
if (AutoVacPID != 0)
signal_child(AutoVacPID, SIGHUP);
if (PgArchPID != 0)
signal_child(PgArchPID, SIGHUP);
if (SysLoggerPID != 0)
signal_child(SysLoggerPID, SIGHUP);
if (!load_hba())
ereport(LOG,
(errmsg("%s was not reloaded", "pg_hba.conf")));
if (!load_ident())
ereport(LOG,
(errmsg("%s was not reloaded", "pg_ident.conf")));
- 处理配置文件加载ProcessConfigFile。
- 将捕获信号传递给子进程signal_child,子进程kill(-pid, signal);
- 加载hba配置文件load_hba();
- 加载ident配置文件load_ident();
子进程收到父进程传递过来的信号好,进入处理流程。
以WalWriter进程为例,入口函数WalWriterMain在backend/postmaster/walwriter.c中。主要处理过程为与postmaster主进程类似,最终也将调用guc_file.c中的ProcessConfigFile函数;
子进程的处理需要注意的是,收到HUP信号后,并不是立即进入处理流程,而是设置重载标志,在下一次loop forever中检查标志,如果为true则调用配置文件重新加载的流程。
- 设置标记ConfigReloadPending
void
SignalHandlerForConfigReload(SIGNAL_ARGS)
{
int save_errno = errno;
ConfigReloadPending = true;
SetLatch(MyLatch);
errno = save_errno;
}
- 读取标记ConfigReloadPending,是否需要重新加载配置文件
static void
HandleWalWriterInterrupts(void)
{
if (ProcSignalBarrierPending)
ProcessProcSignalBarrier();
if (ConfigReloadPending)
{
ConfigReloadPending = false;
ProcessConfigFile(PGC_SIGHUP);
}
if (ShutdownRequestPending)
{
pgstat_report_wal(true);
proc_exit(0);
}
if (LogMemoryContextPending)
ProcessLogMemoryContextInterrupt();
}
ProcessConfigFile
改函数时处理配置文件重新加载的核心部分,整体流程如下:
这部分代码量比较大,宏观的看一下重点,这部分处理使用了递归处理,主要是处理postgressql.conf中include子配置文件是需要递归。
if (guc_name_compare(opt_name, "include_dir") == 0)
{
if (!ParseConfigDirectory(opt_value,
config_file, ConfigFileLineno - 1,
depth + 1, elevel,
head_p, tail_p))
……
}
else if (guc_name_compare(opt_name, "include_if_exists") == 0)
{
if (!ParseConfigFile(opt_value, false,
config_file, ConfigFileLineno - 1,
depth + 1, elevel,
head_p, tail_p))
……
}
else if (guc_name_compare(opt_name, "include") == 0)
{
if (!ParseConfigFile(opt_value, true,
config_file, ConfigFileLineno - 1,
depth + 1, elevel,
head_p, tail_p))
……
}
else
{
……
}
且递归深度不能超过10级。
if (depth > 10)
{
ereport(elevel,
(errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
errmsg("could not open configuration file "%s": maximum nesting depth exceeded",
config_file)));
record_config_file_error("nesting depth exceeded",
calling_file, calling_lineno,
head_p, tail_p);
return false;
}
所有的配置保存在静态变量guc_variables中:
static struct config_generic **guc_variables;
config_generic结构如下:
struct config_generic
{
const char *name;
GucContext context;
enum config_group group;
const char *short_desc;
const char *long_desc;
int flags;
enum config_type vartype;
int status;
GucSource source;
GucSource reset_source;
GucContext scontext;
GucContext reset_scontext;
GucStack *stack;
void *extra;
char *last_reported;
char *sourcefile;
int sourceline;
};
注意
几种不能设置参数的情况,未一一列举:
ereport(elevel,(errcode(ERRCODE_INVALID_TRANSACTION_STATE),
errmsg("cannot set parameters during a parallel operation")));
case PGC_INTERNAL:
if (context != PGC_INTERNAL)
{
ereport(elevel,
(errcode(ERRCODE_CANT_CHANGE_RUNTIME_PARAM),
errmsg("parameter "%s" cannot be changed",
name)));
return 0;
}
break;
else if (context != PGC_POSTMASTER)
{
ereport(elevel,
(errcode(ERRCODE_CANT_CHANGE_RUNTIME_PARAM),
errmsg("parameter "%s" cannot be changed without restarting the server",
name)));
return 0;
}
case PGC_SIGHUP:
if (context != PGC_SIGHUP && context != PGC_POSTMASTER)
{
ereport(elevel,
(errcode(ERRCODE_CANT_CHANGE_RUNTIME_PARAM),
errmsg("parameter "%s" cannot be changed now",
name)));
return 0;
}
break;
case PGC_SU_BACKEND:
if (context == PGC_BACKEND)
{
AclResult aclresult;
aclresult = pg_parameter_aclcheck(name, GetUserId(), ACL_SET);
if (aclresult != ACLCHECK_OK)
{
ereport(elevel,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("permission denied to set parameter "%s"",
name)));
return 0;
}
}
结构
其中涉及一些内存结构:
整体调用逻辑:
总结这篇的内容比较多,有其是解析流程和参数设置的过程,这部分代码经过10几年的打磨,应该是很成熟了,但是面向过程的涉及显得代码有点冗长,后续看看openGauss对这部分的处理是否有改进。



