perfbook 付録F7-F9の訳

以下は、perfbook の付録F7からF9の kanda.motohiro@gmail.com による訳です。perfbook の訳の他の部分は、親文書を参照。付録の続きは、こちらを参照。

F.7 ロック

7.1 ロックが、研究論文において、むち打たれる少年である理由は、それが現実にたくさん使われているからです。そうでなくて、ロックを誰も使わず、気にしないなら、ほとんどの研究論文はわざわざそれに触れることもないでしょう。

7.2

グラフにサイクルが無いと仮定します。すると、directed acyclic graph (DAG) ができます。それは少なくても1つのリーフノードを持つはずです。

もしこのリーフノードがロックなら、どのスレッドにも持たれていないロックを待っているスレッドがいることになり、定義に反します。(そして、この場合、そのスレッドは、直ちにロックを取れます。)

一方、そのリーフノードがスレッドなら、ロックを何も待っていないスレッドがいることになり、これも定義に反します。(そして、この場合、そのスレッドは実行中であるか、ロックでないもののためにブロックしています。)

なので、このデッドロックの定義によれば、対応するグラフにサイクルがなくてはいけません。

7.3

まさしくあります!以下は、そのわずかです。

1 ライブラリ関数の引数の1つが、ロックへのポインタで、このライブラリ関数でそのロックを取るとします。そしてもし、ライブラリ関数が、自分のロックの一つを保持している時に、コール元のロックを取ろうとすると、コール元とライブラリの両方を含むデッドロックサイクルができることがあります。

2 ライブラリ関数の戻り値が、ロックへのポインタで、コール元でそのロックを取るとします。そしてもし、コール元が、ライブラリのロックを保持している時に、コール元のロックの1つを取ろうとすると、またも、デッドロックになります。

3 ライブラリ関数の一つがロックを取り、それを持ったまま戻ったとします。そしてもし、コール元が自分のロックの一つを取ろうとすると、またまた、デッドロックになります。

4 コール元が、ロックを取るシグナルハンドラを持っているとします。すると、デッドロックになります。しかしこの場合、ライブラリのロックは、デッドロックサイクルにおいて無実の傍観者です。しかし、シグナルハンドラからロックを取るのは、ほとんどの環境で、ダメダメであることに注意下さい。それは悪い考えであるだけでなく、サポートされてないのです。

7.4

比較されるデータ要素をプライベート化します。(8章で述べるように)あるいは、参照カウンティングのような、遅延機構を使います。(9章で述べるように)

7.5 ロッキングプリミティブです。もちろん! 7.6 絶対に違います! あるプログラムが、mutex_a そして mutex_b を、この順に取るとします。そして、 mutex_a を pthread_cond_wait に渡します。ここで、pthread_cond_wait は、mutex_a を放し、戻る前に再度、確保します。何か他のスレッドがその間に mutex_a をとって、mutex_b を待ってブロックするなら、プログラムはデッドロックします。 7.7 絶対に違います! この変換は、layer_2_processing() 関数が冪等であることを前提とします。つまり、layer_1() のルーティング決定が変わったら、同じパケットに対して複数回実行することができます。なので、本当の人生では、この変換は任意に複雑となります。

7.8 そうかも。 もし、layer_1() のルーティング決定がとても頻繁に変わると、コードは常にリトライし、決して前に進みません。これは、どのスレッドも進むことができない時は「ライブロック」と呼ばれ、あるスレッドは進むけど他のスレッドは進まない時は、「飢餓」と呼ばれます。(7.1.2節を参照。) 7.9 もう一つ、グローバルロックを追加します。あるスレッドが必要なロックを取るのを、何度も繰り返して試み、失敗した時には、そのスレッドに、無条件に新しいグローバルロックを取らせます。その後、無条件に必要なロックを全て取らせます。(Doug Lea の提案によります。)

7.10

なぜなら、これはデッドロックになるからです。シグナルハンドラ以外のところで、シグナルをブロックすることなく、ロックAが取られているとします。このロックを持っている時にシグナルが来るかもしれません。対応するシグナルハンドラは、次に、ロックBを取ろうとします。こうして、ロックAを持ったまま、ロックBが取られます。このため、問題で述べたように、ロックBを持っている時にさらにロックAを取るなら、デッドロックサイクルになります。

なので、シグナルハンドラの中で取られる可能性のあるロックを持っている時に、シグナルをブロックすることなく、シグナルハンドラ以外のところで取られる可能性のある別のロックをさらに取るのは違法です。

7.11

最も簡単で、高速な一つの手段は、シグナルを設定する時に sigaction() に渡す struct sigaction の sa_mask フィールドを使うことです。

7.12

なぜならば、これらの同じ規則が、オペレーティングシステムカーネルと、ある種の組み込みアプリケーションで使われる、割り込みハンドラに適用できるからです。

多くのアプリケーション環境では、シグナルハンドラでロックを取るのは非難されることです。しかし、それは、賢い開発者が、(通常は愚かにも)自作のロックをアトミック操作を使って作るのを止めることはできません。そして、アトミック操作は多くの場合、シグナルハンドラで完全に合法です。

7.13

アプローチはたくさんあります。

1 シミュレーションでパラメタサーチをする場合、(例えば)機械あるいは電子デバイスの優れた設計に集中するために、多数のシミュレーションが実行される場合は、シミュレーションはシングルスレッドのままにしておいて、そのシミュレーションの多数のインスタンスを並列に実行します。これは、オブジェクト指向デザインを保持し、しかもより高いレベルでの並列性を得ます。さらに、同期オーバヘッドも避けることが期待されます。

2 オブジェクトをグループに分割し、ある時点では、一つ以上のグループのオブジェクトを操作する必要が無いようにします。そして、それぞれのグループにロックを関連付けます。これは、7.1.17節で述べた、一度に一つのロック設計の例です。

3 オブジェクトをグループに分割し、すべてのスレッドはグループのオブジェクトを、なんらかのグループごとの順序に従って操作できるようにします。そして、それぞれのグループにロックを関連付けます。さらに、グループ間にロック階層を強制します。

4 ロックに、任意に決めた階層を強制します。そして、7.1.15節で述べたように、ロックを逆順に取らないといけない時は条件付きロックを使います。

5 ある操作のグループを実行する前に、どのロックが取られるかを予想します。そして、実際に何らかの更新を行う前にそれらを取ろうと試みます。予想が間違っていた時は、すべてのロックを放して、新しい予想のもとでやり直します。それは、経験によって改善しているはずです。このアプローチは、7.1.1.6節で述べました。

6 トランザクショナルメモリを使います。このアプローチは多くの利点と欠点を持ち、16.2節で議論します。

7 アプリケーションをリファクタして、同時実行に優しくします。これは、そのアプリケーションを、シングルスレッドの時にでも速くする副作用も持つことが期待されます。しかし、そのアプリケーションを変更するのをより難しくすることもあるかもしれません。

8 ロックの他に、これ以降の章で述べるテクニックを使います。

7.14

図7.10に、良いヒントがあります。多くの場合、ライブロックは、あなたがご自分のロック設計を見直すべきだということを示します。あるいは、そのロック設計が「できたばかり」なら、初めて、よく見る機会です。

とはいえ、1つの良い、十分なアプローチは、Doug Lea によるもので、7.1.1.5節で述べる条件付きロックを使うことです。さらに、7.1.1.6節で述べる。共用データを変更する前に、まず必要なロックをすべて取るというのと組み合わせます。もしあるクリティカルセクションがあまりに多くリトライする時は、無条件にグローバルロックを取って、そして無条件に必要なロックを全て取ります。これにより、デッドロックとライブロックを避けます。さらに、グローバルロックがあまりに頻繁に取られる必要のある時を除けば、けっこうスケールします。

7.15

いくつかあります。

1 1秒の待ちは、ほとんどの用途にはかなり長過ぎます。待ち時間は、ほぼクリティカルセクションを実行するのに必要な時間から始めるべきです。それは普通は、マイクロ秒かミリ秒の範囲でしょう。

2 コードは、オーバーフローを考慮していません。一方、このバグは、前のバグによって、無害になります。32ビットの秒は、50年以上です。

7.16

ある意味ではそれも良いでしょう。しかし、時には高いロック競合になる設計を使うのが適切である状況もあります。

例えば、まれにエラー条件が起きるシステムを考えましょう。まれなエラー条件の間は、低い性能とスケーラビリティを持つ単純なエラー処理の設計をするほうが、複雑でデバッグが困難で、前記まれなエラー条件が実際に起きた時にだけ役に立つ設計よりも良いかもしません。

とはいえ、普通は、単純であり、かつ、エラー条件のときにも効率が良い設計を作るように努力するのは意味のあることです。例えば、問題を分割することによって。

7.17

ロックで守られるデータがロック自身と同じキャッシュラインにあると、他のCPUがそのロックを取ろうと試みるとき、ロックを持っているCPUにとって高価なキャッシュミスが起きます。これは、偽の共有の特別ケースで、異なるロックで守られる変数の対がたまたまキャッシュラインを共有すると起きることがあります。それに対して、ロックがそれが守るデータと異なるキャッシュラインにあれば、ロックを持っているCPUは通常はその変数への最初のアクセスの時にキャッシュミスをするだけです。

もちろん、ロックとデータを別々のキャッシュラインに置く欠点は、競合のないときにコードが1回でなく2回のキャッシュミスを起こすことです。

7.18

この使い方はまれです。しかし、たまに行われます。要点は、排他ロックのセマンティクスが2つのコンポーネントを持つことです。(1)よく知られた、データ保護のセマンティクスと、(2)メッセージのセマンティクスです。後者では、あるロックを放すと、同じロックの確保を待っている人に通知がされます。空のクリティカルセクションは、データ保護コンポーネントを使わないで、メッセージのコンポーネントを使います。

この答えの残りの部分は、空のクリティカルセクションの使用例をいくつか提供します。しかし、これらの例は、「灰色の魔法」と考えられるべきです。

脚注

この言葉を考えてくれた Alexey Roytman に感謝します。

なので、空のクリティカルセクションは、ほとんど決して、現実では使われません。ですが、この灰色の領域に進みましょう。。。

空のクリティカルセクションの1つの歴史的な使用は、2.4 Linux カーネルのネットワークスタックに現れました。この使用パターンは、9.3節で述べるリード、コピー、アップデート(RCU)の効果を近似する方法と考えることができます。

空のクリティカルセクションのイディオムは、ある状況でロック競合を減らすのに使うこともできます。例えば、マルチスレッドのユーザ空間アプリケーションで、それぞれのスレッドがスレッドごとのリストで維持される、仕事の単位を処理する時を考えましょう。スレッドは、お互いのリストを触ることは禁止されています。そして、また、更新があり、更新が進む前には、全ての以前にスケジュールされた仕事の単位が完了していることが必要です。これを処理する1つの方法は、それぞれのスレッドで1つの仕事の単位をスケジュールすることです。これらの全ての仕事の単位が完了すれば、更新は進むことができます。

あるアプリケーションでは、スレッドは来て、去ります。例えば、それぞれのスレッドはそのアプリケーションの1人のユーザに対応するかもしれません。なので、そのユーザがログアウトあるいは切断したら、スレッドは除去されます。多くのアプリケーションで、スレッドはアトミックに去ることはできません。そうでなく、そのアプリケーションのいろいろな部分から、特別のアクションのシーケンスを使って、明示的に自身を取り除いていく必要があります。1つの特定のアクションは、それ以上の要求を他のスレッドから受け取るのを拒否することでしょう。そして、もう一つの特定のアクションは、自分のリストに残った全ての仕事の単位を捨てることでしょう。例えば、これらの仕事の単位をグローバルな、仕事のアイテムを捨てるためのリストに置いて、残った他のスレッドの一つに拾ってもらうようにすることでしょう。(単に、そのスレッドの仕事のアイテムのリストを順に実行して、空にしたらどうでしょう?ある仕事のアイテムはさらに多くの仕事のアイテムを生成するかもしれないので、リストは期限内に空にできないかもしれません。)

アプリケーションが性能とスケールの点でうまくやるには、良いロック設計が必要です。1つの一般的な解決策は、一つのグローバルロック(Gと呼びましょう)を、去るプロセス全体(そしてたぶん、他のものも)を保護するために使い、より細かい粒度のロックをそれぞれの除去操作を保護するために使うことです。

さて、去るスレッドは明らかに、自分のリストにある仕事を捨てる前に、それ以上の要求を受け付けるのを拒否しないといけません。なぜなら、そうしないと捨てたアクションの後から、新しい仕事が届き、せっかく捨てたのが無効になります。なので、去るスレッドのための単純化された擬似コードは、以下になります。

1 ロックGを取る。

2 コミュニケーションを守るロックを取る。

3 他のスレッドからの以降のコミュニケーションを拒否する。

4 コミュニケーションを守るロックを放す。

5 グローバルな仕事アイテムを捨てるためのリストを守るロックを取る。

6 全てのペンディングの仕事アイテムを、グローバルな仕事のアイテムを捨てるためのリストに移す。

7 グローバルな仕事アイテムを捨てるためのリストを守るロックを放す。

8 ロックGを放す。

もちろん、全ての既存の仕事アイテムを待つ必要のあるスレッドは、去るスレッドも考慮に入れないといけません。これを考えるために、このスレッドが、去るスレッドが他のスレッドからのコミュニケーションを拒否したちょうどすぐ後に、全ての既存の仕事アイテムが終わるのを待ち始めたとしましょう。このスレッドは、どうしたら、去るスレッドの仕事アイテムが完了するのを待つことができるでしょう?スレッドは、他のスレッドの仕事アイテムのリストをアクセスするのは許されないのを思い出して下さい。

一つの直裁的なアプローチは、このスレッドがG を取って、次に、グローバルな仕事アイテムを捨てるためのリストを守るロックを取って、次に、仕事アイテムを自分のリストに移すことです。

そのスレッドは、次に、両方のロックを放し、仕事アイテムを自分のリストの最後に置き、そして、自分がそれぞれのスレッド(自分も含む)のリストに置いた仕事アイテムが全て完了するのを待ちます。

このアプローチは、多くの場合にうまくいきます。しかし、それぞれの仕事アイテムが、グローバルな仕事アイテムを捨てるためのリストから取り出されるときに特別な処理が必要だとすると、Gの競合が激しくなります。この競合を避ける一つの方法は、Gを取って、すぐに放すことです。そうすると、全ての既存の仕事アイテムを待つ処理は、以下のようになります。

1 グローバルカウンタを1にして、条件変数をゼロにします。

2 全てのスレッドにメッセージを送って、それぞれが、グローバルカウンタをアトミックに加算し、仕事アイテムをエンキューするようにさせます。仕事アイテムはアトミックにグローバルカウンタを減算し、結果がゼロなら、条件変数を1にします。

3 Gを取ります。これにより、現在去ろうとしているスレッドが去り終わるのを全て待つことになります。一度に一つのスレッドしかされないので、残りのスレッドは全て、前の手順で送ったメッセージを受け取っているはずです。

4 Gを放します。

5 グローバルな仕事アイテムを捨てるためのリストを守るロックを取ります。

6 グローバルな仕事アイテムを捨てるためのリストから全ての仕事アイテムを、このスレッドのリストに移します。

7 グローバルな仕事アイテムを捨てるためのリストを守るロックを放します。

8 このスレッドのリストに、追加の仕事アイテムをエンキューします。(前と同じく、この仕事アイテムは、グローバルカウンタを減算し、もし結果がゼロなら、条件変数を1にします。)

9 条件変更が値1を取るのを待ちます。

この手順が完了したら、全ての既存の仕事アイテムは完了したことが保証されます。空のクリティカルセクションは、ロックを、データ保護のためだけでなく、メッセージとしても使います。

7.19

実際、いくつかのやり方があります。一つの方法は、 null、保護されたリード、排他モードを使うことです。他の方法は、null、保護されたリード、並列同時ライトモードを使うことです。3つ目の方法は、null、並列同時リード、排他モードを使うことです。

7.20

単一のグローバルロックを条件付きで確保するのは、確かにとてもうまくいきます。ただし、比較的少ない数のCPU に限ります。なぜこれが、数百のCPUを持つシステムで問題になるかを考えるために、図5.3を見て、遅延を8から1000CPUに外挿して下さい。

7.21

なぜかって?これは、並列制御において、人生と同じく、ゲームを始める前に、勝利が実際に何を伴うのかを調べるべきだということを、単に示しているのです。

7.22 なぜならば、このデフォルト初期化は、ある関数のスコープ内で auto 変数としてアロケートされたロックに対しては効かないからです。

7.23 そのロックが確保されていて、いくつかのスレッドがそのロックを取ろうとしているとします。この状況では、これらのスレッドが全部、アトミック交換操作でループしたならば、そのロックを含むキャッシュラインはそれらの間でピンポンされて、インターコネクトに負荷を与えます。一方、これらのスレッドが7から8行目の内側のループでスピンしたなら、それらはすべて自分自身のキャッシュ内でスピンしますから、インターコネクトには無視できる負荷しか与えません。 7.24 それも正しい実装です。ただし、このストアの前にメモリバリアがあって、それが ACCESS_ONCE() を使う限りにおいて。xchg() 操作を使う時にはメモリバリアは不要です。なぜならばそれが値を返すためにその操作は完全なメモリバリアを暗黙的に意味するからです。

7.25 C 言語では、以下のマクロがこれを正しく扱います。

#define ULONG_CMP_LT(a, b) \

(ULONG_MAX / 2 < (a) - (b))

単純に二つの符号付き整数を引き算しがちですが、それはいけません。C 言語では、符号付きオーバーフローは未定義です。例えば、もしコンパイラが、値の片方が正数でもう一つが負数だと知っていれば、単純に、正数のほうが負数より大きいと判断する権利があります。しかし、正数から負数を引いた結果がオーバーフローして、負数になることもあります。

コンパイラは、どのようにして2つの数の符号を知るのでしょう?以前のポインタアサインメントあるいは、比較から推測できるかもしれません。この場合、もしCPU ごとのカウンタが符号付きであれば、コンパイラはそれらが常に値が増加すると推論できます。そして、決して負にならないと仮定するかもしれません。この仮定は、コンパイラに不幸なコードを生成させることになります。

7.26 フラグアプローチは通常、キャッシュミスが少ないと思われます。しかし、より良い答えは、両方をやってみて、あなたの固有のワークロードにとってどちらが良いか、見ることです。

7.27 以下は、暗黙的存在保証の不適切な使用に起因するバグのいくつかです。

1 プログラムが、グローバル変数のアドレスをファイルに書きます。そして、後で、その同じプログラムのインスタンスがそのアドレスを読んで、ディレファレンスしようとします。これは、アドレス空間ランダム化のために失敗することがあります。そのプログラムを再コンパイルしたら、言うまでもありません。

2 モジュールが、自分の変数の一つのアドレスを他のモジュールにあるポインタに記録するとします。そのモジュールがアンロードされた後で、そのポインタをディレファレンスしようとします。

3 関数が、自分のスタック上の変数の一つのアドレスをグローバルポインタに記録するとします。その関数が戻った後で、他の関数がそれをディレファレンスしようとします。

あなたはきっと他の可能性も思いつくことでしょう。

7.28

これはとても単純なハッシュテーブルで、チェーニングはありません。なので、あるバケツにある唯一の要素は、最初の要素です。読者は、この例を、完全なチェーニングを持つハッシュテーブルに適用すると良いでしょう。

7.29

以下のイベントのシーケンスを考えましょう。

1 スレッド0が、delete(0) を呼んで、図の10行目に来て、ロックを取ります。

2 スレッド1が、同時に delete(0) を呼び、10行目に来て、スレッド0がロックを持っているのでスピンします。

3 スレッド0は、11から14行目を実行し、その要素をハッシュテーブルから除き、ロックを放し、要素を解放します。

4 スレッド0は、実行を続け、メモリを確保し、今解放したばかりのそのブロックを得ます。

5 スレッド0は、このメモリブロックを、何か他の型の構造として初期化します。

6 スレッド1の、 spin_lock() 操作は失敗します。 p->lock であると信じているものが、既にスピンロックでは無いという事実のためです。

存在保証が無いため、データ要素のアイデンティティは、あるスレッドがその要素のロックを、10行目で確保しようとしている間に変わることがあります!

F.8 データ所有 8.1 関数での auto 変数の使用です。デフォルトで、これらは、現在の関数を実行しているスレッドにプライベートです。 8.2 sh & 演算子を使ったスレッド作成と、 sh wait コマンドを使ったスレッドジョインです。 もちろん、プロセスが、例えば、shmget() や mmap() システムコールによって、明示的にメモリを共有するときは、共用メモリをアクセスあるいは更新するときには明示的な同期がもちろん必要です。プロセスは、以下のプロセス間通信機構のどれを使っても、同期することができます。 1 システム V セマフォー 2 システム V メッセージキュー 3 UNIX ドメインソケット 4 TCP/IP, UDP そしてその他いろいろのネットワークプロトコル 5 ファイルロック 6 O_CREAT と O_EXCL フラグをつけた open() システムコールの使用 7 rename() システムコールの使用 可能な同期機構の完全なリストを作るのは、読者への課題とします。なお、とても長いリストになるだろうと警告します。驚くほどの数の予期しないシステムコールを、同期機構として使うことができます。 8.3 これは哲学的質問です。 「いいえ」と答えたい人は、プロセスは定義からして、メモリを共用しないと主張するでしょう。 「はい」と答えたい人は、共用メモリを必要としない多数の同期機構をリストするでしょう。なお、カーネルはいくらかの共用状態を持ちますし、プロセスID (PID) の割り当てでさえ、共用データを構成すると主張することもできるでしょう。 そのような議論は、素晴らしい知的演習です。そして、自分が知的だと感じたり、不運なクラスメートや同僚から点を勝ち取ったり、(特に!)なにか役に立つことをするのを避けるための素晴らしい方法です。 8.4 十分驚くべきことに、はい、です。一つの例は、単純なメッセージパッシングシステムで、スレッドが他のスレッドのメールボックスにメッセージを置く時です。そして、それぞれのスレッドが、そのメッセージが処理された後、メッセージを削除する責任があるとします。そのようなアルゴリズムの実装は、読者への課題とします。また、似たような所有パターンを持つ他のアルゴリズムを探すのも課題とします。 8.5 そのような機構にはとても多くがあります。例えば: 1 システム V メッセージキュー 2 共用メモリデキュー(6.1.2節を参照) 3 共用メモリメールボックス 4 UNIX ドメインソケット 5 TCP/IP や UDP。そして、 RPC, HTTP, XML, SOAP などなど多数の高レベルのプロトコルによる補強。 完全なリストを作るのは、十分に視野の狭い読者への課題とします。なお、とても長いリストになるだろうと警告します。 8.6

鍵となるフレーズは、「データへの権利を持つ」です。この場合、問題となる権利は、図の1行目で定義されるスレッドごとの counter 変数をアクセスする権利です。この状況は、8.2節で述べたものに似ています。

しかし、eventual() スレッドが所有しているデータも実際にあります。つまり、図の17と18行目で定義される、t と sum 変数です。

専用スレッドの他の例としては、Linuxカーネルのカーネルスレッドを見て下さい。例えば、kthread_create() と kthread_run() で作られるものです。

8.7

はい。一つのアプローチは、read_count() が、自分自身のスレッドごと変数の値を加える事です。これは、完全な所有と性能を維持しますが、正確さはわずかしか改善しません。特に、とても多数のスレッドのあるシステムでは。

他のアプローチは、read_count() が、関数シッピングを使うことです。例えば、スレッドごとのシグナルという形で。これは、大いに正確さを改善しますが、read_count() に多大な性能コストを払わせます。

しかし、これらのどちらの方法も、カウンタを更新するという一般的な場合での、キャッシュラインの往復を除くという利点があります。

F.9 遅延処理

9.1

これは、最後の参照を放すことと新しい参照を得ることの間の競合を解決することができますが、データ構造が解放されて再確保、時には全く異なるタイプの構造体として、されるのを防ぐためには、全く何もしません。「単純なコンペアアンドスワップ操作」を、異なるタイプの構造体に適用したら、未定義の結果になるのは十分あり得ることです。

要するに、コンペアアンドスワップのようなアトミック操作を使うには、型の安全あるいは存在の保証が絶対に必要です。

9.2

CPUは、参照を得るためには、すでにその参照を持っていなければいけないというのが規則でした。なので、あるCPUが最後の参照を放したら、新しい参照を得ることのできるCPUはいないはずです。図9.2の22行目でアトミックでないチェックをしてもいいのは、このためです。

9.3

これらの関数が正しく使われるなら、そういうことは起きないはずです。既に参照を持っていないのに、 kref_get() を呼ぶのは間違いです。その場合、kref_sub() がカウンターをゼロに減算するはずはありません。

9.4

コール元は、少なくとも1つの参照があり続けることがわかっている場合以外は、オブジェクトが継続的に存在することを期待できません。通常、コール元はこれを知る手段がないので、kref_sub() を呼んだ後は、そのオブジェクトを参照しないように注意しないといけません。

9.5

「if」条件が終わって、参照カウンター値が1だったとします。解放操作が走って、参照カウンターをゼロにし、クリーンアップ操作を始めます。しかし、「then」節がそのカウンターを1に戻します。こうして、オブジェクトがクリーンアップの後に使われることになります。

9.6

hp_record() は、同時実行する変更をチェックする必要があるからです。そのために、要素へのポインターのポインターが必要です。要素へのポインターの変化をチェックできるためです。

9.7

ある意味、非効率かもしれませんが、そのような再開始は正確さのために絶対必要だというのが真実です。これを見るために、ハザードポインタで保護された連結リストに、要素、A,B そして C があり、以下のイベントが起きるとします。

1 スレッド0が要素Bへのハザードポインタを格納する。(たぶん、要素Aから要素Bにトラバースしてきたのでしょう。)

2 スレッド1がリストから要素Bを削除し、要素Bから要素Cへのポインターを、特別な HAZPTR_POISON 値にして、削除がわかるようにする。スレッド0は要素Bへのハザードポインタを持っているので、まだ解放されることはできない。

3 スレッド1が要素Cをリストから削除する。要素Cを参照するハザードポインタはないので、直ちに解放される。

4 スレッド0が、現在は削除されている要素Bの次の要素のハザードポインタを得ようとする。しかし、HAZPTR_POISON 値を見て、ゼロを返す。コール元は、リストの最初からトラバーサルをやり直さないといけない。

それはとても良いことです。そうしないと、スレッド0は現在は解放されている要素Cをアクセスしようとし、とんでもないメモリー破壊になるところでした。特に、要素Cのメモリーがそれ以降、他の用途のために再確保されていた場合は大変です。

9.8

論文のハザードポインタ実装は、挿入と削除にノンブロッキング同期技術を使っています。この技術は、データ構造をトラバースするリーダーが、更新者が更新を完了するのを「助ける」必要があります。つまり、リーダーは削除された要素の次を見る必要が有ることを意味します。

一方、私達は更新を同期するのにロックを使います。リーダーが、更新者が更新を完了するのを助ける必要はありません。このため、ポインターのボトムビットを使う必要はありません。このアプローチは、リード側のコードが単純で高速になります。

9.9

これらの制限は、参照確保が失敗する可能性のある、参照カウンティング機構にだけ必要です。

9.10

そういう見方もできます。しかし、問題のアトミック変数に他のCPUがアクセスしない場合、実際にアトミック命令を使うオーバーヘッドは無駄です。他のCPUがアクセスしない例を2つ上げると、初期化とクリーンアップです。

9.11

たしかにしません。しかし、ハザードポインタ自身への書き込みはしますし、もっと大切なのは、それぞれが失敗する可能性のある hp_store() すべてについて失敗のケースが処理されないといけないことです。なので、ハザードポインタはとても便利ですが、改善された機構を調べるのは意味のあることでしょう。

9.12

シーケンスロック機構は、実際には、2つの別々の同期機構、シーケンスカウントとロック、の結合です。実際、シーケンスカウント機構は、 Linux カーネルでは、独立した write_seqcount_begin() と write_seqcount_end() プリミティブで使用可能です。

しかし、結合した、write_seqlock() と write_sequnlock() プリミティブの方が、 Linux カーネルでは、ずっとより頻繁に使われます。もっと大切なのは、あなたが「シーケンスカウント」と言うより「シーケンスロック」と言う方が、より多くの人があなたが意味するものを理解できるということです。

なので、この節は、人々が題だけからなんのことかわかるように、「シーケンスロック」と題がついています。「遅延処理」の章にあるのは、(1)「シーケンスロック」の「シーケンスカウント」面を強調するためと、(2)「シーケンスロック」はただのロック以上のものだからです。

9.13

それを実現する1つの自明な方法は、リードオンリーアクセスを含むすべてのアクセスを、write_seqlock() と write_sequnlock() で囲むことです。もちろん、この方法はすべてのリード側並列性を禁止しますし、それより、単純なロックを使って同じように簡単に実装できます。

あなたが、read_seqbegin() と read_seqretry() を使ってリード側アクセスを保護する解決策を思いついたなら、以下のイベントシーケンスを正しく処理できるか確認して下さい。

1 CPU0が連結リストをトラバースしていて、リスト要素Aへのポインターを、拾う。

2 CPU1がリストから要素Aを削除して、解放する。

3 CPU2が無関係なデータ構造を確保して、かつて要素Aが占めていたメモリを得る。この無関係なデータ構造で、かつて要素Aの ->next ポインターとして使われていたメモリーは、浮動小数点数が入っている。

4 CPU0は、かつての要素Aの ->next ポインターを拾い、ランダムビットを得て、セグメントフォールトする。

この種の問題を防ぐ1つの方法は、「型安全なメモリー」を使うことです。これは、9.3.3.6節で説明します。しかしその場合、あなたはシーケンスロックに加えて、他の同期機構も使っていることになります!

9.14

それも正しい実装でしょう。しかし、チェックを read_seqretry() まで動かしても、得るものはないでしょう。命令数はほぼ同じでしょう。さらに、リーダーが呪われたリード側クリティカルセクションから行うアクセスはライターのキャッシュミスという形でオーバーヘッドを起こすことがあります。このキャッシュミスは、図9.9の19行目にあるように、チェックを read_seqbegin() の中に置くことで避けることができます。 9.15 もしそれがないと、コンパイラもCPUも、read_seqretry() 呼び出しの前にあったクリティカルセクションを、この関数の後に動かすことができます。すると、シーケンスロックはクリティカルセクションを守ることができません。smp_mb() プリミティブがそのような再配置を防ぎます。 9.16 古い Linux カーネルでは、ノーです。 とても新しいバージョンの Linux カーネルでは17行目を、ACCESS_ONCE() でなく、smp_load_acquire() にすることができます。すると、18行目の smp_mb() を落とすことができます。同じように、44行目は smp_store_release() を使うことができます。例えば、以下のとおり。 smp_store_release(&slp->seq, ACCESS_ONCE(slp->seq) + 1); これによって、43行目の smp_mb() を落とすことができます。 9.17 なにもありません。これが、シーケンスロックの1つの弱点です。この結果、シーケンスロックはリードがほとんどの状況でしか使うべきではありません。もちろん、あなたの状況で、リード側の飢餓が許容できるならば、シーケンスロックの更新をじゃんじゃんやるがよいでしょう! 9.18 今の場合、 ->lock フィールドは除くことができます。Linux カーネルでは、seqcount_t にありますから。 9.19 全然ダメです。Linux カーネルはいくつかの特別な属性を持って、以下のイベントのシーケンスを無視できるようにしています。 1 スレッド0が read_seqbegin() を実行して、17行目で up ->seq を拾う。値は偶数なので、コール元に返す。 2 スレッド0がリード側クリティカルセクションを始める。しかし、長い間、プリエンプトされる。 3 他のスレッドが、繰り返し write_seqlock() と write_sequnlock() を呼んで、up ->seq がオーバーフローしてスレッド0がフェッチした値になる。 4 スレッド0は実行を続行し、リード側クリティカルセクションを終わるが、データは矛盾している。 5 スレッド0は read_seqretry() を呼び、シーケンスロックで守られたデータは一貫していると誤って結論付ける。 Linux カーネルはシーケンスロックを、まれにしか更新されないものに使います。現在時刻はその例です。この情報はせいぜい1ミリ秒に1回、更新されるだけなので、カウンターがオーバーフローするには7週間必要です。もしカーネルスレッドが7週間の間プリエンプトされたなら、Linux カーネルのソフトロックアップ検出コードがその間ずっと、2分に1回、警告を出し続けるはずです。 これに比べて、64ビットカウンターでは、ナノ秒ごとに更新をしても、オーバーフローには5世紀以上必要です。なので、この実装は64ビットシステムでは ->seq の型に64ビットを使います。 9.20 はいでもいいえでもあります。seqlock リーダーはライターと同時実行できますが、それが起きた時には、read_seqretry() プリミティブはリーダーにリトライを強制します。これはつまり、seqlock ライターと同時実行する seqlock リーダーの行った仕事は、すべて、破棄され、やり直さないといけないということです。なので、seqlock リーダーは、ライターと同時に走ることはできますが、実際にはなんの仕事も行えません。 これとは違って、RCUリーダーは同時実行するRCU更新者がいても、役に立つ仕事をすることができます。 9.21 Linux が動くシステムではみんな、ポインタへのロードとストアはアトミックです。つまり、もしポインタへのストアがその同じポインタからのロードと同時に起きた場合、ロードは、最初の値か、ストアされた値のいずれかを返し、決してその2つをビットごとの混ぜあわせたものを返すことはありません。 さらに、list_for_each_entry_rcu() は常にリストを前に進み、後ろを見ることはありません。なので、list_for_each_entry_rcu() は list_add_rcu() が加えた要素を見るか見ないかのどちらかであり、いずれにしても有効で、正しい形式のリストを見ます。 9.22 hlist では、終端チェックはヘッドに当たるのでなくて、NULL で行う必要があるからです。(単一ポインタの hlist_for_each_entry_rcu() をコーディングしてみるとよいです。ちゃんとできたら、すごいです!) 9.23 それをやる1つの方法を図 F.5 に示します。 複数の同時実行する削除が synchronize_rcu() で待つことがあるのに注意してください。

9.24 それは同期の設計に依存します。更新を守るセマフォーが、グレースピリオドの間、持たれたままなら、最大でも2バージョンしかありません。新しいのと古いのです。 しかし、検索と更新と list_replace_rcu() だけがロックで守られ、synchronize_rcu() がロックの外にあるとします。図 F.5 に示すコードを参照。さらに、多数のスレッドがRCU 置換をほぼ同時に行い、リーダーはデータ構造をコンスタントにトラバースしているとします。 すると、以下のイベントのシーケンスが起きます。図9.22の最後の状態から始まります。 1 スレッドAがリストをトラバースして、5,2,3 要素への参照を得る。 2 スレッドBが 5,2,3 要素を新しい 5,2,4 要素と置換する。そして、synchronize_rcu() 呼び出しが戻るのを待つ。 3 スレッドCがリストをトラバースして、 5,2,4 要素への参照を得る。 4 スレッドDが、5,2,4 要素を新しい 5,2,5 要素と置換する。そして、synchronize_rcu() 呼び出しが戻るのを待つ。 5 スレッドEがリストをトラバースして、5,2,5 要素への参照を得る。 6 スレッドFが5,2,5 要素を新しい 5,2,6 要素と置換する。そして、synchronize_rcu() 呼び出しが戻るのを待つ。 7 スレッドGがリストをトラバースして、5,2,6 要素への参照を得る。 8 前記2ステップが素早く繰り返され、全部が、いずれの synchronize_rcu() 呼び出しが戻るより前に起きる。 このように、任意の数のバージョンがアクティブであることができます。メモリーと、グレースピリオドの間に、どれだけの更新が完了できるかにだけ制限されます。しかし、こんなに頻繁に更新されるデータ構造は、RCU には向いてないかもしれないことに注意してください。結論は、RCU は、必要とあれば、高い更新率も処理できるということです。 9.25 あるRCU更新者が行う変更は、関連するCPUの、そのデータを含むキャッシュラインを無効にし、同時実行するRCUリーダーに高価なキャッシュミスを強制します。(同時実行するリーダーに高価なキャッシュミスを起こさないで、データ構造を変更できるアルゴリズムを設計できますか?以降のすべてのリーダーに対しても?)

9.26 まず、この測定をしたときの内部のループは、以下です。 つぎに、rcu_read_lock() と rcu_read_unlock() の実質的定義は以下です。 さらに、コンパイラが簡単な最適化をして、ループは、以下になります。 なので、100フェムト秒の「測定」は、時間測定の固定オーバーヘッドをrcu_read_lock() と rcu_read_unlock() 呼び出しを含む内部ループの回数で割ったものとなります。なので、実はこの測定は、誤りです。任意のオーダーだけ誤っています。上記 rcu_read_lock() と rcu_read_unlock() の定義でわかるように、実際のオーバーヘッドは正確にゼロです。 100フェムト秒の時間計則が見積もり過ぎだったというのは珍しいことです! 9.27 元となる rwlock_t に対する競合が、クリティカルセクションのオーバーヘッドが長くなるに連れて少なくなるためです。しかし、このリードライトロックのオーバーヘッドは、単一CPUの時の値まで減ることはありません。キャッシュスラッシングのオーバーヘッドがあるためです。 9.28 RCUリード側クリティカルプリミティブを含むデッドロックサイクルを起こす1つの方法は、以下の(不正な)文のシーケンスです。 synchronize_srcu() は、すべての既存のSRCUリード側クリティカルセクションが完了するまで戻りません。しかし、それは、synchronize_srcu() が戻るまで完了できない SRCUリード側クリティカルセクションの中にあります。この結果、古典的な自己デッドロックになります。リーダーライターロックで、リードロックを持っている時にそれをライトロックしようとするのと同じです。 この自己デッドロックシナリオは、クラシックRCUには当てはまりません。なぜならば、synchronize_rcu() によって起こるコンテキストスイッチがこのCPUに対して静止状態として働くため、グレースピリオドが完了するからです。しかし、これはむしろよりひどいことであり、RCUリード側クリティカルセクションで使っているデータがグレースピリオド満了の結果、解放されることがあるからです。 つまり、RCUリード側クリティカルセクションの中から、同期のRCU更新側プリミティブを呼んではいけません。 9.29 本当にうまくいきます。実際、そうでなければ、 Linux カーネルは動きません。 9.30 これは、おもちゃの例の法則の結果です。ある時点を超えるとコード断片は同じに見えます。私達がコードをどう考えるかだけが違います。しかし、この差が、とても重要なときがあります。1つの例をあげると、RCUを制限された参照カウンティングスキーマであると思うならば、更新がRCUリード側クリティカルセクションを排除することがあると誤って思い込むことはないでしょう。 それにしても、例えばあなたがリーダーライターロックをRCUで置換しようとしているならば、RCUをリーダーライターロックの代わりとして考えるのはとても役に立ちます。 9.31 たぶん、NUMA効果です。しかし、エラーバーでわかるように、 refcnt 線の測定値にはかなりの偏差があります。実際、ある場合には標準偏差が測定値の10%以上になることもありました。なので、オーバーヘッドのへこみは、統計的異常なのかもしれません。 9.32 図7.17と同じように、これはとても単純なハッシュテーブルで、チェーンを持ちません。なので、あるバケツには先頭要素しかありません。読者は、この例を、完全なチェーンを持つハッシュテーブルに拡張してみるよう、再度、おすすめします。 9.33 まず、14行目の2回めのチェックは必要なことに注意ください。私達がロックを取るのを待つ間に他のCPUがこの要素を削除しているかもしれません。しかし、ロックを取るときに、私達がRCUリード側クリティカルセクションにいるという事実は、この要素が再確保されてこのハッシュテーブルに再挿入されることがありえないことを保証します。さらに、ロックを取った以降は、ロック自身が、要素の存在を保証します。なので、RCUリード側クリティカルセクションにいる必要はなくなります。 要素のキーをもう一度チェックする必要があるかという問題は、読者の宿題とします。 9.34 この2行の順序を入れ替えるとします。するとこのコードは以下のイベントのシーケンスの場合こわれます。 1 CPU0が delete() を呼び、15行目を実行して削除する要素を見つけます。要素を実際には削除しておらず、これからするところです。 2 CPU1が同時に delete() を呼び、この同じ要素を削除しようとします。しかし、CPU0がロックを持っているので、CPU1は13行目で待ちます。 3 CPU0が16と17行目を実行して18行目で、CPU1がそのRCUリード側クリティカルセクションを抜けるのを待ちます。 4 CPU1がロックを取りますが、CPU0が既に要素を削除しているので14行目のテストが失敗します。CPU1は22行目を実行します。(クイッククイズのために、23行目と入れ替えてあります。)そして、RCUリード側クリティカルセクションを抜けます。 5 CPU0は synchronize_rcu() から抜けることができます。そして、19行目を実行し、要素をフリーリストに移します。 6 CPU1は要素のロックを放そうとしますが、要素は解放されています。さらに悪いことに、何か他のデータ構造型として再確保されているかもしれません。これは致命的なメモリ破壊エラーです。 9.35 少なくても1つのスレッドがRCUリード側クリティカルセクションにいる期間が、任意に長くなる可能性のあるということは確かです。しかし、9.3.3.6節の説明のキーワードは、「使用中」と「既存の」です。あるRCUリード側クリティカルセクションは、概念的には、そのクリティカルセクションが開始した時に使用中であったデータ要素だけに対して参照を得ることができることに注意下さい。さらに、スラブは、そのすべてのデータ要素が解放された時に限りシステムに返されることを思い出して下さい。実際、それらすべてが解放されるまで、RCUグレースピリオドは始まりません。 このため、スラブキャッシュは、スラブの最後の要素が解放される前に始まったRCUリード側クリティカルセクションだけを待てばよいのです。これはつまり、最後の要素が解放された後に開始したRCUグレースピリオドであれば、どれでも良いということです。そのグレースピリオドが終わったら、スラブは、システムに返されることができます。 9.36 1つのアプローチは、nmi_profile() で、rcu_read_lock() と rcu_read_unlock() を使うことです。そして、synchronize_sched() を、synchronize_rcu() に代えます。図F.6で示したとおりです。 9.37 感嘆符の着いたAPIメンバー(rcu_read_lock(), rcu_read_unlock(),そして call_rcu())だけが、Paul E. McKenney が、1990年代中頃に知っていた Linux RCU API です。その頃は、彼は、RCU のことは全部わかったという誤った思いを抱いていました。 9.38 RCUリード側クリティカルセクションが synchronize_rcu() 呼び出しを無限にブロックするのを防ぐために、何かをする必要はありません。なぜならば、synchronize_rcu() 呼び出しは既存のRCUリード側クリティカルセクションだけを待つ必要があるからです。なので、それぞれのRCUリード側クリティカルセクションが有限の長さならば、何も問題はありません。 9.39 全然違います!特に、プリエンプティブRCUを使っている時は違います!その時は、synchronize_irq() を使う必要があります。あるいは、あなたが、synchronize_rcu() に待ってほしい割り込みハンドラの中に、rcu_read_lock() と rcu_read_unlock() 呼び出しを入れてもよろしい。 9.40 call_rcu_bh() が呼ばれた時に、rcu_read_lock_bh() と rcu_read_unlock_bh() で区切られるRCUリード側クリティカルセクションがない場合、RCUはコールバックを直ちに呼び出す権利を持ちます。それはたぶん、RCUリード側クリティカルセクションでまだ使っているデータ構造を解放することになるでしょう!これは、理論的な可能性というだけではありません。rcu_read_lock() と rcu_read_unlock() で区切られる長く走るRCUリード側クリティカルセクションは、この失敗モードになる可能性があります。 しかし、rcu_dereference() ファミリーの関数は、RCUのすべてのフレーバーで使えます。(かつては、rcu_dereference() のフレーバーごとの変種を作ろうとしたことがありますが、単純に面倒過ぎました。)

9.41

絶対にいいえ!そして特に、プリエンプト可能RCUを使っている時にはそうです!もしあなたが割り込みハンドラ内で “rcu_bh” で守られるデータ構造をアクセスする必要があるならば、rcu_read_lock_bh() と rcu_read_unlock_bh() の明示的な呼び出しが必要です。

9.42 non-PREEMPT あるいは PREEMPTカーネルでは、この二つを混ぜると、「事故によって」うまくいきます。なぜならば、これらのカーネルビルドでは、RCUクラシックとRCU Sched は同じ実装にマップするからです。しかし、-rt パッチセットを使った PREEMPT_RT ビルドでは、この混合は致命的です。それは、リアルタイムRCUのリード側クリティカルセクションはプリエンプトされることがあるという事実のためです。その結果、RCUリード側クリティカルセクションがその rcu_read_unlock() 呼び出しに達する前に、synchronize_sched() が戻ることが許されます。この結果、そのリード側クリティカルセクションがあるデータ構造を使い終わる前にそれが解放されることにつながります。それはあなたのカーネルが経験する統計的危険を大きく増すことになります。 実は、RCUクラシックとRCU Sched を分けたのは、プリエンプト可能なRCUリード側クリティカルセクションの必要性に触発されたからです。

9.43 それは正しいです!-rt Linux はスレッド化した割り込みハンドラを使うため、割り込みハンドラのまんなかでコンテキストスイッチがあるかもしれません。synchronize_sched() はそれぞれのCPUがコンテキストスイッチを通過するまで待つだけなので、それはある割り込みハンドラが完了する前に戻ることがあります。 もしあなたが、ある割り込みハンドラが完了するのを待つ必要があるならば、その代わりに synchronize_irq() を使うか、あるいはあなたが待ちたい割り込みハンドラの中に、明示的なRCUリード側クリティカルセクションを置くべきです。

9.44 非同期インタフェースがあると、一つのタスクが、任意の多数のSRCUあるいはQRCUコールバックを登録できます。それは、任意の多量のメモリを消費します。それに対して、現在の同期の synchronize_srcu() と synchronize_qrcu() インタフェースでは、あるタスクは次のグレースピリオドを待ち始めることができる前に、その前のグレースピリオドの待ちを終わらなくてはいけません。

9.45 原理的には、あなたは、ある srcu_struct を使っているRCUリード側クリティカルセクションの中で、何か他の srcu_struct によって synchronize_srcu() を使うことができます。しかし現実的には、そうするのはほぼ確実に、悪い考えです。特に、図F.7に示すコードはそれでもデッドロックになります。 9.46 next ポインタをポイズンするのは、このポインタを使わなくてはいけない同時実行するRCUリーダーに干渉します。しかし、RCUリーダーはprev ポインタを使うことを禁止されているので、安全にそれをポイズンできます。

9.47 そのような例外の一つは、複数要素の連結データ構造が一つの単位として他のCPUからアクセスできない状況で初期化され、そしてその後、このデータ構造へのグローバルポインタを植え付けるために、rcu_assign_pointer() が一度使われる場合です。初期化時のポインタ代入は rcu_assign_pointer() を使う必要はありません。しかし、その構造体がグローバルに見えるようになった後に起きる全てのそのような代入は rcu_assign_pointer() を使わなくてはいけません。 しかし、この初期化コードが極めてホットなコードパスにあるのでない限り、いずれにしても rcu_assign_pointer() を使うのが賢明かもしれません。理論的には不必要としてもです。「ちっぽけな」変更が、あなたが大切に守ってきた、初期化がプライベートに起きるという前提を無効にするのはあまりにも簡単なことです。

9.48 “sparse”のような自動コードチェッカー(あるいは、実に、人類)にとって、あるRCUトラバーサルプリミティブがどんなタイプのRCUリード側クリティカルセクションに対応するかを判断するのが難しいことがあります。例えば、図F.8に示すコードを考えましょう。 このrcu_dereference() プリミティブは、RCU クラシックのクリティカルセクションにあるのでしょうか、RCU Sched でしょうか?これを見つけるにはどうしたらいいでしょう?

9.49 図 F.9 の foo() と bar() が異なるCPUで同時に実行されたとします。foo() は、3行目で my_lock をとり、 bar() は13行目で rcu_gp_lock をとります。foo() が4行目に行って、 bar() が持つ rcu_gp_lock をとろうとします。そして、bar() が14行目で、foo() が持つ my_lock をとろうとします。 それぞれの関数は、他方が持つロックを待ちます。古典的なデッドロックです。 他のRCU実装は、rcu_read_lock() でスピンもブロックもしませんから、デッドロックは起きません。 9.50 実際、このようにリーダーライターロックを使うこともできます。しかし、教科書のリーダーライターロックは、メモリー競合が激しいので、実際に並列実行を可能とするには、RCUリード側クリティカルセクションはとても長くないといけません。 一方、rcu_read_lock() で、リード用にリーダーライターロックを使うのは、前記のデッドロック条件を避ける事ができます。 9.51 この変更は、デッドロックを再度、導入します。なので、いいえ、クリーンではありません。 9.52 1つのデッドロックは、ロックが synchronize_rcu() の間保持されていて、その同じロックがRCUリード側クリティカルセクションで取られるケースです。しかし、この状況は、どんな正しいRCU実装でもデッドロックします。結局、synchronize_rcu() プリミティブはすべての既存のRCUリード側クリティカルセクションが終わるのを待たないといけませんが、これらクリティカルセクションのどれかが synchronize_rcu() を実行しているスレッドが持つロックに対してスピンしているならば、これはRCUの定義に内在するデッドロックと言えます。 もうひとつのデッドロックは、RCUリード側クリティカルセクションをネストしようとすると起きます。このデッドロックは、この実装固有のもので、再帰的なロックを使ったり、リーダーライターロックを使って rcu_read_lock() でリード確保、synchronize_rcu() でライト確保するなどをすることによって避ける事ができます。 しかし、以上の2つのケースを除くと、このRCU実装は、これ以上のデッドロック状況を持ちません。これは、他のスレッドのロックを取ろうとするのが、synchronize_rcu() を実行するときだけであり、その場合も、ロックは直ちに解放されるので、最初のケースで述べた synchronize_rcu() の間ずっとロックが保持されるケースを除いては、デッドロックサイクルの出現を許さないためです。 9.53 これはたしかに、利点です。でも、rcu_dereference() と rcu_assign_pointer() も必要なことを忘れないように。つまり、rcu_dereference() では volatile 操作、rcu_assign_pointer() では、メモリーバリアです。もちろん、アルファCPUの多くは両方のプリミティブでメモリーバリアを必要とします。 9.54 そのとおりです。これは、すべての正しいRCU実装でデッドロックします。しかし、rcu_read_lock() は、本当にデッドロックサイクルに寄与していますか?そうだと思うなら、9.3.5.9節のRCU実装を見てから、同じ問題を考えてください。 9.55 更新側のテストはリーダーなしに行われたので、 poll() システムコールは呼ばれていません。 訳注。問いと答えが合ってないみたい。 9.56 そうすればたしかに、飢餓を除くことはできますが、それは、rcu_read_lock() がライターを待ってスピンあるいはブロックすることを意味します。一方、ライターはリーダーを待っています。リーダーのどれかが、スピンあるいはブロックしている rcu_read_lock() が持っているロックを取ろうとしたら、またデッドロックになります。 つまり、治療は、病気よりも悪質でした。正しい治療は、9.3.5.4節を参照。

9.57

スピンロック確保は、そのスピンロックのクリティカルセクションがその確保の前に「漏れ出る」ないことを保証するだけです。それはいかなる意味でも、スピンロック確保の前にあるコードがクリティカルセクションの中にリオーダーされないことを保証するものではありません。そのようなリオーダリングは、RCU保護されたリストからの削除が、rcu_idx をひっくり返した後にリオーダーされることを許すかもしれません。すると、新しく始まったRCUリード側クリティカルセクションが最近削除されたデータ要素を見るということが起きるかもしれません。 読者への課題: Promela/spin のようなツールを使って、図9.41のメモリバリアの内どれが(もしあるとして)本当に必要かを決定しなさい。それらのツールを使うための情報は、12章を参照下さい。正しくて完全な最初の回答は、クレジットされます。

9.58 両方のフリップが、絶対的に必要です。これを見るために、以下のイベントのシーケンスを考えましょう。 1 図9.40の rcu_read_lock() の8行目が、rcu_idx をピックアップし、その値がゼロであることを見つけます。 2 図9.41の synchronize_rcu() の8行目が、rcu_idx の値をひっくり返してその値を1に設定します。 3 synchronize_rcu() の10から13行目は rcu_refcnt[0] の値がゼロであることを見つけます。そのため、戻ります。(質問は、14から20行目が無かったら何が起きるかを聞いているのを思い出して下さい。)

4 rcu_read_lock() の9から10行目は値ゼロをこのスレッドの rcu_read_idx のインスタンスに格納し、rcu_refcnt[0] を加算します。次に実行は、RCUリード側クリティカルセクションの中に進みます。

5 synchronize_rcu() の別のインスタンスが、再び rcu_idx をひっくり返します。今回は、その値をゼロにします。rcu_refcnt[1] はゼロなので、synchronize_rcu() は直ちに戻ります。(rcu_read_lock() は、rcu_refcnt[0] を加算したのであって、rcu_refcnt[1] ではないことを思い出して下さい!)

6 ステップ5で始まったグレースピリオドは、終わることを許されます。しかし、以前にステップ4で始まったRCUリード側クリティカルセクションは完了していないことは事実です。これはRCUセマンティクスに違反します。そして、RCUリード側クリティカルセクションがまだ参照しているデータ要素を、更新が解放することを許すかもしれません。 読者への課題: 8行目のすぐ後に、rcu_read_lock() がとても長い時間(何時間も!)プリエンプトされたら何が起きますか?その場合この実装は正しく動きますか?なぜもしくはなぜそうでない?正しくて完全な最初の回答は、クレジットされます。

9.59 アトミックでない操作を使うと、加算と減算が失われることがあり、その結果実装を損ないます。rcu_read_lock() と rcu_read_unlock() でアトミックでない操作を使う安全な方法は、9.3.5.5節を参照下さい。

9.60 atomic_read() プリミティブは実際には、アトミック機械命令を実行しません。その代わり、 atomic_t からの通常のロードをします。その唯一の目的は、コンパイラの型チェックを幸せにすることです。もしLinuxカーネルが8ビットCPUで走ったならば、それは “store tearing” を防ぐ必要もあるでしょう。それは、ある8ビットシステムで、16ビットのポインタを、2つの8ビットアクセスにして格納する必要があることから起きることがあります。しかしありがたいことに、誰も、Linuxを8ビットシステムでは走らせないようです。

9.61 以下に注意下さい。 あるスレッドを待たないといけないのは、そのスレッドがまだ既存のRCUリード側クリティカルセクションにいる時だけです。 そして、一つの抵抗し続けるスレッドを待つことにより、他の全てのスレッドは、まだそれが実行しているかもしれない任意の既存のRCUリード側クリティカルセクションを完了する機会を得ます。 このため、2N の期間待たなくてはいけない場合は、私たちがその前のスレッドをすべて待ったのにもかかわらず、最後のスレッドが、まだ既存のRCUリード側クリティカルセクションに残っている時だけです。短く言えば、この実装は、不必要に待つことはありません。 訳注 なるほど。スレッドごとのビットマップを持って、抜けてないものに印をつけて、全体の走査ごとに一度だけ眠る、繰り返し、とやっても、必要時間は結局一番遅い人で決まるわけね。 しかし、もしあなたがRCUを使うコードのストレステストをしているなら、poll() 文をコメントアウトすれば、RCUリード側クリティカルセクションの外側で、RCUで守られたデータ要素への参照を不当に保持するというバグをより良く捕まえることができるかもしれません。 9.62 特別用途のRCUのユニプロセッサ実装はその理想郷を達成できます [McK09c]。

9.63 実は、ゼロ(もしくは他の任意の偶数定数)を設定してもうまくいきます。しかし、rcu_gp_ctr の値を設定することは、貴重なデバッグ補助を提供することができます。それは開発者に、対応するスレッドが最後にRCUリード側クリティカルセクションを抜けたのはいつであるかについてのヒントを与えるからです。

9.64 これらのメモリバリアは必要です。なぜならば、ロックプリミティブはクリティカルセクションを閉じ込めることしか保証しないからです。ロックプリミティブは他のコードがクリティカルセクションの中にしみこんでくるのを防ぐ義務は絶対的に何もありません。なのでこのメモリバリアの対は、コンパイラにせよCPUにせよそれらが実行するこのようなコードの移動を防ぐために必要です。

9.65 確かにそれは、少し直せば可能です。この作業は読者への課題とします。

9.66 それは本当の問題です。失敗につながるイベントのシーケンスがあり、それを対策する可能な方法がいくつかあります。より詳しくは、9.3.5.8節の終わりあたりにあるクイッククイズを参照下さい。その議論をそこに置いた理由は、(1)あなたにそれを考える時間をより多く与えるためと、(2)その節で加えられるネストのサポートが、カウンタがオーバーフローするために必要な時間を大いに減らすからです。

9.67 独立したスレッドごと変数という明白な単純性は、人を惑わすものです。このアプローチは、操作の注意深いオーダリングという点で、ずっと大きな複雑さを引き起こします。特に、シグナルハンドラがRCUリード側クリティカルセクションを含むことを許されている時にはそうです。でも、私の言葉を信じないで下さい。コーディングして、どうなるか自分で見ること!

9.68 一つの方法は、33と34行目の大きさの比較をスレッドごと変数 rcu_reader_gp と rcu_gp_ ctr+RCU_GP_CTR_BOTTOM_BIT が等しいかの判定に代えることです。

9.69 それは実に致命的となることがあります。それを見るために、以下のイベントのシーケンスを考えましょう。 1 スレッド0が rcu_read_lock() に入ります。それはネストしていないと判断して、グローバルな rcu_gp_ctr の値をフェッチします。次にスレッド0は極めて長い時間プリエンプトされます(そのスレッドごと rcu_reader_gp 変数にストアする前に)。

2 他のスレッドが繰り返し synchronize_rcu() を呼びます。その結果、グローバルな rcu_gp_ctr の新しい値は、スレッド0がそれをフェッチした時よりも、RCU_GP_CTR_BOTTOM_BIT 小さくなります。

3 スレッド0が再び動き出します。そしてそれは、スレッドごとの rcu_reader_ gp 変数に格納します。それが格納した値は、グローバルな rcu_gp_ctr よりも、RCU_GP_CTR_BOTTOM_BIT+1 だけ大きいです。

4 スレッド0はRCUで守られたデータ要素Aへの参照を得ます。

5 スレッド1は、スレッド0が参照を得たばかりのデータ要素Aを削除します。

6 スレッド1は synchronize_rcu() を呼びます。それは、グローバルな rcu_ gp_ctr を RCU_GP_CTR_BOTTOM_BIT だけ加算します。次にそれは全てのスレッドごと変数 rcu_reader_gp を調べます。しかし、スレッド0の値は、(誤ったことに)それがスレッド1の synchronize_rcu() 呼び出しの後に始まったことを示します。このためスレッド1はスレッド0がそのRCUリード側クリティカルセクションを完了するのを待ちません。

7 スレッド1は次にスレッド0がまだ参照しているデータ要素Aを解放します。

シナリオは、9.3.5.7節で示した実装においても起きることに注意下さい。

この問題をなおす一つの戦略は、64ビットのカウンタを使うことです。そうすれば、それをオーバーフローさせるのに必要な時間は、計算機システムの有効な寿命を超えるでしょう。32ビットの x86 CPUファミリーの、骨董品でないメンバは、cmpxchg64b 命令によって、64ビットカウンタをアトミックに操作できることに注意下さい。

もう一つの戦略は、同様の効果を得るために、グレースピリオドが起きることのできる頻度を制限することです。例えば、synchronize_rcu() は、前回呼ばれた時刻を記録します。そして以降の全ての呼び出しはこの時刻をチェックして、望ましい間隔を空けるために必要に応じてブロックします。例えば、ネストのためにカウンタの下位4ビットが使われ、グレースピリオドが一秒に最大10回、起きることが許されるならば、カウンタがオーバーフローするには300日以上かかります。しかしこのアプローチは、このシステムがCPUバウンドの高優先度リアルタイムスレッドによってまるまる300日間、完全にビジーとなる可能性が少しでもあるならば役に立ちません。(たぶん、あり得ない可能性でしょう。しかし、あらかじめ考えておくのが最善です。)

三つ目のアプローチは、管理的手法によって問題のシステムでリアルタイムスレッドを禁止することです。この場合、プリエンプトされたプロセスは、時間が経つにつれて優先度が上がります。なので、カウンタがオーバーフローする機会を得るずっと前に走ることができるでしょう。もちろん、このアプローチは、リアルタイムアプリケーションにとっては全く役に立ちません。

最後のアプローチは、rcu_read_lock() が、グローバルな rcu_gp_ctr の値を、スレッドごとの rcu_reader_gp カウンタに格納した後に、再びチェックすることです。そして、グローバルな rcu_gp_ctr の新しい値が変ならばリトライします。これはうまくいきますが、rcu_read_lock() に、非決定論的な実行時間をもたらします。ところで、もしあなたのアプリケーションがカウンタがオーバーフローするほど長くプリエンプトされるならば、どっちにしてもあなたは決定論的な実行時間を望むことはできません!

9.70

確かに、その通りです。このため、このRCU実装を使うアプリケーションは、rcu_quiescent_state をまれに呼ぶべきです。その代わり、ほとんどの場合には rcu_read_lock() と rcu_read_unlock() を使いましょう。

しかし、このメモリバリアは絶対に必要です。それは、他のスレッドが、この呼び出し元が実行する全ての以降のRCUリード側クリティカルセクションの前に、12と13行目のストアを見るためです。

9.71

19行目のメモリバリアは、rcu_thread_offline() の前にあるかもしれない、全てのRCUリード側クリティカルセクションがコンパイラあるいはCPUによって20と21行目の代入の後にリオーダーされるのを防ぎます。22行目のメモリバリアは、厳密に言うと、不必要です。rcu_thread_offline() の呼び出しの後に何らかのRCUリード側クリティカルセクションを持つのは違法だからです。

9.72

測定ループは空の関数の対を含むので、コンパイラはそれを最適化して除きます。この測定ループは、それぞれのrcu_quiescent_state() 呼び出しの間で1000回かかりますから、この測定はほぼ、rcu_quiescent_state() の単一の呼び出しの1000分の1のオーバーヘッドです。

9.73

ライブラリ関数は、その呼び出し元を全く制御できません。なので、呼び出し元が定期的に rcu_quiescent_state() を呼ぶように強制できません。しかし、あるRCUで守られたデータ構造に多くの参照をしたライブラリ関数は、それに入る時に rcu_thread_online を呼び、定期的に rcu_quiescent_state() を呼び、抜ける時に rcu_thread_offline() を呼ぶことができるかもしれません。

9.74

RCUリード側クリティカルセクションは、実効的には、それを取り囲む rcu_read_lock() と rcu_read_unlock() を超えて、rcu_quiescent_state() の一つ前と次の呼び出しにまで拡張されることに注意下さい。この rcu_quiescent_state() は、rcu_read_unlock() と、それにすぐに続く rcu_read_lock() と考えることができます。

そうであっても、実際のデッドロック自身は、RCUリード側クリティカルセクションと、synchronize_rcu() の中でのロック確保に関連し、決して rcu_quiescent_state(). とは関連しないでしょう。

9.75

この状況は、call_rcu() のような非同期のグレースピリオドプリミティブが存在する一つの理由です。このプリミティブはRCUリード側クリティカルセクションの中から呼ぶことができます。そして、グレースピリオドが経過した後、指定したRCUコールバックがその後、呼ばれます。

RCUリード側クリティカルセクションの中にいて、RCU更新を実効できることは、とても便利な時があります。そしてそれは、リーダーライターロックでの、(神秘的な)無条件のリードからライトへのアップグレードに似ています。

9.76

ヒント: グローバル変数 finalcount と、配列 counterp[] を、単一のRCUで守られた構造体に置きます。初期化の時に、この構造体は確保され、全て、ゼロとNULLに設定されます。

inc_count() 関数は、変わらないでしょう。

read_count() 関数は、final_mutex を取る代わりに、rcu_read_lock() を使うでしょう。そして、現在の構造体への参照を得るために rcu_dereference() を使う必要があるでしょう。

count_register_thread() 関数は、新しく作られたスレッドに対応する配列要素が、そのスレッドのスレッドごと counter 変数を指すように設定するでしょう。

count_unregister_thread() は、以下が必要でしょう。新しい構造体を確保し、final_mutex を確保し、古い構造体を新しいものに複写し、去っていくスレッドの counter 変数を合計に加算し、この同じ counter 変数へのポインタを NULL にし、古いものの代わりに新しい構造体をインストールするために、rcu_assign_pointer() を使い、final_mutex を放し、グレースピリオドだけ待ち、最後に古い構造体を解放します。

これは本当にうまくいくでしょうか?なぜ、あるいはなぜそうでない?

詳しくは、344ページの13.2.1節を参照下さい。

9.77

ヒント:リーダーライターロックのリード確保を、RCUリード側クリティカルセクションに置換しなさい。そして、それに合うように、デバイス削除のコード断片を調整しなさい。

この問題への一つの解答は、347ページの13.2.2節を参照下さい。

9.78

ほとんど。“Unconditional Acquisition” カラムで見るように、参照カウンティングもハザードポインタも、無条件の参照確保を提供しません。なので、参照を確保するのは、競合する更新がある時には定数でないオーバーヘッドを持つことがあります。

さらに、複数のデータ要素をカバーするために単一の参照カウントを使うことは、厳しい結果をもたらすことがあります。例えば、あなたは、全てのデータ要素の全ての参照が放されるまで、いかなるデータ要素の削除もできません。これは、データ要素を後始末するコードをより複雑にし、メモリフットプリントもRCUなみに大きくするかもしれません。言葉を代えて言えば、メモリフットプリントの増大は、特にRCUの結果ではなく、バルク参照カウント確保に一般的に見られるということです。

付録の続きは、こちらを参照。

以上