|
よく知られているように、C/C++はガベージコレクションをネイティブにサポートしていません。言語自体は高い柔軟性を備えていますが、煩雑なメモリ管理は大規模プロジェクトでは非常に負担になる可能性があります。最近のC/C++ライブラリは、メモリ管理の妥協策として、STLの`auto_ptr`、Boostの`Smart_ptr`ライブラリ、Qtの`QPointer`ファミリなど、スマートポインタを提供するのが一般的です。C言語で構築されたGTK+でさえ、参照カウントを使用して同様の機能を実現しています。Linuxカーネルはこの問題をどのように解決しているのでしょうか?これもCベースのソリューションですが、Linuxカーネルは参照カウントを使用しています。C++に詳しい方は、Boostの`shared_ptr`やQtの`QSharedPointer`と比較してみてください。 Linuxカーネルでは、参照カウントは `struct kref` 構造体を用いて実装されています。`kref` の使い方を紹介する前に、シナリオを考えてみましょう。キャラクタデバイスドライバを開発しているとします。デバイスが接続されると、システムは自動的にデバイスノードを作成し、ユーザーはファイル操作を通じてそのデバイスノードにアクセスします。 上図に示すように、左端の緑色のボックスはデバイスの実際の挿入と取り外し、中央の黄色のボックスはカーネル内のデバイスオブジェクトのライフサイクル、右端の青色のボックスはユーザープログラムによるシステムコールの順序を表しています。ユーザープログラムがデバイスにアクセスしているときに突然デバイスが取り外された場合、ドライバー内のデバイスオブジェクトはすぐに解放されるべきでしょうか?すぐに解放してしまうと、ユーザープログラムが実行したシステムコールは確実に不正なメモリアクセスを引き起こしてしまいます。ユーザープログラムがデバイスオブジェクトを閉じるまで待ってから解放したい場合、どのように実装すればよいでしょうか?このような問題を解決するために、`kref` が作成されました。 kref の定義は非常に単純で、その構造には 1 つのアトミック変数のみが含まれます。
Linux カーネルは、kref を使用するための次の 3 つの関数インターフェイスを定義しています。
まず、擬似コードを通じて kref の使い方を理解しましょう。
このコードでは、デバイスオブジェクトを解放する関数として `obj_release` を定義しています。参照カウントが0の場合、この関数は直ちに呼び出され、実際の解放処理を実行します。まず、`device_probe` で参照カウントを1に初期化します。ユーザープログラムが `open` を呼び出すと、参照カウントが1増加します。その後、デバイスが取り外されると、`device_disconnect` によってカウントが1減少します。ただし、この時点では `refcnt` はまだ0ではなく、デバイスオブジェクト `obj` は解放されません。`obj_release` は `close` が呼び出された後にのみ実行されます。 擬似コードを確認した後、実際の例を試してみましょう。スペースを節約するため、この実装ではキャラクタデバイスを作成せず、モジュールのロードとアンロードを通じてkrefプロセスを示すだけです。
#p# kbuildでコンパイルするとkref_test.koが生成されます。その後、以下のコマンドを順番に実行してモジュールをマウントおよびアンマウントします。
この時点で、システム ログに次のメッセージが出力されます。
これはまさに我々が期待していた結果です。 kref 参照カウントを使用すると、カーネル ドライバーが非常に複雑な方法で記述されている場合でも、メモリ管理に自信を持てるようになります。 次のセクションでは、主に kref を使用する際に注意すべき点を紹介します。 Linux カーネルドキュメント kref.txt には、kref を使用するときに従わなければならない 3 つのルールが記載されています。 ルール1:
ルール2:
ルール3:
ルール1は、主に複数の実行パス(例えば、別のスレッドの開始など)が存在する状況に対応します。関数へのポインタの受け渡しなど、単一の実行パス内では、`kref_get` は不要です。次の例を考えてみましょう。
call_something の前後にある kref_get と kref_put のペアは冗長だと思いますか? オブジェクトが制御から逃れていないので、実際には不要です。 しかし、複数の実行パスがある場合は状況は全く異なり、ルール1に従う必要があります。以下はカーネルドキュメントから引用した例です。
スレッド more_data_handling がいつ終了するかわからないため、データを保護するために kref_get を使用する必要があります。 ルール1の「前」という言葉に注目してください。kref_getはポインタを渡す前に実行する必要があります。この例では、kref_getはkthread_runを呼び出す前に実行する必要があります。そうでなければ、どのように保護できるでしょうか? ルール2については詳しく説明する必要はありません。先ほどkref_getを呼び出しているので、当然kref_putも同じように使用するはずです。 ルール3は主に連結リストの場合を扱います。連結リストがあり、リスト内のノードが参照カウントによって保護されているシナリオを考えてみましょう。どのように操作しますか?まず、ノードへのポインタを取得してから、`kref_get` を呼び出してそのノードの参照カウントをインクリメントする必要があります。ルール3によれば、この場合、これら2つのアクションをシリアル化する必要があります。これは通常、ミューテックスを使用して実現できます。次の例をご覧ください。
この例では、既に保護のためにミューテックスを使用しています。ミューテックスを削除するとどうなるでしょうか?マルチスレッド操作を扱っていることを思い出してください。スレッドAが `container_of` を使用してエントリポインタを取得した後、`kref_get` を呼び出す前にスレッドBにプリエンプトされ、スレッドBが `kref_put` 操作を実行中だった場合、スレッドAが実行を再開すると必ずメモリアクセスエラーが発生します。したがって、このような場合にはシリアル化が不可欠です。 データを安全かつ効果的に管理するには、kref を使用する際に次の 3 つのルールを厳守する必要があります。 |