4

背景

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

有一个名为 Scheduler.h 的文件,它声明了一个Scheduler类,该类在特定时间或间隔提供同步事件,以及一些用于向调度程序发出请求的辅助类。有一个Event类,它保存计时数据,还有一个抽象action类,它有一个纯虚函数,DoEvent. 还有一个 Scheduler.cpp,它包含了 Scheduler.h 中大部分功能的定义(模板类除外,它们在头文件中声明和定义)。

AnEvent拥有一个指向 的子类的指针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),并使用该工厂方法创建某个对象的实例(语义不相关)。为了关闭库,工厂方法创建的对象被删除,并被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是懒惰的。它不是遍历事件的整个优先级队列,而是维护一组“已取消的事件”。如果在其事件处理过程中,它从其队列中弹出一个 ID 与其已取消事件集中的 ID 匹配的事件,则该事件不会运行或重新安排并立即被清除。清理事件的过程需要删除action它拥有的对象。

问题

我遇到的问题是程序段错误。故障发生在调度器的事件循环内部,大致如下:

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循环的主体,强制删除,并注释掉重新安排事件的行,从而强制事件一执行就取消,则不会发生故障。

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

4

1 回答 1

5

这是编译器的行为。

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

这是对在不寻常情况下具有意外副作用的模板类的普通限制。

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

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

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

于 2012-08-22T15:22:45.203 回答