6、插件管理器实现:插件的注册、发现、加载与卸载生命周期管理
好,咱们今天来啃一块硬骨头——插件管理器。
说实话,我在做第一个插件系统的时候,犯过一个特别蠢的错误。我把插件的加载和卸载逻辑写成了一个大函数,结果上线第一天就崩了。为什么?因为卸载插件时,另一个线程还在用它的函数指针。嗯,从那以后,我再也不敢把生命周期管理当儿戏了。
插件管理器,说白了就是插件的「大管家」。它要管四件事:注册、发现、加载、卸载。这四个环节环环相扣,少一个都不行。
6.1 生命周期总览
先看一张图,把整个流程串起来。
你看,这其实是一个闭环。插件从注册开始,到卸载结束,但卸载后还能重新注册。我当年设计这个状态机的时候,特意留了回退路径——因为实际项目中,插件热更新太常见了。
6.2 注册:给插件上户口
注册是什么?就是让插件管理器知道「有这么一个插件存在」。我习惯用一个结构体来保存插件的基本信息。
// 插件描述符
typedef struct {
char name[64]; // 插件名称,唯一标识
char version[16]; // 版本号,比如 "1.2.0"
char path[256]; // .so 文件路径
void *handle; // dlopen 返回的句柄,未加载时为 NULL
int state; // 当前状态:0=未注册 1=已注册 2=已加载 3=已卸载
int ref_count; // 引用计数,防止重复卸载
void (*init)(void); // 初始化函数指针
void (*fini)(void); // 反初始化函数指针
} PluginDesc;
注册函数长这样:
int plugin_register(const char *name, const char *version,
const char *path, void (*init)(void), void (*fini)(void)) {
// 检查是否已存在同名插件
if (find_plugin(name) != NULL) {
log_error("插件 %s 已注册,请勿重复注册", name);
return -1;
}
PluginDesc *p = malloc(sizeof(PluginDesc));
strncpy(p->name, name, 63);
strncpy(p->version, version, 15);
strncpy(p->path, path, 255);
p->handle = NULL;
p->state = 1; // 已注册
p->ref_count = 0;
p->init = init;
p->fini = fini;
// 插入到全局插件链表
list_add_tail(&plugin_list, &p->list_node);
log_info("插件 %s v%s 注册成功", name, version);
return 0;
}
我的习惯:注册时不做任何资源分配,只记录元数据。这样即使注册了100个插件,内存开销也很小。真正「重」的事情留给加载阶段。
6.3 发现:插件在哪?
发现机制有两种主流做法:
- 目录扫描:遍历指定目录下的所有 .so 文件
- 清单文件:读取一个 JSON/XML 配置文件,里面列出了插件路径
我个人更推荐目录扫描 + 命名约定。比如规定插件文件名必须是 plugin_xxx.so 的格式。这样扫描时一眼就能认出来。
int plugin_discover(const char *dir_path) {
DIR *dir = opendir(dir_path);
if (!dir) return -1;
struct dirent *entry;
while ((entry = readdir(dir)) != NULL) {
// 只处理 plugin_ 开头的 .so 文件
if (strncmp(entry->d_name, "plugin_", 7) == 0 &&
strstr(entry->d_name, ".so") != NULL) {
char full_path[512];
snprintf(full_path, 511, "%s/%s", dir_path, entry->d_name);
// 尝试加载并获取插件信息
void *handle = dlopen(full_path, RTLD_LAZY | RTLD_LOCAL);
if (!handle) {
log_warn("无法加载 %s: %s", full_path, dlerror());
continue;
}
// 获取插件元数据
PluginMeta *meta = dlsym(handle, "plugin_meta");
if (meta) {
plugin_register(meta->name, meta->version,
full_path, meta->init, meta->fini);
}
dlclose(handle);
}
}
closedir(dir);
return 0;
}
注意:发现阶段会临时 dlopen 插件,读取元数据后立刻 dlclose。这样做是为了避免在发现阶段就占用插件资源。我曾经见过有人发现阶段不关闭句柄,结果加载阶段又 dlopen 一次,导致同一个 .so 被加载了两次——嗯,内存泄漏就是这么来的。
6.4 加载:让插件真正跑起来
加载是重头戏。它要做三件事:
- dlopen 打开动态库
- dlsym 获取所有函数指针
- 调用插件的 init 函数完成初始化
int plugin_load(const char *name) {
PluginDesc *p = find_plugin(name);
if (!p) {
log_error("插件 %s 未注册", name);
return -1;
}
if (p->state == 2) {
log_warn("插件 %s 已加载,增加引用计数", name);
p->ref_count++;
return 0;
}
// 1. 打开动态库
p->handle = dlopen(p->path, RTLD_NOW | RTLD_GLOBAL);
if (!p->handle) {
log_error("dlopen 失败: %s", dlerror());
return -1;
}
// 2. 获取函数指针
p->init = dlsym(p->handle, "plugin_init");
p->fini = dlsym(p->handle, "plugin_fini");
if (!p->init || !p->fini) {
log_error("插件 %s 缺少必要接口", name);
dlclose(p->handle);
p->handle = NULL;
return -1;
}
// 3. 调用初始化
p->init();
p->state = 2;
p->ref_count = 1;
log_info("插件 %s 加载成功", name);
return 0;
}
关键点:RTLD_NOW 表示立即解析所有符号,如果缺少依赖库会立刻报错。RTLD_GLOBAL 让插件中的符号可以被其他插件使用——这在插件间有依赖关系时特别重要。
6.5 卸载:优雅地告别
卸载比加载难得多。为什么?因为你要确保没有人在用这个插件。
int plugin_unload(const char *name) {
PluginDesc *p = find_plugin(name);
if (!p) return -1;
if (p->state != 2) {
log_warn("插件 %s 未加载,无需卸载", name);
return 0;
}
// 引用计数减一
p->ref_count--;
if (p->ref_count > 0) {
log_info("插件 %s 仍有 %d 个引用,暂不卸载", name, p->ref_count);
return 0;
}
// 调用反初始化
p->fini();
// 关闭动态库
dlclose(p->handle);
p->handle = NULL;
p->state = 3; // 已卸载
log_info("插件 %s 已卸载", name);
return 0;
}
我曾经踩过的坑:卸载时直接 dlclose,结果插件里还有线程在跑。fini 函数应该负责停止所有线程、释放所有资源。如果插件作者忘了写 fini,或者 fini 写得不对,dlclose 后程序直接崩溃。所以我现在会在卸载前加一个「健康检查」——确认插件没有未释放的资源。
6.6 完整生命周期示例
把上面这些串起来,一个典型的使用场景是这样的:
int main() {
// 1. 发现阶段:扫描目录,自动注册
plugin_discover("./plugins");
// 2. 加载阶段:按需加载
plugin_load("plugin_http");
plugin_load("plugin_db");
// 3. 使用插件...
// 4. 卸载阶段:先卸载依赖方,再卸载被依赖方
plugin_unload("plugin_http");
plugin_unload("plugin_db");
// 5. 清理所有注册信息
plugin_cleanup();
return 0;
}
| 阶段 | 核心操作 | 状态变化 | 注意事项 |
|---|---|---|---|
| 注册 | 填写元数据,加入链表 | 未注册 → 已注册 | 不做资源分配 |
| 发现 | 扫描目录或读取清单 | 无状态变化 | 临时 dlopen,用完即关 |
| 加载 | dlopen + dlsym + init | 已注册 → 已加载 | 注意 RTLD_NOW 和 RTLD_GLOBAL |
| 卸载 | fini + dlclose | 已加载 → 已卸载 | 引用计数 + 健康检查 |
嗯,到这里,插件管理器的核心骨架就搭好了。你想想看,有了这套生命周期管理,插件的热插拔、热更新都变得可控了。我当年在嵌入式设备上做插件系统时,就是靠这套状态机,硬是把一个动不动就重启的系统,改造成了可以7x24小时在线升级的稳定系统。
说白了,插件管理器不是什么高深的技术,但细节决定成败。注册时多检查一次重名,加载时多判断一次状态,卸载时多确认一次引用——这些「多一步」的习惯,就是专业和业余的分水岭。