Raymond Chen 2007年08月29日
内核句柄不是引用计数的
简要
内核句柄不使用引用计数管理。复制句柄会增加内核对象的引用计数,每个复制的句柄都需要独立关闭,以减少引用计数。关闭句柄时,它将不再可用。
正文
这里有一个过去浮出水面的问题:
在我的代码中,我有多个对象想要通过
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
!)
与此同时,调用代码的 h
副本也是坏的, 因为 pfred1
在被删除时关闭了它。
我们真正想做的是复制句柄并将复制的句柄传递给每个对象。 DuplicateHandle
函数创建一个新的句柄,该句柄引用与原始句柄相同的对象。 那个新句柄可以关闭,而不影响原始句柄。
// h 是我们想要与新 CFred 对象共享的句柄
CFred *MakeFred(HANDLE h)
{
// 创建另一个引用与 "h" 相同对象的句柄
// 为了说明方便,去掉了所有错误检查
HANDLE hDup;
DuplicateHandle(GetCurrentProcess(), h,
GetCurrentProcess(), &hDup,
0, FALSE, DUPLICATE_SAME_ACCESS);
return new CFred(hDup);
}
修正就是蓝色高亮的那一个词。 我们给 CFred
对象复制的句柄。 这样,它就有自己的句柄,可以随时自由关闭,而且不会影响其他人的句柄。
你可以将 DuplicateHandle
想象成内核对象的 AddRef
。 每次你复制一个句柄,内核对象的引用计数就增加一,你获得了一个新的引用(新句柄)。 每次你关闭一个句柄,内核对象的引用计数就减少一。
总结,句柄不是一个引用计数对象。 当你关闭一个句柄时,它就无效了。 当你复制一个句柄时,除了有义务关闭原来的句柄外,你还有义务关闭复制的句柄。复制的句柄引用与原始句柄相同的对象, 并且是底层对象是引用计数的。 (请注意,内核对象可以有来自非句柄的引用。例如,正在执行的线程会维护对底层线程对象的引用。关闭线程的最后一个句柄不会破坏线程对象,因为只要线程还在运行,它就会保留对自身的引用)