『Android 5.0システムを深く解析する』——第6章,第6.1節原子操作

6784 ワード

本節の本は非同期コミュニティ「Android 5.0システムを深く解析する」の第6章から抜粋し、第6.1節の原子操作、著者の劉超、より多くの章の内容は雲生コミュニティ「非同期コミュニティ」の公衆番号を訪問して見ることができる.
6.1原子操作解析Android 5.0システムが単純なタイプのグローバル変数を操作する場合、加算、減算などの単純な操作でも、アセンブリレベルで複数の命令が必要になります.操作全体が完了するには、メモリの値を読み取り、CPUで計算してからメモリに書き込む必要があります.スレッド切り替えが発生し、メモリの値が変更されると、最後に実行された結果にエラーが発生します.このような問題を回避する最善の方法は原子操作を使用することである.
原子操作ではロックは使用されず,効率的にはロックを使用してグローバル変数を保護するよりも得である.しかし、原子操作にも性能上の代価がないわけではないので、できるだけ使用を避けなければならない.
Androidではアセンブリ言語で原子操作関数のセットが実現され,これらの関数は同期機構の実現に広く用いられている.
6.1.1 Androidの原子操作関数1.原子変数の加算操作
int32_t android_atomic_add(int32_t value, volatile int32_t* addr);

                           。

2.            

int32_t android_atomic_inc(volatile int32_t* addr);
int32_t android_atomic_dec(volatile int32_t* addr);
3.        

int32_t android_atomic_and(int32_t value, volatile int32_t* addr);
4.        

int32_t android_atomic_or(int32_t value, volatile int32_t* addr);
5.       

void android_atomic_acquire_store(int32_t value, volatile int32_t* addr);
void android_atomic_release_store(int32_t value, volatile int32_t* addr);

6.原子変数の読み出し
int32_t android_atomic_acquire_load(volatile const int32_t* addr);
int32_t android_atomic_release_load(volatile const int32_t* addr);
                         ,                  ,          。 7.          
int android_atomic_acquire_cas(int32_t oldvalue, int32_t newvalue, volatile int32_t* addr);
int android_atomic_release_cas(int32_t oldvalue, int32_t newvalue, volatile int32_t* addr);

8.さらに2つの原子変数のマクロ定義
#define android_atomic_write android_atomic_release_store
#define android_atomic_cmpxchg android_atomic_release_cas

6.1.2原子操作の実現原理Android原子操作の実現方式はCPUのアーキテクチャと密接な関係があり、現在の原子操作は一般的にCPU指令レベルで実現されている.この実現方式は簡単であるだけでなく,効率も非常に高い.
原子操作のインタフェース関数は10個以上あるが,2つの関数だけがアセンブリコードによって原子操作を実現し,それらは関数android_である.atomic_add()とandroid_atomic_cas()は、他の関数が内部で呼び出されているだけです.この2つの関数の原理はあまり違わない.
ARMプラットフォームの実現はもっと複雑で、以下はARMプラットフォームの加算関数を例に原子変数の実現原理を分析する.
extern ANDROID_ATOMIC_INLINE
int32_t android_atomic_add(int32_t increment, volatile int32_t *ptr)
{
    int32_t prev, tmp, status;
    android_memory_barrier();
    do {
        __asm__ __volatile__ ("ldrex %0, [%4]
" "add %1, %0, %5
" "strex %2, %1, [%4]" : "=&r" (prev), "=&r" (tmp), "=&r" (status), "+m" (*ptr) : "r" (ptr), "Ir" (increment) : "cc"); } while (__builtin_expect(status != 0, 0)); return prev; }

上のコードの最初の行で使用されるマクロANDROID_ATOMIC_INLINEの定義は次のとおりです.
#define ANDROID_ATOMIC_INLINE inline __attribute__((always_inline))

このマクロの役割は、関数をinline関数として定義することです.
コード内の2行目の呼び出しandroid_memory_barrier()関数の役割は、ここでメモリバリアが必要であることを示します(後述するメモリバリア).
次は「インラインアセンブリ」です(「インラインアセンブリ」を知らない場合は筆者のブログを参照してください).「インラインアセンブリ」は分かりにくいですが、次の展開の偽コードで表すことができます.
do {
    ldrex  prev,[ptr]
    add  tmp,  prev,  increment
    strex  status,  tmp, [ptr]
} whiile(status != 0)

add命令の前後には、AMRV 6が新たに導入した同期命令であるldrexとstrexの2つの見慣れない命令がある.ldrex命令の役割は、ポインタptrが指す内容をprev変数に格納するとともに、実行プロセッサにタグ(tag)を付け、ポインタptrのアドレスをタグ付けし、このメモリアドレスにCPUがアクセスしていることを示す.strex命令が実行されると、ptrのアドレスタグが存在するかどうかをチェックし、タグが存在する場合、strex命令はadd命令の実行結果をポインタptrが指すアドレスに書き込み、0を返し、タグをクリアします.返された結果0はstatus変数に保存され、ループが終了し、関数は結果を返します.
strex命令の実行前にスレッドのコンテキスト切替が発生した場合、切替が戻った後、ldrx命令設定のフラグがクリアされます.このときstrex命令を実行すると、このフラグがないため、strex命令はptrポインタの格納操作を完了せず、status変数の戻り結果は1になります.したがって、ループは成功するまで実行を再開します.
builtin_expect()はgccの組み込み関数で、2つのパラメータがあり、最初のパラメータは式であり、2番目のパラメータは値である.式の計算結果も関数の結果です.builtin_expect()は、gcc予測式がより可能な値が何であるかを示すために使用され、gccは予測値に基づいてコードを最適化する.コードで表される意味は、「status!=0」という式の値が「0」であることを予測し、whileループが終了することを予測することです.
画像は、原子操作が中断の発生やコンテキスト切替を禁止するのではなく、操作の結果に影響を与えないことを示す.
6.1.3メモリバリアとコンパイルバリア現代CPUにおける指令の実行順序は必ずしも順序によって実行されるとは限らず、相関性のない指令は順序を乱して実行することができ、CPUの指令ラインを十分に利用し、実行速度を高める.また,コンパイラは命令を最適化し,例えば,命令順序を調整してCPUの命令ラインを利用する.これらの最適化方式は、ほとんどの場合良好に動作するが、いくつかの複雑な状況でエラーが発生する可能性がある.例えば、同期コードを実行すると、最適化によって同期原語の後の命令が同期原語の前に実行される可能性がある.
メモリバリアとコンパイルバリアは,CPUとコンパイラに最適化の停止を知らせる手段である.コンパイルバリアとは、擬似命令「memory」を用いてコンパイラに「memory」の実行前後のコードを混同してはいけないことを伝えることであり、このとき「memory」はバリアを最適化する役割を果たす.メモリバリアは、ARMのdmb、dsb、isb命令、x 86のsfence、lfence、mfence命令などのコードにいくつかの特殊な命令を使用する.CPUは、これらの特殊命令に遭遇した後、前の命令の実行が完了するまで待機してから、後の命令を実行する.これらの命令の役割は、バリアが前後の命令を隔離し、CPUが前後の2つの命令を逆さまに実行することを防止するようなものである.
(1)ARMプラットフォームのメモリバリア指令.
dsb:データ同期バリア命令.その役割は、すべての前の命令が完了するのを待ってから、後の命令を実行することです.
dmb:データメモリバリア命令.前のメモリへのアクセス命令が完了してから、後のメモリへのアクセス命令を実行する役割を果たします.
isb:命令同期バリア.その役割は、流水ライン内のすべての命令の実行が完了するのを待ってから、後の命令を実行することです.
(2)x 86プラットフォーム上のメモリバリア命令.
sfence:バリア命令を格納します.前の書き込みメモリの命令が完了するのを待ってから、後の書き込みメモリの命令を実行する役割を果たします.
lfence:バリア指令を読み出します.前のメモリ読み出し命令が完了してから後のメモリ読み出し命令を実行する役割を果たします.
mfence:ハイブリッドバリア命令.前の読み書きメモリの命令が完了するのを待ってから、後の読み書きメモリの命令を実行する役割を果たします.
これらの命令の意味を正確に理解するには、プロセッサの説明を参照する必要があります.ここでは簡単な紹介をしただけです.Androidがこれらのコマンドを使用してメモリバリアとコンパイルバリアを実現する方法を見てみましょう.
1.ARMプラットフォームの関数コード
(1)コンパイルバリア:
void android_compiler_barrier()
{
    asm_volatile_("" : : : "memory");
}
               memory。

(2)    :

void android_memory_barrier()
{
#if ANDROID_SMP == 0
    android_compiler_barrier();
#else
    __asm__volatile_("dmb" : : : "memory");
#endif
}
void android_memory_store_barrier()
{
#if ANDROID_SMP == 0
    android_compiler_barrier();
#else
    __asm_volatile_("dmb st" : : : "memory");
#endif
}

メモリバリアの関数にマクロANDROIDが使用されています.SMP.この値が0の場合は単一CPUを示しているが,この場合はコンパイルバリアのみを使えばよい.マルチCPUの場合、メモリバリアコマンド「dmb」と、バリアをコンパイルするダミーコマンド「memory」が併用される.関数android_memory_store_barrier()のdmb命令では、オプションstも使用され、前のすべてのメモリの命令が実行された後、後のメモリの命令が実行されるのを待つことを示す.
2.x 86プラットフォームでの関数コード
(1)コンパイルバリア:
void android_compiler_barrier(void)
{
    __asm__ __volatile__ ("" : : : "memory");
}
 ARM     ,               memory。

(2)    :

#if ANDROID_SMP == 0
void android_memory_barrier(void)
{
    android_compiler_barrier();
}
void android_memory_store_barrier(void)
{
    android_compiler_barrier();
}
#else
void android_memory_barrier(void)
{
    asm__volatile_("mfence" : : : "memory");
}
void android_memory_store_barrier(void)
{
    android_compiler_barrier();
}
#endif

x 86プラットフォームも同様であり,単一CPUであればメモリバリアの実装にはコンパイルバリアのみが用いられる.マルチCPUの場合、関数android_memory_barrier()は,CPUコマンド「mfence」を用いて,読み書きメモリの場合をバリアした.でもandroid_memory_store_barrier()関数はコンパイルバリアのみを使用しています.これはIntelのCPUが書き込みメモリの命令を並べ替えないためです.メモリシールド命令は必要ありません.