Linux カーネル リストの WRITE_ONCE



ダブルリンクリストのLinuxカーネル実装を読んでいます。マクロ 08 の使い方がわかりません .これは、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/いつ使うべきかを知る方法は?たとえば、15 に使用されます ただし、25 ではありません :


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;
}

編集:

このファイルと 32 に関するコミット メッセージは次のとおりです。 、しかし、それは私が何かを理解するのに役立ちません...



答え:


あなたが参照する最初の定義は、カーネルロックバリデーター、別名「lockdep」の一部です。 49 (およびその他) 特別な扱いは必要ありませんが、その理由は別の質問の主題です。


関連する定義はここにあり、非常に簡潔なコメントはその目的を述べています:



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



問題


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



  1. 読み取り/書き込みの「引き裂き」:1 回のメモリ アクセスを多数の小さなメモリ アクセスに置き換えます。 GCC は、特定の状況で 55 のようなものを置き換える場合があります (実際に置き換えます!)。 おそらく定数をレジスタに配置してからメモリアクセスなどを行う代わりに、2つの16ビットストアイミディエイト命令を使用します。 69 71 のように、GCC に対して「それをしないでください」と言うことができます。



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





for(;;) {
owner = lock->owner;
if (owner && !mutex_spin_on_owner(lock, owner))
break;
/* ... */
}


  1. 共有メモリへの「タグ付け」アクセスがない場合、できません そのような意図しないアクセスを自動的に検出します。このようなバグを見つけようとする自動ツールは、意図的に際どいアクセスと区別できません。



解決策


まず、Linux カーネルは GCC でビルドする必要があることに注意してください。したがって、このソリューションで対処する必要があるコンパイラは 1 つだけであり、そのドキュメントを唯一のガイドとして使用できます。


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


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


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



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

正確に知らなかったとしましょう 何 111 そうです - そして見つけるのは簡単ではありません! (#5 を参照) - これを達成する別の方法は、メモリ バリアを配置することです。これは、サイズが 1、2、4、または 8 以外の場合に Linux が行うこととまったく同じで、122 and の前にメモリ バリアを配置する 電話の後。メモリ バリアは問題 (2) も簡単に解決しますが、パフォーマンスが大幅に低下します。


C 標準の解釈を深く掘り下げずに概要を説明したことを願っていますが、もしよろしければ、時間を取ってそれを行うこともできます。