Sunday, June 9, 2013

カーネルハンドルは参照カウントされていない

Kernel handles are not reference-counted@The Old New Thing の翻訳。

ここにしばらく前の質問がある。

私のコードに、同じハンドルで(DeviceIoControl関数を通して)デバイスと会話したい複数のオブジェクトがある。私がオブジェクトを作るときはいつも、ハンドルの参照カウントを増やすためにDuplicateHandle関数を使う。これにより、各オブジェクトがCloseHandle関数を呼んでも、最後のオブジェクトの時だけ実際にハンドルが閉じられる。ところが実際にコードを走らせてみると、最初のオブジェクトがCloseHandle関数を呼ぶや否や、このハンドルはもはや有効ではなく、誰も使用する事が出来なくなる。これを機能させるために、どのようなフラグをCreateFile関数に渡す必要があるのか?

言い換えると、コードはこのようになっている:

// hは新しいCFredオブジェクトによって共有したいハンドル

CFred *MakeFred(HANDLE h)
{
 //  "参照カウントを増やすためにハンドルを複製する"
 //  このコードは正しくない - 以下の議論を見よ
 //  すべてのエラーチェックは説明目的のために削除した
 HANDLE hDup;
 DuplicateHandle(GetCurrentProcess(), h,
                 GetCurrentProcess(), &hDup,
                 0, FALSE, DUPLICATE_SAME_ACCESS);
 return new CFred(h);
}

カーネルハンドルは参照カウントされていない。CloseHandle関数を呼ぶと、それはハンドルを閉じる。物語はこれで終わりだ。

もとの問題の記述から、CFredオブジェクトはそれが削除されたときハンドルを閉じる事が読み取れる。純粋に議論のために、呼び出し元はこのようになっているとしよう:

CFred *pfred1 = MakeFred(h);
CFred *pfred2 = MakeFred(h);
delete pfred1;
delete pfred2;

このコードを実行したとき、実際には何が起きるのだろうか?

最初にMakeFred関数を呼ぶと、私たちはもともとのハンドルhを受け取り、それを複製するが、私たちはもとのハンドルをCFredコンストラクターに渡し、hDupはリークする。もとの投稿者は、ハンドルの複製は、単にハンドルの想像上の参照カウントをインクリメントするものだ、と想定した。つまりh == hDupであると(加えてこれは、もとの投稿者を、そもそもなぜlpTargetHandleパラメーターがあるんだ、と不思議に思わせただろう)。

pfred1が削除されたとき、それはそのハンドルhを閉じる。これはhを閉じ、それを無効にし、CreateFileや他のハンドルを作成する操作によって再利用される事を可能にする。

pfred2が削除されたとき、それもそのハンドルhを閉じる。これは既に閉じられたハンドルを閉じている。エラーだ。もし私たちが敢えてこのハンドルを使用するpfred2のメソッドを呼んだなら、そのハンドルがもはや有効ではないために、それらの操作は同様に失敗するだろう(あー、もし私たちが幸運ならエラーになるだろう。もし私たちが不幸なら、そのハンドルは再利用されていて、私たちは誰かのハンドルでDeviceIoControlを行う結果になるだろう)。

ところで、pfred1が削除されるとき、それはhを閉じるので、呼び出しコードにおけるhのコピーも良くない。

私たちが本当にここでしたい事は、ハンドルを複製し、その複製物を各オブジェクトに渡す事だ。DuplicateHandle関数は、もとのハンドルと同じオブジェクトを参照する新しいハンドルを作成する。この新しいハンドルは、もとのハンドルへの影響なしに閉じる事が出来る。

// hは新しいCFredオブジェクトによって共有したいハンドル

CFred *MakeFred(HANDLE h)
{
 //  "h"と同じオブジェクトを参照する別のハンドルを作成する
 //  すべてのエラーチェックは説明目的のために削除した
 HANDLE hDup;
 DuplicateHandle(GetCurrentProcess(), h,
                 GetCurrentProcess(), &hDup,
                 0, FALSE, DUPLICATE_SAME_ACCESS);
 return new CFred(hDup);
}

この修正は青で強調された1語だ。私たちはCFredオブジェクトに複製されたハンドルを与える。これにより、オブジェクトは独自の、いつでも閉じたいときに自由に閉じる事が出来るハンドルを得、それは他の誰のハンドルにも影響しない。

あなたはDuplicateHandleを、カーネルオブジェクト用のAddRefのようなものとみなすことができる。あなたがハンドルを複製するたびに、カーネルオブジェクトの参照カウントが1つ増え、あなたは新しい参照(新しいハンドル)を得る。あなたがハンドルを閉じるたびに、カーネルオブジェクトの参照カウントが1つ減る。

要するに、ハンドルは参照カウントされたオブジェクトではない。ハンドルを閉じると、それは無くなる。ハンドルを複製すると、あなたは、もとのハンドルを閉じなければならないという義務に加えて、その複製を閉じなければならないという新しい義務を負う。複製されたハンドルはもとのハンドルと同じオブジェクトを参照し、それが参照カウントされたオブジェクトである(なお、カーネルオブジェクトは、ハンドルではないものからの参照を持つ事が出来る。例えば、実行スレッドはスレッドオブジェクトへの参照を保持する。スレッドはそれが動作し続けている限り自身への参照を保持するので、スレッドに対する最後のハンドルを閉じても、スレッドオブジェクトは破棄されない)。

No comments:

Post a Comment