私はARM用の組み込みOSに取り組んできましたが、ARMARMとLinuxソースを参照した後でも、アーキテクチャについて理解できなかったことがいくつかあります。
原子操作。
ARM ARMは、ロードおよびストア命令はアトミックであり、割り込みハンドラーが実行される前に実行が完了することが保証されていることを示しています。
Arch/arm/include/asm/atomic.h :
#define atomic_read(v) (*(volatile int *)&(v)->counter)
#define atomic_set(v,i) (((v)->counter) = (i))
ただし、ARMv7(ターゲット)にLDREXとSTREXを使用するCPU命令(atomic_inc、atomic_dec、atomic_cmpxchgなど)を使用してこの値をアトミックに操作したい場合に問題が発生します。
ARMARMは、このセクションでブロックされている割り込みについては何も述べていないため、LDREXとSTREXの間で割り込みが発生する可能性があります。それが言及していることは、メモリバスをロックすることについてであり、これはMPより多くのCPUが同時に同じ場所にアクセスしようとする可能性があるシステムにのみ役立つと思われます。 MP)、LDREXとSTREXのこの小さなウィンドウでタイマー割り込み(またはSMPの場合はIPI)が発生すると、例外ハンドラーが実行され、CPUコンテキストが変更されて新しいタスクに戻る可能性がありますが、衝撃的な部分が発生すると、「CLREX」が実行されます。したがって、以前のスレッドによって保持されていた排他ロックをすべて削除します。したがって、LDREXとSTREXをLDRよりも、UPシステムでのアトミック性のためにSTR=
排他的ロックモニターについて何か読んだので、スレッドが再開してSTREXを実行すると、osモニターがこの呼び出しを失敗させて検出できるため、新しいループを使用してループを再実行できると考えられますプロセスの値(LDREXに戻る)、私はここにいますか?
さて、彼らの website から答えを得ました。
プロセスがLoad-Exclusiveを実行した後、Store-Exclusiveを実行する前に、コンテキストスイッチがプロセスをスケジュールすると、プロセスが再開したときにStore-Exclusiveが偽陰性の結果を返し、メモリは更新されません。プロセスは操作をすぐに再試行できるため、これはプログラムの機能には影響しません。
Load-linked/store-exclusiveパラダイムの背後にある考え方は、ストアがロード後にvery soonの場合、メモリ操作が介在せず、他に何も場所に触れていない場合、ストアは可能性が高い成功しますが、他の何かがその場所に触れた場合、ストアは特定の失敗します。明らかな理由もなくストアが失敗しないことが保証されているわけではありません。ただし、ロードとストアの間の時間が最小限に抑えられていて、それらの間にメモリアクセスがない場合、次のようなループが発生します。
do
{
new_value = __LDREXW(dest) + 1;
} while (__STREXW(new_value, dest));
一般に、数回の試行で成功することが期待できます。古い値に基づいて新しい値を計算するためにかなりの計算が必要な場合は、ループを次のように書き直す必要があります。
do
{
old_value = *dest;
new_value = complicated_function(old_value);
} while (CompareAndStore(dest, new_value, old_value) != 0);
... Assuming CompareAndStore is something like:
uint32_t CompareAndStore(uint32_t *dest, uint32_t new_value, uint_32 old_value)
{
do
{
if (__LDREXW(dest) != old_value) return 1; // Failure
} while(__STREXW(new_value, dest);
return 0;
}
このコードは、新しい値の計算中に何かが* destに変更された場合、メインループを再実行する必要がありますが、__ STREXWが他の理由で失敗した場合、小さなループのみを再実行する必要があります__LDREXWと__STREXWの間には約2つの命令しかありません]
補遺 「古いものに基づいて新しい値を計算する」が複雑になる可能性がある状況の例は、「値」が事実上複雑なデータ構造への参照である場合です。コードは古い参照をフェッチし、古い参照から新しいデータ構造を派生させてから、参照を更新する場合があります。このパターンは、「ベアメタル」プログラミングよりもガベージコレクションフレームワークで頻繁に発生しますが、ベアメタルをプログラミングする場合でもさまざまな方法で発生する可能性があります。通常のmalloc/callocアロケーターは、通常、スレッドセーフ/割り込みセーフではありませんが、固定サイズの構造体のアロケーターは、多くの場合、スレッドセーフです。 2の累乗のデータ構造(たとえば255)の「プール」がある場合は、次のようなものを使用できます。
#define FOO_POOL_SIZE_SHIFT 8
#define FOO_POOL_SIZE (1 << FOO_POOL_SIZE_SHIFT)
#define FOO_POOL_SIZE_MASK (FOO_POOL_SIZE-1)
void do_update(void)
{
// The foo_pool_alloc() method should return a slot number in the lower bits and
// some sort of counter value in the upper bits so that once some particular
// uint32_t value is returned, that same value will not be returned again unless
// there are at least (UINT_MAX)/(FOO_POOL_SIZE) intervening allocations (to avoid
// the possibility that while one task is performing its update, a second task
// changes the thing to a new one and releases the old one, and a third task gets
// given the newly-freed item and changes the thing to that, such that from the
// point of view of the first task, the thing never changed.)
uint32_t new_thing = foo_pool_alloc();
uint32_t old_thing;
do
{
// Capture old reference
old_thing = foo_current_thing;
// Compute new thing based on old one
update_thing(&foo_pool[new_thing & FOO_POOL_SIZE_MASK],
&foo_pool[old_thing & FOO_POOL_SIZE_MASK);
} while(CompareAndSwap(&foo_current_thing, new_thing, old_thing) != 0);
foo_pool_free(old_thing);
}
複数のスレッド/割り込み/同じものを同時に更新しようとするものがほとんどない場合、このアプローチでは更新を安全に実行できます。同じアイテムを更新しようとする可能性のあるものの間に優先関係が存在する場合、最も優先度の高いものが最初の試行で成功することが保証され、次に優先度の高いものが優先されない試行で成功する最も優先度の高いタスクなど。ロックを使用している場合、更新を実行したい最も優先度の高いタスクは、優先度の低い更新が完了するまで待機する必要があります。 CompareAndSwapパラダイムを使用すると、最も優先度の高いタスクは下位のタスクの影響を受けません(ただし、下位のタスクは無駄な作業を行う必要があります)。