ユニプロセッサ システムとマルチプロセッサ システムの違いは、プログラムが既に無効になっている領域 (標準では「未定義の動作を引き起こす」) だけです。
サンプル プログラムは、volatile
修飾子を使用せず、他の ISR の同時実行を防ぐことなく、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
明らかに、これはあなたが望むものではありません。
もう 1 つの問題は、IRQ の優先度レベルによって ISR が相互に割り込む場合と割り込まない場合があることと、マルチプロセッサ システムでは複数の ISR が異なるプロセッサで同時に実行されている可能性があることです。
コードで が適切に使用されていると仮定すると、volatile
優先度の高い割り込みとタスクのスケジューリングが任意の 2 つの命令間で発生する可能性があると仮定することで、理論上の動作を検証できます。スニペットのアセンブラー疑似コードは
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
最後の命令はレジスタ 0 を使用しないため、「想定終了」ステートメントは、削除された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 はビジー ループに入り、状態が変化するのを待ちます
- ISR2 (優先順位が高い) がアクティブであるため、ISR1 はブロックされます。
ケース 1 は次と同等です。
- メインループ読み取り
- ISR読み取り
- ISR 書き込み
- メインループ書き込み
と
- スレッド 1 読み取り
- スレッド 2 読み取り
- スレッド 2 書き込み
- スレッド 1 書き込み
ケース 2 は次と同等です。
- ISR はビジー ループに入り、状態が変化するのを待ちます
- ISR がアクティブであるため、メインループがブロックされている
マルチスレッドの場合、スレッドは互いにブロックしないため、デッドロックは発生しません。
マルチプロセッサ (およびデッドロックではなくマルチスレッドの場合) の場合、追加の障害モードがあります。
- ISR1 読み取り
- ISR2 読み取り
- ISR1 書き込み
- ISR2 書き込み
メインループでは発生しませんが (IRQ は常に優先され、メインループをブロックするため)、複数のスレッドでは発生します。
- スレッド 1 読み取り
- スレッド 2 読み取り
- スレッド 1 書き込み
- スレッド 2 書き込み
これらすべてのケースに対する解決策は、 のコピーを含む仮定を保持する必要があるクリティカル セクションで、他のすべての人がロックアウトされるようにするか、エラーが事後に検出され、適切に処理されるようにすることです。register0
x
これらはどちらも実際には同等です。変数の現在の状態を取得し、新しい状態を一度に書き込む (または、古い状態がまだ残っているという条件で新しい状態を書き込む) アトミック命令が必要です。無傷)。次に、誰かがクリティカル セクション内にいるかどうかを表す別の変数を使用するか、変数に対してこの特別な命令をx
直接使用できます。