DUICUO

Linuxカーネルの「スマートポインタ」

よく知られているように、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 つのアトミック変数のみが含まれます。

  1. 構造体kref {
  2. atomic_t 参照カウント;
  3. };

Linux カーネルは、kref を使用するための次の 3 つの関数インターフェイスを定義しています。

  1. void kref_init(構造体kref *kref);
  2. void kref_get(構造体kref *kref);
  3. int kref_put(構造体kref *kref、void (*release) (構造体kref *kref));

まず、擬似コードを通じて kref の使い方を理解しましょう。

  1. 構造体my_obj
  2. {
  3. 整数値;
  4. 構造体 kref refcnt;
  5. };
  6.   
  7. 構造体 my_obj *obj;
  8.   
  9. void obj_release(構造体kref *ref)
  10. {
  11. 構造体 my_obj *obj = container_of(ref, 構造体 my_obj, refcnt);
  12. kfree(obj);
  13. }
  14.   
  15. デバイスプローブ()
  16. {
  17. obj = kmalloc(sizeof(*obj), GFP_KERNEL);
  18. kref_init(&obj->refcnt);
  19. }
  20.   
  21. デバイス切断()
  22. {
  23. kref_put(&obj->refcnt, obj_release);
  24. }
  25.   
  26. 。開ける()
  27. {
  28. kref_get(&obj->refcnt);
  29. }
  30.   
  31. 近い()
  32. {
  33. kref_put(&obj->refcnt, obj_release);
  34. }

このコードでは、デバイスオブジェクトを解放する関数として `obj_release` を定義しています。参照カウントが0の場合、この関数は直ちに呼び出され、実際の解放処理を実行します。まず、`device_probe` で参照カウントを1に初期化します。ユーザープログラムが `open` を呼び出すと、参照カウントが1増加します。その後、デバイスが取り外されると、`device_disconnect` によってカウントが1減少します。ただし、この時点では `refcnt` はまだ0ではなく、デバイスオブジェクト `obj` は解放されません。`obj_release` は `close` が呼び出された後にのみ実行されます。

擬似コードを確認した後、実際の例を試してみましょう。スペースを節約するため、この実装ではキャラクタデバイスを作成せず、モジュールのロードとアンロードを通じてkrefプロセスを示すだけです。

  1. #include <linux/kernel.h>
  2. #include <linux/module.h>
  3.   
  4. 構造体my_obj{
  5. 整数値;
  6. 構造体 kref refcnt;
  7. };
  8.   
  9. 構造体 my_obj *obj;
  10.   
  11. void obj_release(構造体kref *ref)
  12. {
  13. 構造体 my_obj *obj = container_of(ref, 構造体 my_obj, refcnt);
  14. printk(KERN_INFO "obj_release\n" );
  15. kfree(obj);
  16. }
  17.   
  18. 静的  int __init kreftest_init(void)
  19. {
  20. printk(KERN_INFO "kreftest_init\n" );
  21. obj = kmalloc(sizeof(*obj), GFP_KERNEL);
  22. kref_init(&obj->refcnt);
  23. 0を返します
  24. }
  25.   
  26. 静的void __exit kreftest_exit(void)
  27. {
  28. printk(KERN_INFO "kreftest_exit\n" );
  29. kref_put(&obj->refcnt, obj_release);
  30. 戻る;
  31. }
  32.   
  33. モジュールの初期化(kreftest_init)。
  34. モジュール終了(kreftest_exit);
  35.   
  36. MODULE_LICENSE( "GPL" );

#p#

kbuildでコンパイルするとkref_test.koが生成されます。その後、以下のコマンドを順番に実行してモジュールをマウントおよびアンマウントします。

sudo insmod ./kref_test.ko

sudo rmmod kref_test

この時点で、システム ログに次のメッセージが出力されます。

kreftest_init

kreftest_exit

obj_release

これはまさに我々が期待していた結果です。

kref 参照カウントを使用すると、カーネル ドライバーが非常に複雑な方法で記述されている場合でも、メモリ管理に自信を持てるようになります。

次のセクションでは、主に kref を使用する際に注意すべき点を紹介します。

Linux カーネルドキュメント kref.txt には、kref を使用するときに従わなければならない 3 つのルールが記載されています。

ルール1:

ポインタの非一時的コピーを作成する場合、特にそれが別の実行スレッドに渡される可能性がある場合は、渡す前に kref_get() を使用して参照カウントを増分する必要があります。

ルール2:

ポインターの使用が完了したら、kref_put() を呼び出す必要があります。

ルール3:

コードが有効なポインタを保持せずに kref された構造体への参照を取得しようとする場合、kref_get() 中に kref_put() が発生できないアクセスをシリアル化する必要があり、構造体は kref_get() 中は有効なままである必要があります。

ルール1は、主に複数の実行パス(例えば、別のスレッドの開始など)が存在する状況に対応します。関数へのポインタの受け渡しなど、単一の実行パス内では、`kref_get` は不要です。次の例を考えてみましょう。

  1. kref_init(&obj->ref);
  2.   
  3. // ここで何かする
  4. // ...
  5.   
  6. kref_get(&obj->ref);
  7. call_something(obj);
  8. kref_put(&obj->ref);
  9.   
  10. // ここで何かする
  11. // ...
  12.   
  13. kref_put(&obj->ref);

call_something の前後にある kref_get と kref_put のペアは冗長だと思いますか? オブジェクトが制御から逃れていないので、実際には不要です。

しかし、複数の実行パスがある場合は状況は全く異なり、ルール1に従う必要があります。以下はカーネルドキュメントから引用した例です。

  1. 構造体my_data
  2. {
  3. 構造体 kref 参照カウント;
  4. };
  5.   
  6. void data_release(構造体kref *ref)
  7. {
  8. 構造体 my_data *data = container_of(ref, 構造体 my_data, refcount);
  9. kfree(データ);
  10. }
  11.   
  12. void more_data_handling(void *cb_data)
  13. {
  14. 構造体 my_data *data = cb_data;
  15. ここでデータ操作します。
  16. kref_put(&data->refcount, data_release);
  17. }
  18.   
  19. int my_data_handler(void)
  20. {
  21. 整数rv = 0;
  22. 構造体 my_data *データ;
  23. 構造体 task_struct *タスク;
  24. データ = kmalloc(sizeof(*data), GFP_KERNEL);
  25. if (!データ)
  26. -ENOMEMを返します
  27. kref_init(&data->refcount);
  28.   
  29. kref_get(&data->refcount);
  30. タスク = kthread_run(more_data_handling, データ, "more_data_handling" );
  31. if (タスク == ERR_PTR(-ENOMEM)) {
  32. rv = -ENOMEM;
  33. 後藤 ;
  34. }
  35.   
  36. ここでデータ操作します。
  37. kref_put(&data->refcount, data_release);
  38. rvを返します
  39. }

スレッド more_data_handling がいつ終了するかわからないため、データを保護するために kref_get を使用する必要があります。

ルール1の「前」という言葉に注目してください。kref_getはポインタを渡す前に実行する必要があります。この例では、kref_getはkthread_runを呼び出す前に実行する必要があります。そうでなければ、どのように保護できるでしょうか?

ルール2については詳しく説明する必要はありません。先ほどkref_getを呼び出しているので、当然kref_putも同じように使用するはずです。

ルール3は主に連結リストの場合を扱います。連結リストがあり、リスト内のノードが参照カウントによって保護されているシナリオを考えてみましょう。どのように操作しますか?まず、ノードへのポインタを取得してから、`kref_get` を呼び出してそのノードの参照カウントをインクリメントする必要があります。ルール3によれば、この場合、これら2つのアクションをシリアル化する必要があります。これは通常、ミューテックスを使用して実現できます。次の例をご覧ください。

  1. 静的DEFINE_MUTEX(ミューテックス);
  2. 静的LIST_HEAD(q);
  3. 構造体my_data
  4. {
  5. 構造体 kref 参照カウント;
  6. 構造体 list_head リンク;
  7. };
  8.   
  9. 静的構造体 my_data *get_entry()
  10. {
  11. 構造体 my_data *entry = NULL ;
  12. mutex_lock(&mutex);
  13. if (!list_empty(&q)) {
  14. entry = container_of( q.next , struct my_q_entry, link);
  15. kref_get(&entry->refcount);
  16. }
  17. mutex_unlock(&mutex);
  18. 帰国エントリー
  19. }
  20.   
  21. 静的void release_entry(構造体kref *ref)
  22. {
  23. 構造体 my_data *entry = container_of(ref, 構造体 my_data, refcount);
  24.   
  25. list_del(&entry->link);
  26. kfree(エントリ);
  27. }
  28.   
  29. 静的void put_entry(構造体my_data *entry)
  30. {
  31. mutex_lock(&mutex);
  32. kref_put(&entry->refcount, release_entry);
  33. mutex_unlock(&mutex);
  34. }

この例では、既に保護のためにミューテックスを使用しています。ミューテックスを削除するとどうなるでしょうか?マルチスレッド操作を扱っていることを思い出してください。スレッドAが `container_of` を使用してエントリポインタを取得した後、`kref_get` を呼び出す前にスレッドBにプリエンプトされ、スレッドBが `kref_put` 操作を実行中だった場合、スレッドAが実行を再開すると必ずメモリアクセスエラーが発生します。したがって、このような場合にはシリアル化が不可欠です。

データを安全かつ効果的に管理するには、kref を使用する際に次の 3 つのルールを厳守する必要があります。