postgresql源码学习(57)—— pg中的四种动态库加载方法

一、 基础知识

1. 什么是库

       库其实就是一些通用代码,可以在程序中重复使用,比如一些数学函数,可以不需要自己编写,直接调用相关函数即可实现,避免重复造轮子。

      在linux中,支持两种类型的库:

1. 静态库

编译阶段将整个库复制到可执行文件。

  • 优点:不依赖外界库支持,具有良好的可移植性。
  • 缺点: 每次库更新都需要重新编译程序,即使更新很小或只是局部。每个静态库都有一份库文件,存储时增加了硬盘空间消耗,运行时则增加了内存消耗。
  • 命名规则:linux中为xxx.a,Windows中为xxx.lib

2. 动态(共享)库

直到运行时才将库链接到可执行程序。

  • 优点: 动态链接方式的程序不需要包含库,编译时节省时间,占用的空间小很多。运行时系统内存只需提供一个共享库给所有程序动态链接,内存消耗减少。
  • 缺点: 需要系统中动态库支持才可运行,可能有动态库不兼容问题
  • 命名规则:linux中为xxx.so,Windows中为xxx.dll

linux上的动态库路径:

  • 默认在 /usr/local/lib, /usr/local/lib64, /usr/lib, /usr/lib64
  • 系统启动的库文件在 /lib, /lib64。
  • pg共享库的路径在 <安装目录>/lib下,可以设置LD_LIBRARY_PATH环境变量,指定相关库的路径。

二、 pg中四种动态库加载方法

1. shared_preload_libraries变量

       在启动时预先加载,对整个pg实例均生效,修改需要重启。如果这个变量指向了一个不存在的共享库,会导致无法启动数据库

/*
 * Postmaster main entry point
 */
void
PostmasterMain(int argc, char *argv[])
{
    /*
     * process any libraries that should be preloaded at postmaster start
     */
    process_shared_preload_libraries();
…
}

所以为什么这个参数修改需要重启生效,因为只在DB启动时调一次。

/*
 * process any libraries that should be preloaded at postmaster start
 */
void
process_shared_preload_libraries(void)
{
    process_shared_preload_libraries_in_progress = true;
    load_libraries(shared_preload_libraries_string,
                   "shared_preload_libraries",
                   false);
    process_shared_preload_libraries_in_progress = false;
}

2. session_preload_libraries变量

       用于指定在连接创建时预先需要加载的共享库,修改不需要重启生效,但只允许超级用户修改。如果这个变量指向了一个不存在的共享库,会导致无法连接数据库

/* ----------------------------------------------------------------
 * PostgresMain
 *     postgres main loop -- all backends, interactive or otherwise start here
 * ----------------------------------------------------------------
 */
void
PostgresMain(int argc, char *argv[],
             const char *dbname,
             const char *username)
{
…
    /*
     * process any libraries that should be preloaded at backend start (this
     * likewise can't be done until GUC settings are complete)
     */
    process_session_preload_libraries();
…
}

可以看到进行了两个参数配置,session_preload_libraries和local_preload_libraries。

/*
 * process any libraries that should be preloaded at backend start
 */
void
process_session_preload_libraries(void)
{
    load_libraries(session_preload_libraries_string,
                   "session_preload_libraries",
                   false);
    load_libraries(local_preload_libraries_string,
                   "local_preload_libraries",
                   true);
}

3. local_preload_libraries变量

       用户建立连接时加载,任何用户都可以设置如果这个变量指向了一个不存在的共享库,会导致该用户无法连接数据库

    load_libraries(local_preload_libraries_string,
                   "local_preload_libraries",
                   true);

4. LOAD方式

这不是变量,是个命令载入一个共享库文件(加载单个插件),仅对当前会话生效。

LOAD 'pg_hint_plan';

该命令直接调用load_file函数(dfmgr.c文件),后面一起学习。

三、 源码学习

       可以看到,前面三个参数核心都是调用load_libraries函数,下层调用栈主要为:load_libraries()->load_file()->internal_load_library(),其中load_file也是LOAD命令的实现原理。

1. load_libraries函数

       该函数动态加载指定的共享库(即 PostgreSQL 插件)。接收一个字符串参数,即插件名(不包括扩展名 如.so或 .dll),尝试在多个目录下查找可加载的文件,并使用操作系统平台相关的动态链接库机制将其动态地装载到 PostgreSQL 进程中。这些目录包括:

  • PG LD_LIBRARY_PATH环境变量指定的路径
  • PG 软件所在的 lib 目录
  • 与 PG 数据库集群关联的插件目录

       加载成功后,插件代码就会被加载进入数据库的内存空间中,从而可以通过相关函数调用执行插件功能。

/*
 * 加载指定名称的共享库(即插件) 
* libraries:待加载插件名字列表,以逗号分隔
 * gucname:GUC设置项名字,用于错误消息
 * restricted:是否只加载 $libdir/plugins 路径下的插件 
*/
static void
load_libraries(const char *libraries, const char *gucname, bool restricted)
{
    char       *rawstring;  // 用于保存输入参数 libraries 的可写副本
    List       *elemlist;   // 用于保存 libraries 参数解析后的插件名字列表
    ListCell   *l;

    if (libraries == NULL || libraries[0] == '\0')
        return;                 /* nothing to do,没有需要加载的插件 */

    /* Need a modifiable copy of string */
    rawstring = pstrdup(libraries);

    /* Parse string into list of filename paths,将字符串解析为路径名字的列表 */
    if (!SplitDirectoriesString(rawstring, ',', &elemlist))
    {
        /* syntax error in list,列表中存在语法错误 */
        list_free_deep(elemlist);
        pfree(rawstring);
        ereport(LOG,
                (errcode(ERRCODE_SYNTAX_ERROR),
                 errmsg("invalid list syntax in parameter \"%s\"",
                        gucname)));
        return;
    }

    foreach(l, elemlist)
    {
        /* Note that filename was already canonicalized,文件名已被规范化 */
        char       *filename = (char *) lfirst(l);
        char       *expanded = NULL;

        /* If restricting, insert $libdir/plugins if not mentioned already */
        if (restricted && first_dir_separator(filename) == NULL)
        {
            expanded = psprintf("$libdir/plugins/%s", filename);
            filename = expanded;
        }
        /* 核心函数,加载插件文件 */
        load_file(filename, restricted);
        ereport(DEBUG1,
                (errmsg_internal("loaded library \"%s\"", filename)));
        if (expanded)
            pfree(expanded);
    }

    list_free_deep(elemlist);
    pfree(rawstring);
}

2. load_file函数

       该函数负责将共享库动态地装入一个独立的进程中。对于每个输入的插件名,该函数首先检查是否启用了安全限制,并对此进行必要的检查。然后,将可能缩写的文件名扩展为正确的完整路径,接着在内部逻辑中处理加载/卸载操作。调用函数 internal_load_library 加载插件,并确保内存映射中没有冲突,最后回收内存资源。

/*
 * This function loads a shlib file without looking up any particular
 * function in it.  If the same shlib has previously been loaded,
 * unload and reload it. 加载一个动态库文件,不去查找其中特定的函数。如果已经加载同名同地址的动态库,则重新加载。
 *
 * When 'restricted' is true, only libraries in the presumed-secure
 * directory $libdir/plugins may be referenced. 是否限制仅能引用预设安全目录 $libdir/plugins 下的动态库。

 */
void
load_file(const char *filename, bool restricted)
{
    char       *fullname;

    /* Apply security restriction if requested,如果需要,应用安全限制 */
    if (restricted)
        check_restricted_library_name(filename);

    /* Expand the possibly-abbreviated filename to an exact path name,将可能缩写的文件名扩展为正确的完整路径 */
    fullname = expand_dynamic_library_name(filename);

    /* Unload the library if currently loaded,如果当前已经加载了该库,则卸载之 */
    internal_unload_library(fullname);

    /* Load the shared library,核心函数,加载共享库 */
    (void) internal_load_library(fullname);

    pfree(fullname);
}

3. internal_load_library函数

  • 检查要安装的扩展,其文件是否可以访问,不能访问则报错退出。检查是否为已安装扩展的软链接或者硬链接,如果是,则直接返回该已安装扩展的 handle。
  • 调用 dlopen 加载动态库文件,dlopen 返回的就是一个动态库的 handle。
  • 调用 dlsym 获取动态库里的函数 Pg_magic_func,执行该函数,获取 Pg_magic_struct 结构体数据,并与标准的 magic_data 进行对比,进行动态库兼容性测试。
  • 调用 dlsym 获取动态库里的函数 _PG_init,如果有该函数,则执行该函数,进行动态库的初始化。
  • 将动态库 handle 对应的结构体加入到全局 file_list 链表中。
/*
 * Load the specified dynamic-link library file, unless it already is
 * loaded.  Return the pg_dl* handle for the file. 加载指定的动态链接库文件,除非它已经被加载。返回该文件的 pg_dl* 句柄。
 *
 * Note: libname is expected to be an exact name for the library file. 注意:libname 应该是一个准确的库文件名。
 */
static void *
internal_load_library(const char *libname)
{
    DynamicFileList *file_scanner;
    PGModuleMagicFunction magic_func;
    char       *load_error;
    struct stat stat_buf;
    PG_init_t   PG_init;

    /*
     * 扫描文件列表,查找指定的文件是否已经加载,如果是,直接返回该扩展的 handle。如果未加载,进行下一步。

首先定义了一个指针变量 file_scanner,并将其初始化为 file_list,即已加载文件列表的头指针。
执行循环条件判断,当满足以下两个条件之一时,跳出循环:
	file_scanner 为 NULL:已经扫描完所有已加载的文件;
	libname 和当前扫描到的文件名相同:已找到指定的文件。
在循环体中没有任何操作,只是简单的空语句。
每次循环结束时,将 file_scanner 指向下一个已加载文件的节点(链表),继续执行第二步的条件判断。
     */
    for (file_scanner = file_list;
         file_scanner != NULL &&
         strcmp(libname, file_scanner->filename) != 0;
         file_scanner = file_scanner->next)
        ;

//在扫描已加载的文件列表时,如果没有找到指定的文件(file_scanner 为NULL),则进行一个额外的检查以确保不会加载同名但路径不同的重复文件。
    if (file_scanner == NULL)
    {
        /*
         * 对目标文件进行 stat 操作,获取文件的详细信息(如 inode、文件大小、修改时间等)存储到 stat_buf 结构体中
         */
        if (stat(libname, &stat_buf) == -1)
            ereport(ERROR,
                    (errcode_for_file_access(),
                     errmsg("could not access file \"%s\": %m",
                            libname)));
//执行一个新的循环,扫描已加载文件列表,查找是否存在「同名文件但路径不同」的情况,也就是说目标文件可能是加载了一个链接或符号链接。循环过程中依次比较当前节点对应的文件是否与目标文件有相同的 inode 值。
// 如果找到了相同 inode 的节点,说明已经加载了同名但地址不同的文件,直接跳出循环返回该节点,否则说明不存在相同 inode 的节点,可以安全地将目标文件加载到进程中。

        for (file_scanner = file_list;
             file_scanner != NULL &&
             !SAME_INODE(stat_buf, *file_scanner);
             file_scanner = file_scanner->next)
            ;
    }

    if (file_scanner == NULL)
    {
        /*
         * File not loaded yet. file_scanner 为空指针,则表示此前还没有加载过该库文件
        */

//申请一定的内存空间并将目标库文件路径复制到该空间中。
        file_scanner = (DynamicFileList *)
            malloc(offsetof(DynamicFileList, filename) + strlen(libname) + 1);
        if (file_scanner == NULL)
            ereport(ERROR,
                    (errcode(ERRCODE_OUT_OF_MEMORY),
                     errmsg("out of memory")));

        MemSet(file_scanner, 0, offsetof(DynamicFileList, filename));
        strcpy(file_scanner->filename, libname);
//记录目标库文件的 inode 值,并使用 dlopen 函数打开目标库文件。
        file_scanner->device = stat_buf.st_dev;
#ifndef WIN32
        file_scanner->inode = stat_buf.st_ino;
#endif
        file_scanner->next = NULL;

        file_scanner->handle = dlopen(file_scanner->filename, RTLD_NOW | RTLD_GLOBAL);

        if (file_scanner->handle == NULL)
        {
            load_error = dlerror();
            free((char *) file_scanner);
            /* errcode_for_file_access might not be appropriate here? */
            ereport(ERROR,
                    (errcode_for_file_access(),
                     errmsg("could not load library \"%s\": %s",
                            libname, load_error)));
        }

        /* Check the magic function to determine compatibility, 检查共享库文件是否符合PG_MODULE_MAGIC 宏定义的格式。 */
        magic_func = (PGModuleMagicFunction)
            dlsym(file_scanner->handle, PG_MAGIC_FUNCTION_NAME_STRING);
        if (magic_func)
        {
            const Pg_magic_struct *magic_data_ptr = (*magic_func) ();

            if (magic_data_ptr->len != magic_data.len ||
                memcmp(magic_data_ptr, &magic_data, magic_data.len) != 0)
            {
                /* copy data block before unlinking library */
                Pg_magic_struct module_magic_data = *magic_data_ptr;

                /* try to close library */
                dlclose(file_scanner->handle);
                free((char *) file_scanner);

                /* issue suitable complaint */
                incompatible_module_error(libname, &module_magic_data);
            }
        }
        else
        {
            /* try to close library */
            dlclose(file_scanner->handle);
            free((char *) file_scanner);
            /* complain */
            ereport(ERROR,
                    (errmsg("incompatible library \"%s\": missing magic block",
                            libname),
                     errhint("Extension libraries are required to use the PG_MODULE_MAGIC macro.")));
        }

        /*
         * If the library has a _PG_init() function, call it. 若符合要求,则执行 PG/PL SQL 扩展组件必须要实现的入口函数 _PG_init。
         */
        PG_init = (PG_init_t) dlsym(file_scanner->handle, "_PG_init");
        if (PG_init)
            (*PG_init) ();

        /* OK to link it into list,连接该库文件到双向链表中 */
        if (file_list == NULL)
            file_list = file_scanner;
        else
            file_tail->next = file_scanner;
        file_tail = file_scanner;
    }

// 返回该库文件对应的指针,即完成这个函数中最重要的任务——成功加载并且初始化库文件
    return file_scanner->handle;
}

参考

https://blog.csdn.net/pg_hgdb/article/details/100726356

https://blog.51cto.com/u_15078930/5671540

https://blog.csdn.net/dazuiba008/article/details/117436921

https://zhuanlan.zhihu.com/p/375644210

https://www.wendaok.cn/post/16702.html

https://dude6.com/article/249546.html

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Hehuyi_In

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值