
大家好,今天和大家聊聊Postgres 的HOOK 机制以及Extension。
上一篇 《Postgres extension 的新春祝福(2024 版)》 https://www.modb.pro/db/1755155378081976320 我们用最简单的方式制作了自己的第一个extension :Happy2024!
今天我们站在方法论的角度来看看PG的extension 和 hook程序。
我们先来看看HOOK (钩子程序): 什么是钩子程序?
来自权威的维基百科:https://en.wikipedia.org/wiki/Hooking

维基百科中提到了设计模式中的模板方法模式(template method design pattern): 这个设计模式对于国内基数众多的JAVA程序员是再熟悉不过了。
父类FrameworkClass定义好了方法的名称,方法的步骤以及顺序:
templatedMethod() including:
stepOne();
stepTwo();
stepThree();
子类applicationClaaOne和applicationClassTwo 分别实现了自己的stepTwo()方法的override。

我们具体来看一下postgres 中hook的实现机制:
PG 通过全局指针函数(global pointer),来判断PG中预制的各种HOOK是否为空, 如果不为空,则指向用户自定义的HOOK函数(注册用户的自定义 hook)。
如果参数shared_preload_library 中包含多个插件,并且存在多个插件都修改同一个HOOK的情况,那么会把previous hook 保存记录下来, 如果 previous hook 不为空,
会在自己开发中的HOOK中调用previous hook, 从而避免了后面的HOOK覆盖掉之前HOOK的逻辑,类似于 HOOK 链的设计。
当PG启动的时候,会调用方法__PG__init()来加载shared library 的 *.so 文件:
当PG关系实例的时候, 会调用方法__PG___finit()来关闭掉shared library 中加载的插件。
PG 预制HOOK的种类:
General Hooks
Security Hooks
Function Manager Hooks
Planner Hooks
Executor Hooks
PL/pgsql Hooks
开发详情可以参考github: https://github.com/taminomara/psql-hooks/blob/master/Detailed.md#security-hooks
我们尝试用colin debug 一下PG 15.3 的源代码,观察一下hook加载的过程:
我们修改postgres.conf,中的参数shared_preload_libraries,启动的时候加载2个插件:pg_stat_statements和auto_explain
shared_preload_libraries='pg_stat_statements,auto_explain'
我们把断点打在源码: src/backend/postmaster/postmaster.c 的函数: process_shared_preload_libraries();

我们debug模式启动实例: 启动程序指到 erc/backend/postgres 指定参数 -D 数据库目录

点击debug按钮之后,可以看到程序来到了我们的断点:

我们进入函数load_libaries 继续打断点:

我们可以看到 libraries 是值 是我们的GUC 的shared_preload_libraries值:是一个字符串形式。

接下来会把字符串按照逗号拆分放入 elemlist, 进行循环加载调用函数 load_file.

我们进去核心函数load_file, 观察一下动态链接文件是如何加载的:
我们看到 internal_load_library 这个函数是实际的加载链接文件的入口:

函数internal_load_library注解是:
加载指定的动态链接文件,返回指针pg_dl的文件句柄。
动态库名称和指定的动态文件命名成是完全一致的。
当前还没有动态卸载库文件的功能,我们可能会在未来会添加此功能这样可以方便我们安全的从hook函数的指针中卸载hook程序,清除GUC参数,或者解决当前一些不安全的隐患。
/*
* Load the specified dynamic-link library file, unless it already is
* loaded. Return the pg_dl* handle for the file.
*
* Note: libname is expected to be an exact name for the library file.
*
* NB: There is presently no way to unload a dynamically loaded file. We might
* add one someday if we can convince ourselves we have safe protocols for un-
* hooking from hook function pointers, releasing custom GUC variables, and
* perhaps other things that are definitely unsafe currently.
*/
static void *
internal_load_library(const char *libname)
扫描一下文件,查看是否已经加载过
/*
* Scan the list of loaded FILES to see if the file has been loaded.
*/
for (file_scanner = file_list;
file_scanner != NULL &&
strcmp(libname, file_scanner->filename) != 0;
file_scanner = file_scanner->next)
调用底层 操作系统OS 函数 dlopen 打开动态链接文件

通过dlsym 调用动态加载文件中的 PG_MODULE_MAGIC 判断插件程序的兼容性。
如果存在不兼容的情况,则会调用dlclose(file_scanner->handle); 卸载动态库

验证完插件的兼容性之后,会调用插件中的初始化函数 _PG_init(void)

在pg_stat_statements.c 文件中, 初始化函数 _PG_init(void) 完成了:
- EnableQueryId 设置SQL 的query ID
2.DefineCustomIntVariable,DefineCustomEnumVariable,DefineCustomBoolVariable 设置用户自己定义的GUC变量
3.install hook – 加载钩子函数
/*
* Install hooks.
*/
prev_shmem_request_hook = shmem_request_hook;
shmem_request_hook = pgss_shmem_request;
prev_shmem_startup_hook = shmem_startup_hook;
shmem_startup_hook = pgss_shmem_startup;
prev_post_parse_analyze_hook = post_parse_analyze_hook;
post_parse_analyze_hook = pgss_post_parse_analyze;
prev_planner_hook = planner_hook;
planner_hook = pgss_planner;
prev_ExecutorStart = ExecutorStart_hook;
ExecutorStart_hook = pgss_ExecutorStart;
prev_ExecutorRun = ExecutorRun_hook;
ExecutorRun_hook = pgss_ExecutorRun;
prev_ExecutorFinish = ExecutorFinish_hook;
ExecutorFinish_hook = pgss_ExecutorFinish;
prev_ExecutorEnd = ExecutorEnd_hook;
ExecutorEnd_hook = pgss_ExecutorEnd;
prev_ProcessUtility = ProcessUtility_hook;
ProcessUtility_hook = pgss_ProcessUtility;
我们可以看到在HOOK的实现上,如果预制的HOOK已经被加载,则是 优先执行之前的PREVIOUS hook.
这样才能保证插件彼此时间不存在互相覆盖的现象。

整个加载extension 流程图如下:

Have a fun 🙂 !
References:
https://github.com/taminomara/psql-hooks/blob/master/Detailed.md#security-hooks
https://en.wikipedia.org/wiki/Hooking




