【发布时间】:2021-02-26 16:33:25
【问题描述】:
我正在更简单地重新表述这个问题,并且在之前的版本没有获得太大吸引力之后使用更简单的 MCVE。
我的印象是,在main() 结束后,所发生的一切都是全局对象销毁,然后是静态对象销毁,依次类推。
我从未考虑过在main() 结束和流程结束之间的这段时间内发生的其他“事情”的可能性。但我最近一直在使用 Linux 计时器,并且实验性地,似乎可以在进程的这个“后期阶段”、main() 退出之后甚至在静态全局之后调用计时器的回调对象已被销毁。
问题:该评估是否正确?静态全局对象销毁后是否可以调用定时器回调?
我从来没有考虑过在进程的生命周期中这个“晚期”会发生什么。我想我在main() 退出后天真地假设“某事”“阻止”“事情发生”。
问题:我的计时器回调使用静态全局对象——目的是该对象将“始终”存在,无论何时调用回调。但是如果可以在静态全局对象被销毁后调用计时器回调,那么该策略是不安全的。是否有一种众所周知/正确的方法来处理这个问题:即防止计时器回调访问无效的对象/内存?
下面的代码创建了“许多”计时器,设置为在未来 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() 增加到足够大,以便允许所有计时器回调调用发生在静态全局对象销毁导致始终成功执行。
【问题讨论】:
-
当你完成计时器对象后,为什么不打电话给
timer_delete()?然后它不会触发。 -
@JohnZwinck - 这是设计代码的原因之一:我故意不
timer_delete()以强制执行此条件。这个问题背后的动机是了解在静态全局对象被破坏后定时器是否真的会触发。我之前有一个挥手(缺乏)的理解,即“黑魔法”在main()退出后阻止了“事情发生”。