如何安全地更新一个 运行 动态库?
How to safely update a running dynamic library?
我正在为一个 C 程序实现一个实时代码重载机制,我有一个这样的函数:
#include <sys/types.h>
#include <sys/stat.h>
#include <dlfcn.h>
void module_load(mod_t *mod) {
struct stat statbuf;
if (stat(mod->path, &statbuf) != 0) {
// ...
}
if (statbuf.st_mtime != mod->time) {
if (mod->code != NULL) { // THIS here seems unsafe
dlclose(mod->code);
}
mod->code = dlopen(mod->path, RTLD_GLOBAL | RTLD_LAZY);
if (mod->code != NULL) {
mod->foo = dlsym(mod->code, "foo");
mod->bar = dlsym(mod->code, "bar");
// ...
mod->time = statbuf.st_mtime;
}
}
}
我的函数是这样调用的:
mod->foo();
mod->bar();
系统运行良好,功能也得到了适当的更新,但有些事情让我担心。
module_load
函数在一个分离的线程中循环运行,因此更新可能随时发生,虽然它似乎工作正常,但我想知道如果库在更新时会发生什么奇怪的事情被调用。
我知道我可以在可连接的线程中调用没有循环的函数,然后等待它完成。这可能更安全,但我不想每次都创建一个新线程并加入它。
我试过暂时同时拥有两个"live"库的副本,这样在加载新函数的同时旧函数仍然可以使用,但是代码一直在使用旧函数no不管我做了什么改变。
如何安全地重新加载库及其函数,最好是在分离的线程中?
为了保留两个 Live Copy,我试过这样做:
...
void *new_code = dlopen(mod->path, RTLD_GLOBAL | RTLD_LAZY);
if (new_code != NULL) {
mod->foo = dlsym(new_code, "foo");
mod->bar = dlsym(new_code, "bar");
// ...
void *tmp = mod->code;
mod->code = new_code;
if (tmp != NULL) {
dlclose(tmp);
}
mod->time = statbuf.st_mtime;
您标记为可能不安全的 dlclose()
实际上绝对不安全。当前在其他线程中处于活动状态的模块中的任何函数都将从它们下面拉出地板:它们对模块中静态数据和其他函数的引用将成为悬空指针。
所以我想说你最好弄清楚如何让你的模块有两个活动版本。您不能仅通过使用相同路径调用 dlopen
来做到这一点,因为它会缓存打开的句柄,并且只会 return 当前打开的句柄并增加引用计数。相反,您可以执行以下操作:
将您的模块编译成包含版本号的文件,然后将官方模块文件名符号链接到最新版本。 (这是大多数 .so
文件在典型 Unix 系统上生成的方式。)
当您要打开模块时,请先使用readlink(2)
查找当前链接的模块版本。然后打开那个路径。
(我还没有实际尝试过,但我认为它会起作用,至少在类 Unix 系统上是这样。)
我建议尽可能避免 RTLD_GLOBAL
。一般来说,至少 dlclose
使用 RTLD_GLOBAL
打开一个模块是有风险的; dlclose
可能会孤立其他动态加载模块使用的解析符号。 (如果没有其他模块要使用该模块导出的符号,您可能 dlclose
,那么 RTLD_GLOBAL
就没有必要了。)我不相信 RTLD_LAZY
是个好主意, 要么.
最后,您必须想出一些方法来了解何时可以 dlclose
旧模块。在确定当前没有线程正在从过时模块调用函数之前,您不能这样做。您可能需要考虑将引用计数放入模块结构中,并使用宏或包装器来确保引用计数在调用前和 post 调用前递增和递减。您还需要向模块结构添加某种互斥体以避免竞争条件。
我正在为一个 C 程序实现一个实时代码重载机制,我有一个这样的函数:
#include <sys/types.h>
#include <sys/stat.h>
#include <dlfcn.h>
void module_load(mod_t *mod) {
struct stat statbuf;
if (stat(mod->path, &statbuf) != 0) {
// ...
}
if (statbuf.st_mtime != mod->time) {
if (mod->code != NULL) { // THIS here seems unsafe
dlclose(mod->code);
}
mod->code = dlopen(mod->path, RTLD_GLOBAL | RTLD_LAZY);
if (mod->code != NULL) {
mod->foo = dlsym(mod->code, "foo");
mod->bar = dlsym(mod->code, "bar");
// ...
mod->time = statbuf.st_mtime;
}
}
}
我的函数是这样调用的:
mod->foo();
mod->bar();
系统运行良好,功能也得到了适当的更新,但有些事情让我担心。
module_load
函数在一个分离的线程中循环运行,因此更新可能随时发生,虽然它似乎工作正常,但我想知道如果库在更新时会发生什么奇怪的事情被调用。
我知道我可以在可连接的线程中调用没有循环的函数,然后等待它完成。这可能更安全,但我不想每次都创建一个新线程并加入它。
我试过暂时同时拥有两个"live"库的副本,这样在加载新函数的同时旧函数仍然可以使用,但是代码一直在使用旧函数no不管我做了什么改变。
如何安全地重新加载库及其函数,最好是在分离的线程中?
为了保留两个 Live Copy,我试过这样做:
...
void *new_code = dlopen(mod->path, RTLD_GLOBAL | RTLD_LAZY);
if (new_code != NULL) {
mod->foo = dlsym(new_code, "foo");
mod->bar = dlsym(new_code, "bar");
// ...
void *tmp = mod->code;
mod->code = new_code;
if (tmp != NULL) {
dlclose(tmp);
}
mod->time = statbuf.st_mtime;
您标记为可能不安全的 dlclose()
实际上绝对不安全。当前在其他线程中处于活动状态的模块中的任何函数都将从它们下面拉出地板:它们对模块中静态数据和其他函数的引用将成为悬空指针。
所以我想说你最好弄清楚如何让你的模块有两个活动版本。您不能仅通过使用相同路径调用 dlopen
来做到这一点,因为它会缓存打开的句柄,并且只会 return 当前打开的句柄并增加引用计数。相反,您可以执行以下操作:
将您的模块编译成包含版本号的文件,然后将官方模块文件名符号链接到最新版本。 (这是大多数
.so
文件在典型 Unix 系统上生成的方式。)当您要打开模块时,请先使用
readlink(2)
查找当前链接的模块版本。然后打开那个路径。
(我还没有实际尝试过,但我认为它会起作用,至少在类 Unix 系统上是这样。)
我建议尽可能避免 RTLD_GLOBAL
。一般来说,至少 dlclose
使用 RTLD_GLOBAL
打开一个模块是有风险的; dlclose
可能会孤立其他动态加载模块使用的解析符号。 (如果没有其他模块要使用该模块导出的符号,您可能 dlclose
,那么 RTLD_GLOBAL
就没有必要了。)我不相信 RTLD_LAZY
是个好主意, 要么.
最后,您必须想出一些方法来了解何时可以 dlclose
旧模块。在确定当前没有线程正在从过时模块调用函数之前,您不能这样做。您可能需要考虑将引用计数放入模块结构中,并使用宏或包装器来确保引用计数在调用前和 post 调用前递增和递减。您还需要向模块结构添加某种互斥体以避免竞争条件。