【问题标题】:Using static objects from signal handler使用信号处理程序中的静态对象
【发布时间】:2020-03-13 13:25:31
【问题描述】:

根据cppreference.com(我在标准中没有搜索到),使用信号处理程序中的静态对象是UB。

为什么UB要做这样的事情?这有什么潜在的问题?

如果信号处理程序因std::abortstd::raise(异步信号)而被调用,则如果 [...] 信号处理程序引用的任何对象的静态存储持续时间不是std::atomic(C++11 起)或volatile std::sig_atomic_t

【问题讨论】:

  • 最可能的原因:线程安全。非原子全局变量不能安全地被多个线程使用,除非您使用互斥锁或其他线程同步方法。
  • POSIX/C 的同样问题是answered here
  • @NathanOliver :由于存在死锁的风险,(常规)互斥锁对信号处理程序没有多大帮助。 std::mutex 信号不安全是有原因的。
  • @Peregring-lk : malloc is not signal safe - 你不应该从信号处理程序中调用它。
  • @SanderDeDycker:该标准将此类问题视为其管辖范围之外的实施质量。对于没有信号安全的 malloc 就不可能完成的任务的实现需要使其信号安全才能适用于此类任务;那些不适合此类任务的人不需要使其信号安全。

标签: c++ signals undefined-behavior


【解决方案1】:

C++ 标准在 [intro.execution] 中有这样的说法:

19 如果信号处理程序作为调用std::raise 函数的结果而执行,则处理程序的执行顺序在调用std::raise 函数之后并在其返回之前进行。 [ 注意: 当由于其他原因接收到信号时,信号处理程序的执行相对于程序的其余部分通常是无序的。 — 尾注 ]

前面已经阐明了“未排序”的含义:

15 ...SNIP... [ 注意: 未排序的评估的执行可以重叠。 — 尾注 ]

然后在 [intro.races] 中:

20 如果

,两个动作可能同时发生

(20.1) — 它们由不同的线程执行,或者

(20.2) — 它们是未排序的,至少有一个由信号处理程序执行,并且它们不是由同一个信号处理程序调用执行。

如果一个程序的执行包含两个潜在的并发冲突动作,则该程序的执行包含一个数据竞争,其中至少一个不是原子的,并且两者都不会在另一个之前发生,除了特殊情况下面描述的信号处理程序。任何此类数据竞争都会导致未定义的行为。

所指的特例是:

21 对volatile std::sig_atomic_t 类型的同一对象的两次访问如果都发生在同一个线程中,则不会导致数据竞争,即使在信号处理程序中发生一次或多次。

总而言之:当信号处理程序(调用异步信号)访问具有非原子静态存储持续时间的对象时,该访问是无序的,并且当它与冲突访问同时发生时(对同一对象,例如),那么就会出现数据竞争,导致未定义的行为。

请注意,这在单线程应用程序和多线程应用程序中都可能发生。示例(如果需要,将 int 替换为任何其他更明显非原子的类型):

#include <csignal>

int global = 0;

void signal_handler(int signal) {
    global = 0;  // OOPS : this access is (typically) unsequenced
                 // and might happen concurrently with the access
                 // in main, when the interrupt happens right in
                 // the middle of that access
}

int main(void) {
    std::signal(SIGINT, signal_handler);

    while (true) {
        ++global;  // potentially concurrent access
    }

    return 0;
}

【讨论】:

    【解决方案2】:

    这个问题可以追溯到 C 标准,它使用术语 UB 来描述一般情况,实现以顺序一致的方式处理代码有时可能会很昂贵,即使大多数实现应该在实际时有意义地处理这种情况。考虑如下函数:

    extern int x,y,z;
    
    void test(int a)
    {
      int i;
      for (i=0; i<a; i++)
      {
        x=a*i;
        y=a*a;
        z=a*i;
      }
    }
    

    是否应该要求编译器在写入xz 之间将值a*a 存储到y,或者应该允许它在闲暇时将分配提升到y 之前循环或将其推迟到循环完成后。如果允许编译器提升或推迟对y 的赋值,如果在循环执行期间恰好出现信号,并且信号处理程序读取 x 的值,是否有任何简单而干净的方式来描述程序行为? , y 和 z?在这种情况下提供各种行为保证的成本和价值将取决于标准作者无法知道的各种因素。该标准的作者并没有尝试为应该保证的内容编写规则,而是希望编译器编写者能够比委员会更好地判断客户的需求。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2013-01-07
      • 1970-01-01
      • 2014-08-29
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-10-30
      相关资源
      最近更新 更多