c++ - 在从不再加载的动态库实例化的对象上使用在主代码库中定义的模板类方法时出现段错误

标签 c++ linux templates dynamic segmentation-fault

背景

我有一个多组件c++代码库。有一个包含主要可执行文件的中央组件,并且有许多组件可以编译为动态模块(.so文件)。中央可执行文件能够在运行时加载和卸载它们(如果需要,可以热交换它们)。

有一个名为Scheduler.h的文件,它声明一个Scheduler类,该类在特定的时间或间隔提供同步事件,还有一些用于向调度程序发出请求的帮助程序类。有一个Event类,它保存计时数据,还有一个抽象的action类,它具有一个纯虚拟函数DoEvent。还有一个Scheduler.cpp,其中包含Scheduler.h中大多数功能的定义(模板类除外,这些类在头文件中声明和定义)。
Event拥有一个指向action子类的指针,这是控制调度程序功能的方式。 Scheduler.h本身提供了一些此类。
action的声明如下:

class action
{
    action();
    virtual ~action();
    virtual DoEvent() = 0;
};
FunctionCallAction,是action的子类,其声明和定义如下:

template <class R, class T>
class FunctionCallAction : public action
{
public:
    FunctionCallAction(R (*f)(T), T arg) : argument(arg), callback(f) {}
    ~FunctionCallAction() {}
    void DoEvent() { function(argument); }
private:
    R (*callback)(T);
    T argument;
};

另一个子类HelloAction声明如下:

// In Scheduler.h
class HelloAction : public action
{
    ~HelloAction();
    void DoEvent();
};

// in Scheduler.cpp
HelloAction::~HelloAction() {}
void HelloAction::DoEvent() { cout << "Hello world" << endl; }

我的动态库之一CloneWatch在CloneWatch.h中声明并在CloneWatch.cpp中定义,使用了此调度程序服务。在其构造函数中,它创建一个持久事件,该事件计划每300秒运行一次。在其析构函数中,它将删除此事件。加载此模块后,它将获得对现有调度程序对象的引用。 “加载”模块的过程需要使用dlopen()打开库,使用dlsym()搜索工厂方法(适当地命名为Factory),并使用此工厂方法创建某些对象的实例(语义无关)。要关闭库,将删除factory方法创建的对象,并调用dlclose()从进程的地址空间中删除该库。

在运行时加载和卸载库由命令控制。

// relevant declarations
const float DB_CLEAN_FREQ = 300;
event_t cleanerevent; // event_t is a typedef to an integral type
void * RunDBCleaner(void *); // static function of CloneWatch
Scheduler& scheduler;

// in constructor:
Event e(DB_CLEAN_FREQ, -1, new FunctionCallAction<void *, void *>(CloneWatch::RunDBCleaner, (void *) this));
cleanerevent = scheduler.AddEvent(e);

// in destructor:
scheduler.RemoveEvent(cleanerevent);
Scheduler::RemoveEvent很懒。它没有遍历事件的整个优先级队列,而是维护一组“已取消的事件”。如果在事件处理过程中从其队列中 pop 一个事件,该事件的ID与已取消事件集中的ID相匹配,则该事件不会运行或重新安排,而是会立即清除。清理事件的过程需要删除事件拥有的action对象。

问题

我遇到的问题是程序段错误。该错误发生在Scheduler的事件循环内,大致如下所示:

while (!eventqueue.empty() && e.Due())
{
    Event e = eventqueue.top();
    eventqueue.pop();
    if (cancelled.find(e.GetID()) != cancelled.end())
    {
        cancelled.erase(e.GetID());
        e.Cancel();
        continue;
    }

    QueueUnlock();
    e.DoEvent();
    QueueLock();

    e.Next();

    if (e.ShouldReschedule()) eventqueue.push(e);
}

调用e.Cancel会删除事件的操作。调用e.Next可能会删除事件的操作(仅当事件本身已过期时)。在这种情况下,e.ShouldReschedule将返回false,并且事件将被删除。为了进行测试,我在操作类和子类的析构函数中添加了一些打印语句,以查看发生了什么。

踢球者

如果事件从e.Next中删除,直到到期,一切都会正常进行。但是,当我卸载模块,导致事件通过已取消的列表退出时,一旦调用操作的析构函数,程序就会遇到分段错误。由于调度程序的事件延迟删除,在模块卸载后的某些时间会发生这种情况

它不会输入任何析构函数,但会立即出现故障。我尝试了事件操作的托管和非托管删除的各种组合,以及在不同位置和不同方式下进行的删除。我已经在valgrind和gdb中运行了它,但是它们都只是礼貌地通知我发生了分段错误,并且我一生都无法隔离原因(尽管我不知道如何很好地使用任一方法) 。

如果我也在循环的主体中调用e.Cancel,强制删除,并注释掉重新安排事件的行,从而迫使事件在执行后立即取消,则不会发生错误。

我还用HelloAction替换了该操作,但是这一操作没有错。问题的根源显然在于FunctionCallAction的析构函数。我或多或少地消除了语义错误,并且我怀疑这是编译器或动态链接器某些模糊行为的结果。有人看到这个问题吗?

最佳答案

这是编译器的行为。
问题在于FunctionCallAction是在其头文件中定义(而不是声明)的。这是作为模板类的必要副作用,但是,如果声明一个具有FunctionCallAction<void *, void *>功能的常规类,则会产生与相同的结果(如果是在头文件中定义的)。
这是对模板类的普通限制,在异常情况下会产生意外的副作用。
原因是,如果类的定义位于头文件中,则它将被编译为使用该类的每个文件。因为我是从动态库的代码中使用它的,所以这里就是它的编译位置。因此,当库被卸载时,析构函数的代码以及整个类的其余部分不再存在。
我通过制作FunctionCallAction非模板类并将其声明保留在Scheduler.h中并将其定义移至Scheduler.cpp来解决了此问题。这样,功能是由始终加载的核心可执行文件提供的,而不是由动态模块单独提供的。
对该操作的析构函数的调用存在段错误,因为析构函数本身不再是进程地址空间的一部分。

关于c++ - 在从不再加载的动态库实例化的对象上使用在主代码库中定义的模板类方法时出现段错误,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/12076396/

相关文章:

c++ - 在 C++ 项目中包含 dlib 库时出错

c++ - 从模板函数调用的模板类的模板成员函数

linux - 如何使 zsh `run-help` 忽略 `sudo` 并获得有关以下命令的帮助

c++ - 在 CRTP 中使用嵌套名称说明符

C++ 模板,默认参数作为方法

c++ - 括号中的类型定义

c++ - 为什么 GDI+ 颜色会根据工具提示是否可见而变化?

python - 在python上查找mac地址

python - 在python中获取友好的设备名称

c++ - 如何在 C++ 中使用模板函数