热更新DLL/SO的核心是新旧模块共存与原子切换调用入口,而非重复加载或直接dlclose;需通过版本化接口结构体、原子指针切换、引用计数延迟卸载及线程安全等待来实现。

热更新 DLL/SO 的核心约束:不能直接用 dlopen/dlsym 重复加载同名库
直接 dlopen("libfoo.so", RTLD_NOW | RTLD_GLOBAL) 后再 dlopen("libfoo.so", ...) 不会重新加载——系统返回已有句柄;而先 dlclose() 再重载,又会导致符号地址失效、全局状态丢失、线程正在调用旧函数时崩溃。热更新不是“换文件再加载”,而是“新旧模块共存 + 原子切换调用入口”。
实操建议:
- 每个动态库导出统一的版本化接口结构体(如
PluginV1),含init、shutdown、process等函数指针,而非直接暴露裸函数 - 主程序只通过该结构体调用业务逻辑,不直接绑定
dlsym到全局函数指针 - 新库加载成功并
init()返回 OK 后,才用原子指针(如std::atomic)替换当前活跃接口指针 - 旧库的
shutdown()必须在确认无任何线程正在执行其process()后才调用(需配合引用计数或 RCU 风格等待)
Linux 下避免 dlclose 导致段错误的关键:延迟卸载 + 引用计数
常见错误现象:dlclose() 后旧库代码仍在执行(尤其异步回调、定时器触发),访问已释放的 .data 或 .bss 段,触发 SIGSEGV。
使用场景:高频调用插件的网络服务(如 HTTP handler)、实时音视频处理 pipeline。
实操建议:
- 为每个加载的库维护一个
std::shared_ptr管理的句柄对象,内部封装void*(dlopen返回值)和引用计数 - 每次进入插件函数前,调用
acquire()增加计数;返回后release();仅当计数归零且无活跃调用时,才真正dlclose() - 不要依赖
RTLD_NOLOAD判断是否已加载——它只对同一路径生效,无法区分不同时间戳的同名文件 - 检查
dlerror()必须紧跟dlopen/dlsym调用,否则可能被后续系统调用覆盖
Windows 上 LoadLibraryA/FreeLibrary 的陷阱:DLL_THREAD_ATTACH 不等于线程安全
错误认知:“只要 DLL 实现了 DLL_PROCESS_ATTACH 和 DLL_THREAD_ATTACH,就能热更新”。实际中,FreeLibrary 可能阻塞在线程退出的 DLL_THREAD_DETACH 中,且无法保证所有用户线程已完成对 DLL 的调用。
参数差异:LoadLibraryEx 的 LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR 比默认行为更安全,避免从系统路径误加载旧版 DLL。
实操建议:
- 禁用 DLL 的隐式链接(即不使用
.lib导入库),全部走显式GetProcAddress - 在
DllMain中只做极轻量初始化(如 TLS 分配),严禁调用LoadLibrary、WaitForSingleObject或 CRT 函数(如malloc) - 用
InterlockedIncrement/InterlockedDecrement管理调用计数,比std::atomic更兼容 MinGW/MSVC 多运行时场景 - 调用
FreeLibrary前,必须确保:1)原子切换完成;2)所有工作线程已轮询检查新接口指针;3)等待至少 200ms 让残留调用自然退出(或配合条件变量通知)
跨平台热更新的最小可行结构体定义与加载流程
性能影响:每次调用都经过一层函数指针间接跳转,但现代 CPU 分支预测可基本消除开销;真正的瓶颈是同步切换时的缓存失效和内存屏障。
示例(关键部分):
struct PluginV1 {
int (*init)(void* config);
void (*shutdown)();
int (*process)(const uint8_t*, size_t, uint8_t**, size_t*);
const char* name;
uint32_t version; // e.g., 0x00010000 for v1.0.0
};
<p>static std::atomic<pluginv1> g_current_plugin{nullptr};
static std::mutex g_plugin_mutex;</pluginv1></p><p>bool load_plugin(const char<em> path) {
void</em> handle = dlopen(path, RTLD_NOW | RTLD_LOCAL);
if (!handle) { /<em> log dlerror() </em>/ return false; }</p><pre class="brush:php;toolbar:false;">auto get_sym = [&](const char* sym) -> void* {
void* p = dlsym(handle, sym);
if (!p) { dlclose(handle); return nullptr; }
return p;
};
PluginV1* p = new PluginV1{};
p->init = reinterpret_cast<decltype>init)>(get_sym("plugin_init"));
p->shutdown = reinterpret_cast<decltype>shutdown)>(get_sym("plugin_shutdown"));
p->process = reinterpret_cast<decltype>process)>(get_sym("plugin_process"));
p->name = reinterpret_cast<const char>(get_sym("plugin_name"));
if (!p->init || !p->process || !p->name || p->init(nullptr) != 0) {
delete p;
dlclose(handle);
return false;
}
PluginV1* old = g_current_plugin.exchange(p);
if (old) {
// 延迟卸载:交由独立清理线程或下一次 load_plugin 时检查计数
schedule_unload(old, handle);
}
return true;</const></decltype></decltype></decltype>
}
容易被忽略的点:g_current_plugin 是 std::atomic,但其指向的 PluginV1 结构体本身非线程安全——process 函数内部必须自行处理并发,主程序不负责加锁。热更新的“平滑”只保证调用入口切换原子,不保证业务逻辑自动线程安全。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











