单处理器和多处理器系统的不同之处仅在于您的程序已经无效(根据标准“导致未定义的行为”)。
您的示例程序修改来自 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 上使用此特殊指令。