背景
我有一个多组件 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
的问题显然在于问题所在。我或多或少地消除了语义错误,我怀疑这是编译器或动态链接器的一些模糊行为的结果。有没有人看到问题?