【问题标题】:C Program - Output on Uni processor system?C 程序 - 单处理器系统上的输出?
【发布时间】:2012-08-06 06:58:49
【问题描述】:

在嵌入式系统中,由于各种正当原因,趋势仍未完全转向多核处理器。

因此,使用各种机制和多线程特性了解单处理器系统的同步行为仍然很重要。此外,每当我面临面试时,他们都会问我有关特定 C 程序在单处理器系统上的行为的问题。

所以,如果我想在单处理器系统上分析示例 C 程序,以检查它们在家中的行为,我该怎么做?我家的 CPU 有一个 Core i3 处理器。有没有办法让我的操作系统或编译器只考虑一个 CPU 来强制检查行为?

示例:

int x=0;

Snippet-1

    while(x);
    x++;

Snippet-2

    while(!x);
    x--;

考虑到一个单处理器系统,我想检查一个 C 程序的行为,其中

  • 片段 1 和 sn-p 2 在多个线程中
  • 代码段 1 在主程序中,sn-p 2 在 ISR 中
  • Snippet1 和 Snippet2 都在两个不同的 ISR 中(考虑优先级捕获中断,并且在 ISR 内部时,如果有更高优先级的新中断,则立即执行具有最高优先级的中断 - 例如:重置)

在上述问题中,我的主要目标是确定是否会出现死锁,如果存在,需要确定解决方案。请提出你的想法。谢谢。

【问题讨论】:

  • 您可以尝试使用affinity,它允许将进程/线程绑定到特定的CPU(或一组CPU)。
  • 我会说你真的应该重新考虑这一点。凭经验证明死锁(或缺乏死锁)会给你错误的答案——尤其是对于本质上线程不安全的代码。

标签: c multithreading synchronization


【解决方案1】:

您可以根据需要使用“maxcpus”内核参数集简单地启动您的 linux。 它指定 SMP Linux 内核应使用的最大处理器数。 例如 maxcpus=1。

【讨论】:

    【解决方案2】:

    我建议你通过编译新内核来创建一个单处理器系统,在 i3 中只启用一个内核。(显然性能会降低。)

    按照以下链接中的步骤进行操作,

    http://www.cyberciti.biz/tips/compiling-linux-kernel-26.html

    在配置时,

    转到处理器类型和功能-->

    取消选中对称多处理支持

    并研究创建单处理器系统的指令。

    【讨论】:

      【解决方案3】:

      单处理器和多处理器系统的不同之处仅在于您的程序已经无效(根据标准“导致未定义的行为”)。

      您的示例程序修改来自 ISR 的共享变量,而不使用 volatile 修饰符,也没有防范其他 ISR 的并发执行。

      前者的作用是假设x不能改变,编译器可以优化代码:

      while(x);
      x++;
      

      应该编译为执行以下步骤的汇编指令:

      loop:
          read x into register0
          test register0 != 0
          if true => goto loop
          increment register0
          write register0 to x
      

      在优化过程中,编译器看到x不是volatile,将内存访问移到循环外:

          read x into register0
      loop:
          test register0 != 0
          if true => goto loop
          increment register0
          write register0 to x
      

      随后,它发现register0 在循环执行期间从未被修改,因此测试也可以移出循环:

          read x into register0
          test register0 != 0
      loop:
          if true => goto loop
          increment register0
          write register0 to x
      

      然后一些编译器会采取额外的步骤并反转测试,以便能够在循环中使用更便宜的指令

          read x into register0
          test register0 != 0
          if false => goto skip
      loop:
          goto loop
      skip:
          increment register0
          write register0 to x
      

      显然,这不是你想要的。

      另一个问题是,由于 IRQ 优先级,ISR 可能会或可能不会相互中断,而且在多处理器系统中,多个 ISR 可能同时在不同的处理器上运行。

      假设代码正确使用volatile,您可以通过假设任意两条指令之间可以发生更高优先级的中断和任务调度来验证该行为;您的 sn-ps 的汇编器伪代码是

          push register0
      loop:
          load x into register0
          test register0 != 0
          if true => goto loop
          write 1 to x            // can you see what I did there?
          pop register0
      

          push register0
      loop:
          load x into register0
          test register0 == 0
          if true => goto loop
          decrement register0
          write register0 to x
          pop register0
      

      一个可能的星座是

      CPU1    push register0
      CPU2    push register0
      CPU1    load x into register0 [value = 0]
      CPU2    load x into register0 [value = 0]
      CPU1    test register0 != 0 [false]
      CPU2    test register0 == 0 [true]
      CPU1    if true => goto loop [not taken]
      CPU2    if true => goto loop [taken]
      CPU1    increment register0 [value = 1]
      CPU2    read x into register0 [value = 0]
      CPU1    write register0 to x [value = 1]
      CPU2    test register0 == 0 [true]
      CPU1    pop register0
      CPU2    if true => goto loop [taken]
      CPU1    ...
      CPU2    read x into register0 [value = 1]
      CPU1    ...
      CPU2    test register0 == 0 [false]
      CPU1    ...
      CPU2    if true => goto loop [not taken]
      CPU1    ...
      CPU2    decrement register0 [value = 0]
      CPU1    ...
      CPU2    write register0 to x [value = 0]
      CPU1    ...
      CPU2    pop register0
      

      理论上解决这个问题的常用方法是确定持有某些假设的指令范围,然后寻找在面对并发执行时这些假设可能是错误的方式:

          // precondition: address at stack pointer is unused
          // precondition: decrementing the stack pointer will not bring us to a used address
          push register0
          // postcondition: address at stack pointer is unused
          // postcondition: register0 is unused
      

      为了满足这些条件,系统范围的约定是当前堆栈指针下方的所有内存都未使用。这样,ISR 始终可以假定允许将数据推送到堆栈。请注意,写入数据和递减堆栈指针是一个原子操作。如果另一个中断到达这里,它的数据也会被压入堆栈,但使用不同的地址。

      loop:
          // precondition: register0 is unused
          read x into register0
          // begin assumption: register0 contains a copy of x
      

      我想你可以看到这是怎么回事。如果我们从这里开始被打断,x 的值发生了变化,那么这个假设就是错误的。

          test register0 != 0
          // postcondition: processor status contains result of (register0 != 0)
      
          if true => goto loop
          // postcondition[true]: register0 != 0
          // postcondition[false]: register0 == 0
      

      这是我们已经证明退出循环的唯一方法是register0 == 0。因此:

          increment register0
          write register0 to x
          // end assumption: register0 contains a copy of x
      

      可以扩充为

          // precondition: register0 is 0
          increment register0
          // postcondition: register0 is 1
      
          // precondition: register0 is 1
          write register0 to x
          // end assumption: register0 contains a copy of x
      

      然后可以简化为

          // precondition: register0 is 0
          // modified assumption: register0 contains a copy of x, minus one
          // due to precondition, x needs to be written as 1
          write 1 to x
          // end assumption: register0 contains a copy of x, minus one
      

      最后一条指令不使用 register0,因此“结束假设”语句可以向上移动,在现在消除的 increment 操作之前:

          // end assumption: register0 contains a copy of x
          // precondition: register0 is 0
          write 1 to x
      

      前提条件很容易从循环中证明

          // precondition: stack pointer points at address below where we placed the saved copy
          // precondition: memory below the stack pointer is unused
          pop register0
          // postcondition: stack pointer points at unused memory
          // postcondition: stack pointer points at the same address as before the push
          // postcondition: register0 is restored
      

      因此,您需要处理违反假设的情况,即x 的值在我们读取它的时间和新值写回的时间之间被修改的任何情况,以及这种情况您的条件永远不会满足,因为无法调用可能导致它的代码。

      这两种情况都可能发生在单处理器和多处理器设计上;不同之处在于多处理器有一个额外的故障模式,它隐藏了一些错误。

      单处理器的故障模式是

      • ISR1 读取
      • ISR2 读取(ISR2 具有更高的优先级)
      • ISR2 写入
      • ISR1 写入

      • ISR2 进入繁忙循环,等待条件更改
      • ISR1 被阻止,因为 ISR2(更高优先级)处于活动状态

      案例1等价于

      • 主循环读取
      • ISR 读取
      • ISR 写入
      • 主循环写入

      • 线程 1 读取
      • 线程 2 读取
      • 线程 2 写入
      • 线程 1 写入

      情况2等价于

      • ISR 进入繁忙循环,等待条件更改
      • 主循环被阻塞,因为 ISR 处于活动状态

      在多线程的情况下没有死锁,因为线程之间不会互相阻塞。

      对于多处理器(和多线程情况,而不是死锁),还有一个额外的故障模式:

      • ISR1 读取
      • ISR2 读取
      • ISR1 写入
      • ISR2 写入

      主循环不会发生这种情况(因为 IRQ 始终具有优先级并阻塞主循环),但会发生在多个线程中:

      • 线程 1 读取
      • 线程 2 读取
      • 线程 1 写入
      • 线程 2 写入

      对于所有这些情况,解决方案是确保在关键部分期间其他所有人都被锁定,其中假设register0 包含x 的副本需要保持,或者在事后检测到错误并适当处理。

      这两者实际上是等价的——您需要一条原子指令,既可以为您提供变量的当前状态,又可以一次性写入新状态(或者,在旧状态的条件下写入新状态)状态仍然完好)。然后,您可以使用一个单独的变量来表示某人是否在临界区内,或者直接在变量x 上使用此特殊指令。

      【讨论】:

        【解决方案4】:

        这在 Windows 中很容易做到。在 Windows 任务管理器中,单击进程选项卡。在进程列表中,找到您的程序,单击鼠标右键,然后在下拉菜单中单击“设置关联...”。这会弹出一个对话框,您可以在其中设置可用于运行进程的处理器。取消选中除一个以外的所有处理器,您的程序将仅在该处理器上运行。不幸的是,每次启动程序时都必须这样做。

        【讨论】:

          猜你喜欢
          • 2014-08-17
          • 2013-07-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2014-11-22
          • 1970-01-01
          • 1970-01-01
          • 2013-04-13
          相关资源
          最近更新 更多