使用在主代码库中定义的模板类方法对从不再加载的动态库实例化的对象进行分段错误

Wug*_*Wug 4 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;
};
Run Code Online (Sandbox Code Playgroud)

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;
};
Run Code Online (Sandbox Code Playgroud)

HelloAction,另一个子类,声明如下:

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

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

我的一个动态库,CloneWatch在CloneWatch.h中声明并在CloneWatch.cpp中定义,使用此调度程序服务.在它的构造函数中,它创建一个持续事件,计划每300秒运行一次.在它的析构函数中,它会删除此事件.加载此模块时,它将获取对现有调度程序对象的引用."加载"模块的过程需要使用dlopen()打开库,dlsym()搜索工厂方法(适当命名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);
Run Code Online (Sandbox Code Playgroud)

Scheduler::RemoveEvent很懒.它不是遍历事件的整个优先级队列,而是维护一组"已取消的事件".如果在其事件处理过程中,它从其队列中弹出一个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);
}
Run Code Online (Sandbox Code Playgroud)

调用e.Cancel删除事件的操作.调用e.Next 可能会删除事件的操作(仅当事件已自动过期.在这种情况下,e.ShouldReschedule将返回false并且事件将被删除).出于测试目的,我向action类和子类的析构函数添加了一些print语句,以查看发生了什么.

踢球者

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

它不会进入任何析构函数,但会立即出现故障.我已经尝试了托管和非托管删除事件动作的各种组合,以及在不同的地方和不同的方式进行.我通过valgrind和gdb运行它,但是他们都礼貌地告诉我发生了分段错误,并且对于我的生活我无法理清原因(虽然我不知道如何使用其中任何一个) .

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

我也用a替换了动作HelloAction,但是这个没有错.关于析构函数的一些非常具体FunctionCallAction的问题显然是问题所在.我或多或少地消除了语义错误,我怀疑它是编译器或动态链接器的一些模糊行为的结果.有谁看到这个问题?

Wug*_*Wug 5

这是编译器的行为.

问题是FunctionCallAction在头文件中定义(而不仅仅是声明).这是作为模板类的必要副作用,但是如果在头文件中定义类,则声明具有a的功能的常规类FunctionCallAction<void *, void *>产生相同的结果.

这是对模板类的普通限制,在不寻常的情况下会产生意想不到的副作用.

原因是如果类的定义在头文件中,它将被编译到使用它的每个文件中.因为我从动态库的代码中使用它,所以它就是在编译的地方.因此,当卸载库时,析构函数的代码以及类的其余部分不再存在.

我通过创建一个FunctionCallAction非模板类并在Scheduler.h中只保留它的声明并将其定义移动到Scheduler.cpp 来解决这个问题.这样,函数由始终加载的核心可执行文件提供,而不是由动态模块单独提供.

对action的析构函数的调用是段错误,因为析构函数本身不再是进程的地址空间的一部分.