单处理器和多处理器系统仅在您的程序已经无效的那些领域有所不同(根据标准“导致未定义的行为”)。
您的示例程序修改了 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
is 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 写入
对于所有这些情况,解决方案是确保在需要保留包含副本的假设的关键部分期间其他所有人都被锁定,或者在事后检测到错误并进行适当处理。register0
x
这两者实际上是等价的——你需要一个原子指令,它既可以给你变量的当前状态,又可以一次性写入新状态(或者,在旧状态仍然存在的情况下写入新状态)完好无损的)。然后,您可以使用一个单独的变量来表示某人是否在临界区内,或者x
直接在变量上使用这个特殊指令。