【实战指南】4步实现C++插件化编程,轻松实现功能定制与扩展(2)

本文涉及的产品
Serverless 应用引擎免费试用套餐包,4320000 CU,有效期3个月
云原生网关 MSE Higress,422元/月
注册配置 MSE Nacos/ZooKeeper,118元/月
简介: 本文是《4步实现C++插件化编程》的延伸,重点介绍了新增的插件“热拔插”功能。通过`inotify`接口监控指定路径下的文件变动,结合`epoll`实现非阻塞监听,动态加载或卸载插件。核心设计包括`SprDirWatch`工具类封装`inotify`,以及`PluginManager`管理插件生命周期。验证部分展示了插件加载与卸载的日志及模块状态,确保功能稳定可靠。优化过程中解决了动态链接库句柄泄露问题,强调了采纳用户建议的重要性。

4步实现C++插件化编程,轻松实现功能定制与扩展(2)

[TOC]

引言

  此文是对先前文章《4步实现C++插件化编程,轻松实现功能定制与扩展》 的延伸,重点记录在原版本基础上新增的插件热拔插功能。

  起因源于读者的一个评论,如下:
image.png


  看到这个问题时,当时的软件尚不具备“热拔插”功能。 但思考了一下,不支持“热拔插”的插件,应属于一种功能缺陷。于是乎,在原有的基础上增加了这一功能。这里,也很感谢这位读者提出这么好的问题。

  插件化编程的实现方案和代码细节已经在上一篇文章中记录了,本篇主要记录下新增的热拔插功能的实现细节。


注:文末提供本文源码获取方式。文章不定时更新,喜欢本公众号系列文章,可以星标公众号,避免遗漏干货文章。源码开源,如果对您有帮助,帮忙分享、点赞加收藏喔!

优化策略

  第一版软件仅在启动时加载插件。在此基础上,新增以下功能:

  • 在主程序运行过程中,若指定路径下新增插件库,程序将自动识别并加载。
  • 若在主程序运行中从指定路径移除或删除插件库,程序将自动卸载对应的已加载插件。

  要实现上述功能,需要对指定路径下的文件变动进行监控。在Linux环境中,可以利用inotify接口来达成这一目的。关于如何使用 inotify 实现实时文件监控的具体方法,可参考先前文章《使用inotify实现实时文件监控》

详细设计

  优化后的插件加载主要拆分为两个大类SprDirWatchPluginManager:
SprDirWatch 是一个工具类。专门用于封装 inotify 接口,以便于监控文件系统中的特定路径变化。
PluginManager 则是插件管理类。负责通过 SprDirWatch 捕获指定路径下文件的变化,并据此触发插件的自动“加载”或“卸载”操作。

  • SprDirWatch类定义
    ```C++
    class SprDirWatch
    {
    public:
    SprDirWatch();
    ~SprDirWatch();

    int GetInotifyFd() const { return mInotifyFd; }
    int AddDirWatch(const std::string& path, uint32_t mask);
    int RemoveDirWatch(int fd);

private:
int mInotifyFd;
std::set mWatchFds;
};

  `SprDirWatch` 的设计只是对 `inotify` 接口的一个简洁封装,其主要目的是为了更好地管理和控制 `inotify` 的监控资源。具体来说:    
① 封装 `inotify` 的使用复杂性,提供了一个更友好、更易于使用的接口。    
② 在`SprDirWatch`的生命周期结束(即析构)时,自动释放句柄(尽管没必要移除监控句柄,好的编程习惯应该是有始有终)。

* **PluginManager类定义**
```C++
class PluginManager
{
public:
    PluginManager();
    ~PluginManager();

    void Init();

private:
    void InitWatchDir();
    void LoadPlugin(const std::string& path);
    void UnloadPlugin(const std::string& path);
    void LoadAllPlugins();
    void UnloadAllPlugins();
    std::string GetDefaultLibraryPath();

private:
    SprContext mContext;
    SprDirWatch mDirWatch;
    std::string mDefaultLibPath;
    std::shared_ptr<PFile> mFilePtr;
    std::map<std::string, void*> mPluginHandles;
    std::map<int, SprObserver*> mPluginModules;
};
AI 代码解读

PluginManager 的设计则是用于管理所有插件的“加载”和“卸载”。即通过SprDirWatch监听指定路径“插件”的状态:

插件生成
① 当通过 SprDirWatch 监听到指定路径下有新的插件生成时,调用 LoadPlugin 方法加载新插件。
LoadPlugin 使用 dlopen 加载插件库,并保存库地址句柄。
③ 调用插件库的入口函数,启动插件模块。

插件卸载
① 当通过 SprDirWatch 监听到指定路径下的插件被删除时,调用 UnloadPlugin 方法卸载该插件。
UnloadPlugin 调用插件库的退出函数,停止插件模块。
③ 使用 dlclose 关闭插件库,释放资源。

  • 监听动态库,插件“热插拔”实现

    void PluginManager::InitWatchDir()
    {
      // Add a watch on the specified directory. The events to monitor include:
      // - IN_CLOSE_WRITE: Triggered when a file is closed after being written.
      // - IN_DELETE: Triggered when a file or directory is deleted.
      // - IN_MOVED_TO: Triggered when a file or directory is moved to the specified directory.
      // - IN_MOVED_FROM: Triggered when a file or directory is moved from the specified directory.
      // Note: IN_CREATE is not used because it triggers immediately when a file is created,
      // which may result in attempting to process the file before it is fully written and closed.
      mDirWatch.AddDirWatch(mDefaultLibPath.c_str(), IN_CLOSE_WRITE | IN_MOVED_TO | IN_MOVED_FROM | IN_DELETE);
      mFilePtr = std::make_shared<PFile>(mDirWatch.GetInotifyFd(), [&](int fd, void *arg) {
          const int size = 100;
          char buffer[size];
          ssize_t numRead = read(fd, buffer, size);
          if (numRead == -1) {
              SPR_LOGE("read %d failed! (%s)\n", fd, strerror(errno));
              return;
          }
    
          int offset = 0;
          while (offset < numRead) {
              struct inotify_event* pEvent = reinterpret_cast<struct inotify_event*>(&buffer[offset]);
              if (!pEvent) {
                  SPR_LOGE("pEvent is nullptr!\n");
                  return;
              }
    
              if (pEvent->len > 0) {
                  if (pEvent->mask & IN_CLOSE_WRITE || pEvent->mask & IN_MOVED_TO) {
                      SPR_LOGD("File %s is created\n", pEvent->name);
                      LoadPlugin(pEvent->name);
                  }
                  if (pEvent->mask & IN_DELETE || pEvent->mask & IN_MOVED_FROM) {
                      SPR_LOGD("File %s is deleted\n", pEvent->name);
                      UnloadPlugin(pEvent->name);
                  }
              }
              offset += sizeof(struct inotify_event) + pEvent->len;
          }
      });
    
      EpollEventHandler::GetInstance()->AddPoll(mFilePtr.get());
    }
    
    AI 代码解读

      为了避免阻塞或轮询监听动态库路径,使用了 epoll 监听 inotify 的文件描述符,实现触发式监听。

验证

新增插件验证
① 移入插件库

$ mv libpluginonenet.so ../Lib/
AI 代码解读

② 日志打印确认

$ tail -f /tmp/sprlog/sprlog.log | egrep -i "PlugMgr|EntryOneNet"
10-30 21:08:13.277  19597 PlugMgr      D:   84 File libpluginonenet.so is created
10-30 21:08:13.300  19597 EntryOneNet  D:   58 Load plug-in OneNet modules
10-30 21:08:13.300  19597 PlugMgr      D:  141 Load plugin libpluginonenet.so success!
AI 代码解读

③ 模块状态确认

                                   Show All Message Queues
-----------------------------------------------------------------------------------------------
 HANDLE  QLSUM  QMUSED  QCUSED  BLOCK   MLLEN MMUSED MLAST  MTOTAL  NAME
-----------------------------------------------------------------------------------------------
      4     10       6       0  BLOCK    1025     51     1      32  /SprMdrQ_20231126
      5     10       1       0  BLOCK    1025     43     1       1  /TimerM_7lTva1nY
      6     10       1       0  BLOCK    1025     43     1       1  /PowerM_E0pil3lu
      7     10       1       0  BLOCK    1025     43     1       1  /OneDrv_BtJzE38A
      8     10       1       0  BLOCK    1025     43     1       1  /OneMgr_yXTdsXPW
      9     10       1       0  BLOCK    1025     51     1       1  /MQTT-OneJson01_y8M
     10     10       1       0  BLOCK    1025     47     1       1  /MQTT-DEV01_z5TzmqV
     11     10       1       0  BLOCK    1025     47     1       1  /PC_TEST_01_nOBnl0w
     12     10       1       0  BLOCK    1025     47     1       1  /PC_TEST_02_VWQQbIw
-----------------------------------------------------------------------------------------------
Press 'Q' to back
AI 代码解读

通过日志和模块状态,可确认插件OneNet加载成功,涉及到的模块运行正常。

移除插件验证
① 移除插件库

$ mv ../Lib/libpluginonenet.so .
AI 代码解读

② 日志打印确认

10-30 21:11:04.418  19597 PlugMgr      D:   88 File libpluginonenet.so is deleted
10-30 21:11:04.418  19597 EntryOneNet  D:   83 Unload plug-in OneNet modules
10-30 21:11:04.419  19597 PlugMgr      D:  170 Unload plugin libpluginonenet.so success!
AI 代码解读

③ 模块状态确认

                                   Show All Message Queues
-----------------------------------------------------------------------------------------------
 HANDLE  QLSUM  QMUSED  QCUSED  BLOCK   MLLEN MMUSED MLAST  MTOTAL  NAME
-----------------------------------------------------------------------------------------------
      4     10       6       0  BLOCK    1025     51     3      26  /SprMdrQ_20231126
      5     10       1       0  BLOCK    1025     43     1       1  /TimerM_7lTva1nY
      6     10       1       0  BLOCK    1025     43     1       1  /PowerM_E0pil3lu
-----------------------------------------------------------------------------------------------
Press 'Q' to back
AI 代码解读

通过日志和模块状态,可确认插件OneNet卸载成功,涉及到的模块已正常退出。

总结

  • 本次优化实现了插件的“热插拔”功能,通过监控文件变动并相应调用加载或卸载函数来完成。
  • 在此过程中,还发现动态链接库句柄泄露的问题,应确保dlopen返回的句柄得到妥善管理,在插件或程序退出时通过dlclose进行回收。
  • 优化过程中认识到,功能设计需细致入微,同时也应积极采纳并分析他人建议,以提高方案的可行性和实用性。此次就非常感激那位读者提出的问题。
目录
打赏
0
60
62
24
105
分享
相关文章
【实战指南】7个设置/获取接口了解Linux时间管理
本文系统介绍了Linux时间管理中的7个关键设置/获取接口,涵盖时间获取(如`time`、`gettimeofday`、`clock_gettime`)、时间设置(如`stime`、`settimeofday`、`clock_settime`)以及时间转换和格式化等内容。文章详细解析了绝对时间和相对时间的概念,包括GMT、UTC及本地时间的区别,并通过实例测试展示了各接口的使用方法与特性。此外,还探讨了时区设置对时间计算的影响,强调在实际开发中推荐使用UTC作为基准时间以避免时区变化带来的问题。总结部分结合项目经验,提醒开发者注意时间服务的重要性及潜在风险,例如时间跳跃可能引发的应用故障。
192 73
【实战指南】7个设置/获取接口了解Linux时间管理
自建 DeepSeek 时代已来,联网搜索如何高效实现
随着 DeepSeek 等高质量开源大模型的涌现,企业自建智能问答系统的成本已降低 90% 以上。基于 7B/13B 参数量的模型在常规 GPU 服务器上即可获得商业级响应效果,配合 Higress 开源 AI 网关的增强能力,开发者可快速构建具备实时联网搜索能力的智能问答系统。
686 70
Log/Trace/Metric 完成 APIServer 可观测覆盖
12 月 11 日,OpenAI 出现了全球范围的故障,影响了 ChatGPT/API/Sora/Playground/Labs 等服务,持续时间超过四个小时。究其背后原因,主要是新部署的服务产生大量的对 K8s APIServer 的请求,导致 APIServer 负载升高,最终导致 DNS 解析不能工作,影响了数据面业务的功能。面对 APIServer 这类公用基础组件,如何通过 Log/Trace/Metric 完成一套立体的覆盖体系,快速预警、定位根因,降低不可用时间变得非常重要。
171 60
Log/Trace/Metric 完成 APIServer 可观测覆盖
# 2个类轻松构建高效Socket通信库
本文介绍了一种通过两个类`EpollEventHandler`和`IEpollEvent`构建高效Socket通信库的方法。该库支持TCP、UDP和Unix域套接字,采用I/O多路复用技术(如epoll),提升并发处理能力。通过抽象基类和具体事件类的设计,简化了API使用,便于开发者快速上手。文章还提供了服务端与客户端的实例代码,展示其在实际项目中的应用效果。此Socket库适应嵌入式环境,功能定制性强,有助于减少外部依赖并提升维护效率。
121 75
# 2个类轻松构建高效Socket通信库
4步实现状态机驱动的MQTT客户端,快速接入OneNet (1)
本文介绍了基于状态机驱动的MQTT客户端快速接入OneNet平台的实现方法,通过4步完成模块设计。文章以开源项目`Sparrow`为基础,引入`OneNetMqtt`业务模块,采用事件驱动模型和双层状态机设计,实现设备状态管理、消息处理及定时任务等功能。模块分为三层:`OneNetManager`负责核心逻辑,`OneNetDevice`管理设备信息,`OneNetDriver`处理Socket与MQTT通信。验证结果显示设备连接、数据上报及下线功能正常,稳定性良好。该设计简化了复杂条件判断,增强了系统灵活性与可扩展性,适用于实际项目参考。文末提供源码获取方式,助力读者实践与学习。
269 73
【实战经验】C/C++右移高位补0还是1?
本文探讨了C/C++中右移运算时高位补0还是补1的问题。通过示例代码分析,揭示了右移规则:无符号类型高位补0;有符号类型根据正负决定(正数补0,负数补1)。文中列举了可能导致错误的场景,并提供了两种规避措施——使用无符号类型和掩码校正,确保结果符合预期。最后总结指出,右移运算虽常见,但若处理不当易引发隐晦Bug,需谨慎对待。
264 66
Spring AI Alibaba 应用框架挑战赛圆满落幕,恭喜获奖选手
第二届开放原子大赛 Spring AI Alibaba 应用框架挑战赛决赛于 2 月 23 日在北京圆满落幕。
261 70
DLedger —基于 raft 协议的 commitlog 存储库
尊敬的阿里云用户: 您好!为方便您试用开源 RocketMQ 客户端访问阿里云MQ,我们申请了专门的优惠券,优惠券可以直接抵扣金额。请填写下您公司账号信息,点击上图,了解更多哦。 一、DLedger引入目的 在 RocketMQ 4.5 版本之前,RocketMQ 只有 Master/Slave 一种部署方式,一组 broker 中有一个 Master ,有零到多个 Slave,Slave 通过同步复制或异步复制的方式去同步 Master 数据。
13042 76
热门活动速递丨AI 原生应用开发实战营·杭州站
了解 AI 原生应用开发的前沿趋势和核心产品技术,全面 get 典型应用场景及硬核实战经验,快速上手一键部署 DeepSeek 系列模型,现场完成实操,颁发专属证书与精美礼品。
128 68
【C++篇】深度解析类与对象(下)
在上一篇博客中,我们学习了C++的基础类与对象概念,包括类的定义、对象的使用和构造函数的作用。在这一篇,我们将深入探讨C++类的一些重要特性,如构造函数的高级用法、类型转换、static成员、友元、内部类、匿名对象,以及对象拷贝优化等。这些内容可以帮助你更好地理解和应用面向对象编程的核心理念,提升代码的健壮性、灵活性和可维护性。
AI助理

你好,我是AI助理

可以解答问题、推荐解决方案等

登录插画

登录以查看您的控制台资源

管理云资源
状态一览
快捷访问