使用从未装载较长的dynamic库实例化的对象上的主代码库中定义的模板类方法时出现分段错误

背景

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

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

一个Event拥有一个指向action子类的指针,这就是调度器的function是如何被控制的。 Scheduler.h自身提供了一些这样的子类。

action是这样宣布的:

 class action { action(); virtual ~action(); virtual DoEvent() = 0; }; 

FunctionCallAction ,行为的一个子类是这样声明和定义的:

 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; } 

我的一个dynamic库CloneWatch在CloneWatch.h中声明并在CloneWatch.cpp中定义,它使用此调度程序服务。 在其构造函数中,它创build一个持续性事件,计划每300秒运行一次。 在其析构函数中,它删除了这个事件。 加载此模块时,它将获得对现有调度程序对象的引用。 “加载”模块的过程需要使用dlopen()打开库, dlsym()search工厂方法(适当地命名为Factory ),并使用此工厂方法创build某个对象的实例(语义是不相关)。 要closures该库,将删除由工厂方法创build的对象,并调用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是懒惰的。 它不是遍历事件的整个优先级队列,而是维护一组“已取消的事件”。 如果在事件处理过程中,它从队列中popup一个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,事件将被删除)。 出于testing的目的,我添加了一些打印语句给操作类和子类的析构函数,以查看发生了什么。

踢球者

如果事件从e.Next被删除,通过到期,一切正常进行。 但是,当我卸载模块时,通过取消列表导致事件退出,一旦调用操作的析构函数,程序就会遇到分段错误。 这发生在模块卸载后的一段时间,因为调度程序懒惰地删除了事件

它不会进入任何析构函数,而是立即出错。 我尝试了事件操作的托pipe删除和非托pipe删除的各种组合,以及在不同的地方以不同的方式进行删除。 我已经通过valgrind和gdb运行了,但是他们都只是礼貌地告诉我发生了分段错误,对于我的生活,我无法隔离原因(尽pipe我不知道如何使用任何一个) 。

如果我也在循环的主体中调用e.Cancel ,强制删除,并且重新计划重新计划事件的行,从而强制事件在执行时立即被取消,故障不会发生。

我也用HelloAction代替了这个动作,但是这个并没有错。 关于FunctionCallAction的析构FunctionCallAction一些非常具体的问题是问题显而易见的地方。 我已经或多或less地消除了语义错误,我怀疑它是编译器或dynamic链接器的一些模糊行为的结果。 有没有人看到这个问题?

这是编译器的行为。

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

对模板类的异常限制是在一个不寻常的情况下产生意想不到的副作用。

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

我通过制作一个FunctionCallAction非模板类,只在Scheduler.h中留下它的声明,并将其定义移动到Scheduler.cpp来解决这个问题。 通过这种方式,这些功能由始终加载的核心可执行文件提供,而不是由动态模块单独提供。

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