Linux カーネルと FreeBSD カーネルのアトミック変数

はじめに

たまたま、あるプロジェクトで、 Linux カーネルと FreeBSD カーネルの二つに深くかかわる必要にせまられたのですが、 そのとき、アトミック変数を操作する API が両者でかなり異なっていることに気付きました。 調査してみたところ、二つのカーネルの思想的違いが見てとれ、面白いと思ったので報告します。

アトミック操作とは

ある処理について、 その処理の途中の状態が他のプロセスやスレッドからアクセスされることがない場合に、 その処理はアトミックと呼ばれます。例えば、 共有された変数を変更する場合、 それにアクセスする前後でロックの取得と解放をして排他制御を行います。 こうすることで、変更の途中の状態にアクセスされて一貫性が失われることを防ぎます。 この場合、ロックの取得と解放とが、アトミック性を保障することになります。

一般には、アトミック操作としたい処理の前後でロックを取ることで、 複数の処理をアトミックに実行することが出来ます。一方で、 例えば単一の 32 ビット整数のように、 CPU が直接にサポートしている型を操作する場合、 それらにアトミック操作を行うための専用の命令が CPU に用意されている場合があります。 このような命令を使用すれば、ロックを取るよりも圧倒的に低コストでアトミック操作を実現できます。

各カーネルに於いても、このような場面を考慮して、 アトミック操作を行う CPU 命令を呼ぶための API が用意されています。 ここからは、この意味でのアトミック操作 API について見ていきます。

FreeBSD カーネルのアトミック操作 API

最初に FreeBSD でのカーネルアトミック操作 API を見ます。

FreeBSD man page に、 FreeBSD カーネルのアトミック操作 API が載っています。

FreeBSD のアトミック操作 API には、操作の対象となる整数型変数への volatile 修飾されたポインタを渡すことになっています。 volatile 修飾とは、 C 言語の修飾子の一つで、 変数に指定できる型修飾子です。 C コンパイラは、 volatile が指定された変数について、 変数とメモリとの同期が取れなくなるような最適化を抑制しなければなりません。 FreeBSD の アトミック操作 API は、この volatile 修飾されたポインタが指す変数に対して、 アトミック操作のための CPU 命令を呼び出すことで、アトミック操作を行います。

このような API であるために、通常の整数型として宣言した変数に対して、 その時々でアトミック操作を行うことが出来るようになっています。 下の例のように、通常の変数として宣言した後に、 必要に応じてポインタを取ってアトミック変数操作を呼ぶ、という使い方が可能です。

// アトミック操作関数の宣言; <machine/atomic.h> で宣言される。
void atomic_add_int(volatile int *p, int v);


// アトミック変数の宣言; 一般の整数型と同様
int x;

// アトミック変数の非アトミックな初期化
x = 0;

// アトミック変数のインクリメント
atomic_add_int((volatile int *)&x, 1);

// アトミック変数の非アトミックな読み取り
int y = x;

volatile 修飾の性質により、操作の結果はメモリに同期されます。そのため、 操作の結果が他の CPU から見えやすくなるため、伝統的にアトミック変数には volatile を付けるということがされてきました。 FreeBSD の API は、それを踏襲していると言えます。

Linux カーネルのアトミック操作 API

次に Linux でのカーネルアトミック操作 API を見てみましょう。

Linux カーネルの付属ドキュメント に、 Linux カーネルのアトミック操作 API の説明があります。

これを見ると、 atomic_t という アトミック変数型 が存在することが分かります。 Linux のアトミック操作 API は、 全て atomic_t 型変数へのポインタを渡すことになっているのです。 下の例のように、アトミック操作を行う場合には、 あらかじめ対象の変数を atomic_t 型の変数として宣言した上で、 それへのポインタを取ってアトミック操作を行う必要があります。

// アトミック変数の宣言; atomic_t 型
atomic_t x;

// アトミック変数の非アトミックな初期化
atomic_set(&x, 0);

// アトミック変数のインクリメント
atomic_inc(&x);

// アトミック変数の非アトミックな読み取り
int y = atomic_read(&x);

Linux の API では、アトミック変数への非アトミックなアクセスに、 atomic_setatomic_read というマクロを使っています。 これらのマクロは、実体はただの代入なのですが、 atomic_t という型を使っていることを意識させるために、あえてマクロを使っており、 FreeBSD のときのように x=0 とか y=x とかの、 の操作を書く事は禁じられています。

Linux カーネルの volatile 修飾への考え方

FreeBSD カーネルのアトミック操作と Linux カーネルのアトミック操作とで一番異なる点は、 Linux ではアトミック変数専用の atomic_t 型を用意している点です。 さらに、この atomic_t 型の定義には、 volatile 修飾は現れません。 Linux カーネルはアトミック操作のために volatile 修飾を付けることを止めてしまったのです。

Linux カーネルのアトミック変数に volatile 修飾がされていないのは、 volatile 修飾の効果と、アトミック操作の要件とに直接の関係がないためです。 前述の通り、アトミック操作の要件とは、 操作の途中の状態が他の CPU によって見えないことです。 しかし volatile 修飾は、メモリと変数が同期されることを保障するものの、 その変数が読み出し中や変更中の状態になる瞬間が発生することを防ぐことは出来ません。 どちらのカーネルでも、アトミック操作のためには volatile では不十分で、 ロックやアトミック操作のための CPU 命令を呼ぶ必要があるのです。

Linux カーネルには、アトミック変数に volatile 修飾をしないことを説明するための "volatile-considered-harmful.txt" という名前の文書が含まれています。 この中では、以下のようなことが述べられています。

  • カーネルコードで volatile を使うことは、ほとんどの場合で間違っている。
  • 排他制御が必要なデータ構造に、 volatile を付けることは意味がない。 共有されたデータ構造を排他制御することと、 volatile で出来る最適化の抑制とは違うものである。 volatile だけでは排他制御は出来ず、ロックが必要である。 そして、ロックを適切に使っていれば、必要な最適化の抑制も達成される。 この上 volatile を使っても、排他制御された区間の内部で、 さらに無用に最適化を抑制するだけである。
  • メモリマップド I/O へのアクセスについても、 volatile は必要ない。 アーキテクチャによっては、 I/O メモリをポインタで指して直接に使用することは出来ないので、 アクセサ関数を通す必要がある。このアクセサ関数を使うようにすれば、 volatile の出番はない。
  • ビジーウェイトでの待ち合わせ対象の変数に volatile を使う必要もない。 ビジーウェイトの内部では、公平性の確保や電力の削減のため、 cpu_relax() (x86 では、 PAUSE 命令となる) を呼ぶべきであるが、 これにより volatile が不要になる。
  • volatile に意味があるのは、以下の場面だけである
    1. I/O メモリへのアクセサ関数の実装
    2. インラインアセンブラで、コンパイラに見えない形でメモリを変更する場合
    3. jiffes 変数。 これは、歴史的に volatile で実装されてしまっているために残されている。
    4. I/O デバイスによって変更されるメモリを指すポインタ

このように、アトミック性は適切なロックやアトミック命令を使用すれば達成されるものであり、 アトミック性のために volatile 修飾をする必要はありません。

volatile 修飾には、 C コンパイラにメモリとの同期が取れなくなるような最適化を抑制させる効果がありますから、 volatile 修飾を止めれば、最適化の抑制も止まり、性能改善が見込めることになります。

まとめ

アトミック操作について調べましたが、 これだけでも FreeBSD カーネルと Linux カーネルの思想の違いを感じ取れました。 FreeBSD カーネルは良くも悪くも伝統的かつ保守的な実装をしているように見えます。 一方、 Linux カーネルは、 細かい所にも気を遣うことで性能を搾り出してやろうという思想が見えているように思えます。

私見ですが、カーネルのような基盤ソフトウェアでは、 あらゆることをして性能を引き出すべきと思うので、 Linux カーネルのように頑張るのが正しいとは思います。 しかし、 FreeBSD の API と比べると、 Linux カーネルの API には atomic_t の使用のための「お作法」があったりするなど、 どうにも使用方法が面倒な印象があります。 Linux カーネルの開発者は、さぞ多大な努力をしてアトミック操作を実装したのではないでしょうか。

References

Author: YOKOTA Yuki

Date: 2014-03-07

Generated by Org version 7.8.11 with Emacs version 24

© Mathematical Systems Inc. 2014