全局静态对象销毁后是否可以调用操作系统计时器回调



我将更简单地重新表述这个问题,在之前的版本没有获得太多吸引力之后,使用更简单的MCVE。

我已经形成了这样的印象,在main()结束后,所发生的一切都是全局对象破坏,然后是静态对象破坏,按顺序排列。

我从来没有考虑过其他";"东西";在CCD_ 2结束和过程结束之间的这段时间内发生。但我最近一直在使用Linux定时器,实验性地,似乎定时器的回调可以在";后期";在进程的main()退出之后,甚至在静态全局对象被销毁之后。

问题:该评估正确吗?静态全局对象被破坏后,是否可以调用计时器回调?

我从来没有想过会发生什么";"迟到";在进程的生命周期中。我想我天真地认为";某事""阻止"正在发生的事情";在CCD_ 4退出之后。

问题:我的计时器回调使用了一个静态全局对象——目的是让对象";总是";无论何时调用回调,都要随时待命。但是,如果可以在静态全局对象被破坏后调用定时器回调,那么这种策略是不安全的。是否有一种众所周知/正确的方法来处理这一问题:即防止计时器回调访问无效对象/内存?

下面的代码创建";许多";计时器设置为在未来2秒过期,其回调引用静态全局对象。main()在调用计时器回调的中间退出。couts显示静态全局对象已被销毁,而计时器回调仍在被调用。

// main.cpp
#include <algorithm>
#include <cerrno>
#include <csignal>
#include <cstring>
#include <iostream>
#include <map>
#include <mutex>
#include <string>
#include <unistd.h>
using namespace std;
static int tmp = ((srand ( time( NULL ) )), 0);
class Foo { // Encapsulates a random-sized, random-content string.
public:
Foo() {
uint32_t size = (rand() % 24) + 1;
std::generate_n( std::back_inserter( s_ ), size, randChar );
}
void operator=( const Foo& other ) { s_ = other.s_; }
std::string s_;
private:
static char randChar() { return ('a' + rand() % 26); }
};
class GlobalObj { // Encapsulates a map<timer_t, Foo>.
public:
~GlobalObj() { std::cout << __FUNCTION__ << std::endl; }
Foo* getFoo( const timer_t& timer ) {
Foo* ret = NULL;
{
std::lock_guard<std::mutex> l( mutex_ );
std::map<timer_t, Foo*>::iterator i = map_.find( timer );
if ( map_.end() != i ) {
ret = i->second;
map_.erase( i );
}
}
return ret;
}
void setFoo( const timer_t& timer, Foo* foo ) {
std::lock_guard<std::mutex> l( mutex_ );
map_[timer] = foo;
}
private:
std::mutex mutex_;
std::map<timer_t, Foo*> map_;
};
static GlobalObj global_obj; // static global GlobalObj instance.
void osTimerCallback( union sigval sv ) { // The timer callback
timer_t* timer = (timer_t*)(sv.sival_ptr);
if ( timer ) {
Foo* foo = global_obj.getFoo(*timer);
if ( foo ) {
cout << "timer[" << *timer << "]: " << foo->s_ << endl;
delete foo;
}
delete timer;
}
}
bool createTimer( const struct timespec& when ) { // Creates an armed timer.
timer_t* timer = new timer_t;
struct sigevent se;
static clockid_t clock_id =
#ifdef CLOCK_MONOTONIC
CLOCK_MONOTONIC;
#else
CLOCK_REALTIME;
#endif
memset( &se, 0, sizeof se );
se.sigev_notify = SIGEV_THREAD;
se.sigev_value.sival_ptr = timer;
se.sigev_notify_function = osTimerCallback;
if ( timer_create( clock_id, &se, timer ) ) {
cerr << "timer_create() err " << errno << " " << strerror( errno ) << endl;
return false;
}
{
struct itimerspec its;
memset( &its, 0, sizeof its );
its.it_value.tv_sec = when.tv_sec;
its.it_value.tv_nsec = when.tv_nsec;
if ( timer_settime( *timer, 0, &its, NULL ) ) {
cerr << "timer_settime err " << errno << " " << strerror( errno ) << endl;
return false;
}
global_obj.setFoo( *timer, new Foo );
}
return true;
}
int main( int argc, char* argv[] ) { // Creates many armed timers, then exits
static const struct timespec when = { 2, 0 };
for ( uint32_t i = 0; i < 100; ++i ) {
createTimer( when );
}
usleep( 2000010 );
return 0;
}

示例错误:

$ g++ --version && g++ -g ./main.cpp -lrt && ./a.out
g++ (Debian 6.3.0-18+deb9u1) 6.3.0 20170516
Copyright (C) 2016 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
timer[timer[~GlobalObj0x55b34c17bd700x55b34c17be60
]: gx
*** Error in `./a.out': double free or corruption (fasttop): 0xtimer[0x55b34c17bf50]: wsngolhdjvhx
]: npscgelwujjfp
Aborted

注意,该错误提到";双自由";;上面的代码有两个delete语句:删除它们似乎不会影响问题的再现性。我认为,由于访问无效内存,错误消息只是转移注意力。

main()中的usleep()增加到足够大,以便允许在静态全局对象销毁之前发生所有计时器回调调用,从而始终成功执行。

不,没有任何魔法可以阻止计时器在main结束后启动。

在C++中防止类似情况发生的常见方法是为每种需要手动释放的资源类型创建一个小的资源拥有类。参见RAII和三/五/零规则。

这样一个类的基础可能是这样的:

#include <cerrno>    // errno
#include <cstring>   // std::strerror
#include <stdexcept> // std::runtime_error
#include <string>    // std::string
#include <utility>   // std::exchange
class Timer {
public:
Timer(clockid_t clockid, sigevent& sev) {
if(timer_create(clockid, &sev, &timerid))
throw std::runtime_error(std::string("timer_create: ") +
std::strerror(errno));
}
// rule of 5
Timer(const Timer&) = delete;                       // no copy construction
Timer(Timer&& rhs) :                                // move construction ok
timerid(std::exchange(rhs.timerid, nullptr)) {}
Timer& operator=(const Timer&) = delete;            // no copy assignment
Timer& operator=(Timer&& rhs) {                     // move assignment ok
if(this != &rhs) {
if(timerid) timer_delete(timerid);
timerid = std::exchange(rhs.timerid, nullptr);
}
return *this;
}
~Timer() {
if(timerid) timer_delete(timerid);
}
private:
timer_t timerid;
};

现在,您可以将Timer存储在容器中,当容器超出范围时,它们将被正确删除。

每当必须处理这些create/delete对中的一个时,使用这种方法通常会限制像您得到的这种惊喜的数量,这些对通常在C API:s中找到。

另请阅读有关静态初始化顺序Fiasco的信息,以避免其他潜在的陷阱。

注意:这个实现利用了timer_t是我的系统上的指针类型这一事实,我不知道是否总是这样

最新更新