【问题标题】:Is it possible for an OS timer callback to be invoked after global static object destruction?全局静态对象销毁后是否可以调用 OS 计时器回调?
【发布时间】: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() 退出后阻止了“事情发生”。

标签: c++ linux timer


【解决方案1】:

不,没有什么魔法可以防止计时器在main 结束后触发。

在 C++ 中防止此类事情发生的常用方法是为需要手动释放的每种类型的资源创建一个小型资源拥有类。请参阅RAIIThe rule of three/five/zero

这样一个类的基础可能如下所示:

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

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

每当必须处理这些 create / delete 对中的一对(通常在 C API:s 中找到)时,使用这种方法通常会限制您遇到的意外的数量。

还请阅读Static Initialization Order Fiasco 以避免其他潜在的陷阱。

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-01-04
    • 2022-01-25
    • 2018-07-24
    • 1970-01-01
    • 2015-01-13
    • 1970-01-01
    相关资源
    最近更新 更多