PostgreSQL 动态库加载过程

在使用 pg_backtrace 插件时,必须执行 SELECT pg_backtrace_init();,不执行该函数插件就不生效,官方文档也特别说明了,而实际上该函数是一个空函数,为什么必须要执行该函数才生效呢?

这是一个朋友问我关于 pg_backtrace 的问题,这主要涉及到 PostgreSQL 中动态库的加载过程。本文在分析问题的基础之上,简要梳理一下 PostgreSQL 中动态库的加载过程。

重现

首先编译安装 PostgreSQL 数据库(16devel),随后下载 pg_backtrace 编译安装。接着初始化数据库并启动,最后添加 pg_backtrace 插件。

1
2
3
$ initdb -D testdb
$ pg_ctl -l log -D testdb start
$ psql postgres -c 'CREATE EXTENSION pg_backtrace'

一切准备就绪,我们来重现一下问题,使用 psql 登录数据库,并执行下面的 SQL。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
postgres=# SELECT 1 / 0;
ERROR: division by zero

postgres=# SELECT pg_backtrace_init();
pg_backtrace_init
-------------------

(1 row)

postgres=# SELECT 1 / 0;
ERROR: division by zero
CONTEXT: postgres: japin postgres [local] SELECT(int4div+0x6e) [0x564f9872a279]
postgres: japin postgres [local] SELECT(+0x386543) [0x564f983e3543]
postgres: japin postgres [local] SELECT(ExecInterpExprStillValid+0x53) [0x564f983e56be]
postgres: japin postgres [local] SELECT(+0x51553a) [0x564f9857253a]
postgres: japin postgres [local] SELECT(evaluate_expr+0xa1) [0x564f9857a364]
postgres: japin postgres [local] SELECT(+0x51c4ca) [0x564f985794ca]
postgres: japin postgres [local] SELECT(+0x51b627) [0x564f98578627]
postgres: japin postgres [local] SELECT(+0x518b5d) [0x564f98575b5d]
postgres: japin postgres [local] SELECT(expression_tree_mutator+0x1500) [0x564f984ad27f]
postgres: japin postgres [local] SELECT(+0x51af88) [0x564f98577f88]
postgres: japin postgres [local] SELECT(expression_tree_mutator+0x197b) [0x564f984ad6fa]
postgres: japin postgres [local] SELECT(+0x51af88) [0x564f98577f88]
postgres: japin postgres [local] SELECT(eval_const_expressions+0x6f) [0x564f98575168]
postgres: japin postgres [local] SELECT(+0x4ee403) [0x564f9854b403]
postgres: japin postgres [local] SELECT(subquery_planner+0x56a) [0x564f9854a8b8]
postgres: japin postgres [local] SELECT(standard_planner+0x2b8) [0x564f98549a86]
postgres: japin postgres [local] SELECT(planner+0x58) [0x564f985497c4]
postgres: japin postgres [local] SELECT(pg_plan_query+0x7f) [0x564f9868e277]
postgres: japin postgres [local] SELECT(pg_plan_queries+0xfa) [0x564f9868e3c1]
postgres: japin postgres [local] SELECT(+0x63179f) [0x564f9868e79f]
postgres: japin postgres [local] SELECT(PostgresMain+0x7b5) [0x564f986933ef]
postgres: japin postgres [local] SELECT(+0x55e0c2) [0x564f985bb0c2]
postgres: japin postgres [local] SELECT(+0x55d9b0) [0x564f985ba9b0]
postgres: japin postgres [local] SELECT(+0x559c28) [0x564f985b6c28]
postgres: japin postgres [local] SELECT(PostmasterMain+0x1367) [0x564f985b63d9]
postgres: japin postgres [local] SELECT(+0x41d5f5) [0x564f9847a5f5]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xe7) [0x7f3feebc2c87]
postgres: japin postgres [local] SELECT(_start+0x2a) [0x564f981282ba]

从上面的结果可以看到我们需要执行 pg_backtrace() 函数,才能在错误消息中打印出详细的堆栈信息。那么它做了什么呢?我前面已经提到了,它什么都没有做,其实现如下所示:

1
2
3
4
Datum pg_backtrace_init(PG_FUNCTION_ARGS)
{
PG_RETURN_VOID();
}

既然它什么都没有做,为什么前后两次的执行效果不一致呢?它必然在后面做了一些操作。

分析

既然是 pg_backtrace_init() 函数引起的不同,那么我们就针对这个函数来看看它都干了什么,我们可以通过 gdb 附加进程并在该函数打断点从而确定其调用栈。

1
2
3
4
(gdb) b pg_backtrace_init
Function "pg_backtrace_init" not defined.
Make breakpoint pending on future shared library load? (y or [n]) y
Breakpoint 1 (pg_backtrace_init) pending.

从上面的提示,您是否已经猜到答案了?如果没有请继续看下面的分析。

设置好断点之后,我们来执行 SELECT pg_backtrace_init();,可以看到程序在 pg_backtrace_init() 处停止下来,查看堆栈发现并没有什么特别的,我们在继续执行也没有什么有关 pg_backtrace 的特殊处理。这是怎么回事呢?我们在回头来看看 gdb 下断点时给出的提示信息,在当前的代码中没有找到 pg_backtrace_init() 函数,使断点在将来的共享库加载时挂起。我们实际上也在 pg_backtrace_init() 处停止下来了,那么这个动态库肯定是加载了。它又是何时加载的呢?

既然会加载动态库,我们知道 PostgreSQL 在加载动态库时会执行 _PG_init() 函数,我们看看 pg_backtrace 中该函数的实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void _PG_init(void)
{
signal_handlers[SIGSEGV] = pqsignal(SIGSEGV, backtrace_handler);
signal_handlers[SIGBUS] = pqsignal(SIGBUS, backtrace_handler);
signal_handlers[SIGFPE] = pqsignal(SIGFPE, backtrace_handler);
signal_handlers[SIGINT] = pqsignal(SIGINT, backtrace_handler);
prev_executor_run_hook = ExecutorRun_hook;
ExecutorRun_hook = backtrace_executor_run_hook;

prev_utility_hook = ProcessUtility_hook;
ProcessUtility_hook = backtrace_utility_hook;

prev_post_parse_analyze_hook = post_parse_analyze_hook;
post_parse_analyze_hook = backtrace_post_parse_analyze_hook;

DefineCustomEnumVariable("pg_backtrace.level",
"Set error level for dumping backtrace",
NULL,
&backtrace_level,
ERROR,
backtrace_level_options,
PGC_USERSET, 0, NULL, NULL, NULL);
}

它添加了三个 hook,拦截了 4 个信号,同时定义了一个配置参数。看到这里我们就能猜到为什么执行 pg_backtrace_init() 之后结果不一样了。空函数 pg_backtrace_init() 主要的作用是加载 pg_backtracs.so 动态库,一旦该动态库加载之后,pg_backtrace 就可以执行自定义 hook 来进行处理。这里就不在对 hook 的具体实现做分析。

1
2
3
4
5
6
7
8
(gdb) b _PG_init
Function "_PG_init" not defined.
Make breakpoint pending on future shared library load? (y or [n]) y
Breakpoint 1 (_PG_init) pending.
(gdb) b pg_backtrace_init
Function "pg_backtrace_init" not defined.
Make breakpoint pending on future shared library load? (y or [n]) y
Breakpoint 2 (pg_backtrace_init) pending.

当我们再次执行 SELECT pg_backtrace_init() 时,您可以看到如下的堆栈信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#0  _PG_init () at pg_backtrace.c:142
#1 0x0000564f98857ec5 in internal_load_library (
libname=0x564f9924a198 "/mnt/workspace/postgresql/build/pg/lib/pg_backtrace.so")
at /mnt/workspace/postgresql/build/../src/backend/utils/fmgr/dfmgr.c:289
#2 0x0000564f98857873 in load_external_function (
filename=0x564f9924a0c8 "$libdir/pg_backtrace",
funcname=0x564f9924a090 "pg_backtrace_init", signalNotFound=true,
filehandle=0x7fff9a88fde8)
at /mnt/workspace/postgresql/build/../src/backend/utils/fmgr/dfmgr.c:116
#3 0x0000564f9885963d in fmgr_info_C_lang (functionId=16385, finfo=0x564f9924a000,
procedureTuple=0x7f3fe59defd8)
at /mnt/workspace/postgresql/build/../src/backend/utils/fmgr/fmgr.c:400
#4 0x0000564f9885912d in fmgr_info_cxt_security (functionId=16385,
finfo=0x564f9924a000, mcxt=0x564f99249800, ignore_security=false)
at /mnt/workspace/postgresql/build/../src/backend/utils/fmgr/fmgr.c:248
#5 0x0000564f98858dcf in fmgr_info (functionId=16385, finfo=0x564f9924a000)
at /mnt/workspace/postgresql/build/../src/backend/utils/fmgr/fmgr.c:128
#6 0x0000564f983dde2a in ExecInitFunc (scratch=0x7fff9a890400, node=0x564f991835f0,
args=0x0, funcid=16385, inputcollid=0, state=0x564f99249f70)
at /mnt/workspace/postgresql/build/../src/backend/executor/execExpr.c:2748
#7 0x0000564f983d9fda in ExecInitExprRec (node=0x564f991835f0, state=0x564f99249f70,

即 pg_backtrace 插件是通过 internal_load_library() 函数加载的,我们在 gdb 中执行
continue,它会执行 pg_backtrace_init() 函数。

1
2
3
4
5
(gdb) continue
Continuing.

Breakpoint 2, pg_backtrace_init (fcinfo=0x564f9924a058) at pg_backtrace.c:182
182 PG_RETURN_VOID();

分析到这里,我们已经清楚了为什么 pg_backtrace_init() 函数为空,而 pg_backtrace 却能在执行该函数之后输出堆栈信息了。

探索

根据上面的分析,要想使用使用 pg_backtrace 提供的功能,我们仅需要加载动态库即可。我们知道 PostgreSQL 提供了 shared_preload_librariessession_preload_libraries 来个参数指定需要添加的动态库。其中 shared_preload_libraries 是所有后端进程(backend)共享的,而 session_preload_libraries 则是单个会话的。那我们将其添加到这个两个参数中的其中一个是否不需要执行 pg_backtrace_init() 函数即可使用 pg_backtrace 提供的功能呢?答案当然是不需要了。

但是,当我将其配置在 shared_preload_libraries 参数中时,我通过 pg_ctl stop [-m fast] 却不能将数据库停下,这又是为什么呢?如果使用 pg_ctl stop -m { smart | immediate } 则可以将数据库停下。

通过查看源码,我们知道 pg_ctl stop [-m fast] 实际上是发送的 SIGINT 信号给主进程,我们注意到 pg_backtrace 在加载时拦截了 SIGINT 信息,其信号处理函数如下所示:

1
2
3
4
5
6
7
8
9
static void
backtrace_handler(SIGNAL_ARGS)
{
inside_signal_handler = true;
elog(LOG, "Caught signal %d", postgres_signal_arg);
inside_signal_handler = false;
if (postgres_signal_arg != SIGINT && signal_handlers[postgres_signal_arg])
signal_handlers[postgres_signal_arg](postgres_signal_arg);
}

当遇到 SIGINT 信号时,pg_backtrace 直接将其忽略了,这就是为什么主进程在执行 pg_ctl stop [-m fast] 不能停机的原因。

那么为什么设置在 session_preload_libraries 中又可以呢?这就涉及到两个参数加载的时机问题以及 PostgreSQL 父子进程对于 SIGINT 信号的处理了。下图简要给出了这两个参数的处理流程:

PostgreSQL 在主进程中使用 SIGINT 作为停机的一种方式,它会其子进程发送 SIGTERM 信号,而子进程(不一定是所有的子进程)则使用 SIGINT 作为取消查询的一种方式。因此,即便我们将 pg_backtrace 设置在 session_preload_libraries 中,依然会导致问题。当然,只要您使用了该插件,都会遇到这个问题,要么是无法使用 pg_ctl stop [-m fast] 正常停机(配置在 shared_preload_libraries 中),要么是无法取消正在执行的查询(配置在 session_preload_librarieslocal_preload_libraries 或者执行 pg_backtrace_init())。

总结

从这个过程中,我们对 PostgreSQL 的动态库加载有了一个基本的认识,它提供了三种加载方式。

  1. 通过 process_shared_preload_libraries() 全局加载。
  2. 通过 process_session_preload_libraries() 仅在后端进程中加载。
  3. 通过 load_external_function() 按需动态加载。
笑林广记 - 借牛

有走柬借牛于富翁者,翁方对客,讳不识字,伪启缄视之,对来使曰:“知道了,少刻我自来也。”