0

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

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

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

例子:

int x=0;

片段 1

    while(x);
    x++;

片段 2

    while(!x);
    x--;

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

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

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

4

4 回答 4

2

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

于 2012-08-06T07:59:05.097 回答
0

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

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

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

配置时,

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

取消选中Symmetric Multiprocessing Support

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

在此处输入图像描述

于 2012-08-06T07:36:01.850 回答
0

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

您的示例程序修改了 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

在优化过程中,编译器看到xis not 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,您可以通过假设任意两条指令之间可以发生更高优先级的中断和任务调度来从理论上验证行为;您的代码段的汇编器伪代码是

    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

这就是我们已经证明退出循环的唯一方法是 when 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 写入

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

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

于 2012-08-06T13:03:18.633 回答
0

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

于 2012-08-06T13:21:19.617 回答