web-dev-qa-db-ja.com

LinuxカーネルリストのWRITE_ONCE

二重リンクリストの linuxカーネル実装 を読んでいます。マクロWRITE_ONCE(x, val)の使い方がわかりません。これは、compiler.hで次のように定義されています。

_#define WRITE_ONCE(x, val) x=(val)
_

次のように、ファイル内で7回使用されます。

_static inline void __list_add(struct list_head *new,
                  struct list_head *prev,
                  struct list_head *next)
{
    next->prev = new;
    new->next = next;
    new->prev = prev;
    WRITE_ONCE(prev->next, new);
}
_

競合状態を回避するために使用されることを読みました。

2つの質問があります。
1 /マクロはコンパイル時にコードで置き換えられると思いました。それでは、このコードは次のコードとどのように異なりますか?このマクロはどのように競合状態を回避できますか?

_static inline void __list_add(struct list_head *new,
                  struct list_head *prev,
                  struct list_head *next)
{
    next->prev = new;
    new->next = next;
    new->prev = prev;
    prev->next = new;
}
_

2 /いつ使用すべきかを知る方法は?たとえば、__lst_add()には使用されませんが、__lst_splice()には使用されません。

_static inline void __list_splice(const struct list_head *list,
                 struct list_head *prev,
                 struct list_head *next)
{
    struct list_head *first = list->next;
    struct list_head *last = list->prev;

    first->prev = prev;
    prev->next = first;

    last->next = next;
    next->prev = last;
}
_

編集:
これは、このファイルと_WRITE_ONCE_に関するコミットメッセージですが、何かを理解するのに役立ちません...

list:list_head構造体を初期化するときにWRITE_ONCE()を使用します
非RCUリストのロックなしの空性テストを行うコードは、特にINIT_LIST_HEAD()がlist_del_init()から呼び出された場合に、INIT_LIST_HEAD()を使用してリストヘッドの->次のポインタをアトミックに書き込みます。したがって、このコミットはWRITE_ONCE()をこの関数のポインターストアに追加し、ヘッドの->次のポインターに影響を与える可能性があります。

36
Gaut

あなたが参照する最初の定義は、 "-lockdep"とも呼ばれる kernel lock validator の一部です。 _WRITE_ONCE_(およびその他)は特別な扱いは必要ありませんが、その理由は別の質問の主題です。

関連する定義は here であり、非常に簡潔なコメントで、その目的は次のとおりです。

コンパイラが読み取りまたは書き込みをマージまたは再フェッチしないようにします。

...

順序付けを必要としない、または必要な順序付けを提供する明示的なメモリバリアまたはアトミック命令と相互作用するアクセスを、コンパイラがフォールド、スピンドル、またはその他の方法で切断しないようにします。

しかし、それらの言葉はどういう意味ですか?


問題

問題は実際には複数形です。

  1. 「ティアリング」の読み取り/書き込み:単一のメモリアクセスを多数の小さなメモリアクセスに置き換えます。特定の状況では、GCCは_p = 0x01020304;_のようなものを2つの16ビットストアイミディエイト命令で置き換える可能性があります(実際にそうします!)定数をレジスターに配置してからメモリアクセスなどを行う代わりに、 _WRITE_ONCE_を使用すると、GCCに「それをしないでください」と次のように言うことができます。WRITE_ONCE(p, 0x01020304);

  2. Cコンパイラは、Wordアクセスがアトミックであることの保証を停止しました。レースフリーでないプログラムはどれも miscompiled になり、素晴らしい結果が得られます。それだけでなく、コンパイラーは特定の値をループ内のレジスターにnotに保持することを決定する場合があり、複数の参照が次のようなコードを混乱させる可能性があります。

 for(;;){
 owner = lock-> owner; 
 if(owner &&!mutex_spin_on_owner(lock、owner))
 break; 
/* ... */
} 
  1. 共有メモリへの「タグ付け」アクセスがない場合、その種類の意図しないアクセスは自動的に検出されません。 そのようなバグを見つける を試みる自動化されたツールは、それらを意図的に際どいアクセスから区別することができません。

ソリューション

LinuxカーネルはGCCで構築することを要求していることに注目することから始めます。したがって、ソリューションで処理する必要があるコンパイラは1つだけであり、その documentation を唯一のガイドとして使用できます。

一般的なソリューションでは、すべてのサイズのメモリアクセスを処理する必要があります。さまざまな種類の特定の幅、およびその他すべての幅があります。また、既にクリティカルセクションにあるメモリアクセスに特別なタグを付ける必要がないことにも注意してください(理由は何ですか?)。

サイズが1、2、4、8バイトの場合、適切なタイプがあり、volatileは、GCCが(1)で参照した最適化を適用すること、および その他の場合 (「コンパイラバリア」の最後の箇条書き)。また、GCCが(2)でループを誤ってコンパイルすることを禁止します。これは、シーケンスポイント間でvolatileアクセスが移動するためであり、C標準では許可されていません。 Linux ses オブジェクトを揮発性としてタグ付けする代わりに、「揮発性アクセス」(以下を参照)と呼びます。特定のオブジェクトをvolatileとしてマークすることで問題を解決できますができますが、これは(ほとんど?)決して良い選択ではありません。 多数理由 あります有害な可能性があります。

これは、8ビット幅の型の揮発性(書き込み)アクセスがカーネルに実装される方法です。

 *(volatile __u8_alias_t *)p = *(__ u8_alias_t *)res; 

exactlyvolatileが何をしているかを知らなかったとします (簡単ではありません! (チェックアウト#5)-これを達成する別の方法は、メモリバリアを配置することです。これは、サイズが1、2、4、または8以外の場合にmemcpyおよび呼び出しの前およびにメモリバリアを配置する。メモリバリアは問題(2)も簡単に解決しますが、パフォーマンスが大幅に低下します。

C標準の解釈を掘り下げることなく概要を説明できたと思いますが、よろしければ時間をかけてそれを行うことができます。

31