【问题标题】:Is this function considered as re-entrant?此功能是否被视为可重入?
【发布时间】:2017-01-03 09:52:48
【问题描述】:

我有一个具有以下实现的函数:

void func (uint8 index, uint8 status)
{
  if (status == 1)
  {
    myArrayOfStructures[index].status = 1;
  }

  else if (status == 0)
  {
    myArrayOfStructures[index].status = 0;
  }

  else
  {
    /* Nothing */
  }
}

注意:myArrayOfStructures 是文件上的全局变量。

我认为这个函数是可重入的,只要它的参数的传递是通过堆栈完成的,因为以下分析:

函数的 2 个参数在函数调用时被压入堆栈。 如果函数本身被另一个 OS 任务中断,则第二次将参数压入堆栈。 因此,该函数的 2 个实例是“独立的”,因为每个实例在堆栈中都有自己的一组参数。

直到,我使用某个编译器选项优化了这个文件的速度。

经过优化,我发现那些参数的传递是通过寄存器完成的(我知道编译器有权做这种事情(寄存器分配))。但是这个优化使得这个函数是不可重入的。

所以,我的问题是

  • 上面的函数真的是可重入的吗?
  • 如果是(可重入),编译器如何进行这样的优化以恢复其重入状态?

请参考我的答案。

非常感谢。

【问题讨论】:

  • 通过寄存器或堆栈传递根本不相关 - 每个操作系统任务都有自己的一组寄存器(每个内核都有自己的寄存器,操作系统会根据需要小心保存和恢复它们切换到另一个任务/处理同一内核上的中断时)。
  • 旁注,但功能可以简化为简单的myArrayOfStructures[index].status = !!status;。遵守C 非零约定意味着正确。你甚至可以通过使用来自stdbool.hbool 来进行更多的自我记录
  • @StoryTeller:不完全; status=2 表示“不要触摸;保持原样。”
  • @SF。 - 是的,在 OP 的原始帖子中。我的建议是增强。不要成为 254 个值的 NO OP,而是使函数符合 C 语言习惯。
  • @joop 这个元素的写操作是原子的。我检查了编译器生成的对应汇编代码(.lst文件)。

标签: c compiler-optimization reentrancy register-allocation


【解决方案1】:

无论编译器选择何种参数传递模式,您的函数都是可重入的。

虽然依赖全局状态来正确执行会破坏重入,但简单地访问全局状态并不一定会使您的函数不可重入。特别是,如果你从不读取全局变量,就像你的函数那样,你的代码是可重入的,因为它的执行不依赖于全局状态。

就通过寄存器传递参数而言,上下文切换也会保存所有寄存器的内容,因此无论参数传递模式如何,值都是中断安全的。

【讨论】:

    【解决方案2】:

    可能不会。我会指定它什么时候是可重入的,什么时候不是。但在此之前,您必须知道,虽然每个处理器只有一组物理寄存器,但这些寄存器的状态是每个线程的。每个线程都保持自己的状态,不能摆弄其他线程的寄存器状态。操作系统确保了这一点。

    通常,根据定义,编译器优化永远不会在正确代码中引入错误。但是,优化可能会让现有的错误浮出水面,但无论是否使用优化,错误都存在于代码中。因此,无论您编写什么代码,无论优化如何,它都应该可以正常工作。这也有例外,但它们与问题无关。

    现在我来回答你的问题。假设函数调用了一些indexstatus 1。考虑以下情况:

    void func (uint8 index, uint8 status)
    {
      if (status == 1)
      {
        // Interrupt occurs here.
        myArrayOfStructures[index].status = 1;
      }
    
      else if (status == 0)
      {
        myArrayOfStructures[index].status = 0;
      }
    
      else
      {
        /* Nothing */
      }
    }
    

    当中断发生并且同一个线程以相同的indexstatus为0调用同一个函数时,它将将该索引处的数组元素的值设置为0。当第一次调用恢复时,它将 1 写入相同的数组元素。我假设您认为这是不正确的行为,因为新状态丢失了。这表明该函数是不可重入的。

    如果对数组元素的访问不是原子的,那么即使在单词的单线程含义中,该函数也不是可重入的,因为如果更新或读取数组元素,该线程可能会在中间中断。

    现在让我们考虑两个线程同时执行具有相同索引但具有不同状态的函数。在这种情况下,会发生数据竞争。这意味着两件事。首先,结果是不确定的。您不知道将存储哪种状态。如果这与您的正确性要求一致,那很好。但很可能,您想存储最新状态,因此这种不确定性使其不可重入。其次,这是一个更大的问题,单个数据竞争会使您的整个程序根据 C 标准具有未定义的行为。这当然意味着该函数是不可重入的。

    在没有缓存一致性的对称多处理器系统中或在分布式内存计算机系统中,同一个数组元素可以有多个值,因此您将处于不知道最新状态的相同情况。

    编译器优化可以通过降低检测到错误的概率使函数看起来是可重入的。例如,如果编译器可以确定仅使用 0 或 1 的 status 调用该函数,则它可以将生成的汇编代码优化为以下内容:

    1. 比较 status 和 myArrayOfStructures[index].status 是否相等。

    2. 有条件地将状态写入 myArrayOfStructures[index].status if 不相等。

    这是有效的,因为全局变量被初始化为零,因此数组中的每个元素都将为 0 或 1。此代码对应于 x86 上仅有的两条指令,其中使函数不可重入的情况较少。

    事实上,编译器优化甚至可以使函数可重入。例如,如果编译器可以确定该函数仅在 status 为 0 的情况下被调用,那么函数中的所有代码都将变为死代码,从而使其有效地可重入。这就是为什么我一开始就说“可能不会”。

    【讨论】:

      猜你喜欢
      • 2013-06-20
      • 2010-11-26
      • 2015-08-21
      • 1970-01-01
      • 2020-03-24
      • 2016-05-13
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多