第三节:使用终结器来释放本地资源

前面我们基本了解了垃圾回收和托管堆得情况了,包含垃圾回收期如何回收对象的内存,幸运的是,大多数类型只要内存就可以正常工作,但是,另外有一些类型除了使用内存,还要使用本地资源。

例如:System.IO.FileStream类型需要打开一个文件(本地资源)并保存文件的句柄。然后,该类型的Read和Write方法使用该句柄来操作文件,类似的,System.Threading.Mutex类型打开一个WINDOWS互斥体内核对象(本地资源)并保存其句柄,并在调用Mutex方法时使用该句柄。终结(finalization)是CLR提供的一种机制,允许对象在垃圾回收之前进行一些得体的清理工作。任何包含了本地资源(例如文件、网络连接、套接字、互斥体和其他类型)的类型都必须支持终结器。简单的说,类型实现了一个命名为Finalize的方法。当垃圾回收期判断一个对象时垃圾时,会调用对象的Finalize方法。可以这么理解:实现了Finalize方法的任何类型实际上是在说,它的所有对象都希望在“被处决前吃上最后一餐”。

C#团队认为,Finalize方法是在编程语言中需要特殊语法的一种方法。因此,在C#中,必须在类名之前加一个~符号来定义Finalize方法,如下代码所示:

  Public      class      SomeType

  {

                ~SomeType()

             {}

}

编译上诉代码,在ILDasm.exe检查得到的程序集,会发现C#编译器实际是在模块的元数据中生成一个名为Finzlize的方法。

查看方法的元数据,会发现在一个try {} finally{}代码块中,finally块中调用了对基类的Base.Finalize()方法。实现Finalize方法时,一般会调用Win 32的 CloseHandle函数,并向其函数传递本地资源的句柄。例如:FileStream类型定义了一个文件句柄字段,它标示了本地资源。FileStream类型还定义了Finalize方法,它在内部调用CloseHandle函数,并向它传递文件句柄字段。这就确保了在托管的FileStream对象被确定为垃圾之后,本地文件句柄会得以关闭。如果包装了本地资源的类型没有定义Finalize方法,本地资源就没法得到关闭,会导致资源泄露,直到进程终止。进程终止时,这些资源才被本地回收。

1.使用CriticalFinalizerObject 类型确保终结

为了简化编程System.Runtime.ConstrainedExecution命名空间中定义了一个CriticalFinalizerObject类

查看一下该类,发现该类没有什么特别之处,但是CLR以一种特殊的方式对待该类以及其派生类。具体的说CLR赋予了这个类以下三个很酷的功能。

1)首先构造任何CriticalFinalizerObject派生类的一个对象时,CLR立即对继承层次中的所有Finalize方法进行JIT编译。在构造对象时编译这些方法,可确保对象被定为垃圾之后,本地资源肯定得以释放。如果不对Finalize方法提前编译,那么也许能确保编译并使用本地资源,但无法保证会肯定释放这些资源。内存紧张时,CLR可能找不到足够的内存来编译Finalize方法,这回阻止Finzlize方法的执行,造成本地资源泄露。另外,如果Finalize方法中的代码引用了另一个程序集中的一个类型,而且CLR在寻找这个程序集时失败,也会造成资源泄露。

2)CLR在调用一个非CriticalFinalizerObject派生类型的Finalize方法之后,才能调用CriticalFinalizerObject派生类型的Finalize方法。这样一来,托管资源类就可以在他们的Finalize方法中成功访问CriticalFinalizerObject派生类的对象。

3)如果一个AppDomain被一个宿主应用程序(例如 Sql server,asp.net)强行中断,CLR会调用CriticalFinalizerObject派生类型的Finalize方法。宿主应用程序不再信任它内部运行的托管堆代码时,也利用这个功能确保本地资源得以释放。

2.SafeHandle类型及派生类型

现在Microsoft意识到最常用的本地资源就是Windows提供的资源。还意识到大多数资源都是用句柄进行操作的,同样的为了简化编程、更安全,System.Runtime.InteropServices命名空间中定义了一个SafeHandle类,其形式如下:

对于SafeHandle类,其关注两点。其一,它派生字CriticalFinalizerObject;这确保会得到CLR的特殊对待。其二,他是一个抽象类,必须有一个类从该类派生,并重写受保护的构造器、抽象方法ReleaseHandle以及抽象属性IsInvalid的get访问器方法。

在Window中,如果句柄的值为0或者-1,那么他们中的大多数都是无效的,命名空间Microsoft.Win32.SafeHandles中包含了一个SafeHandleZeroOrMinusOneIsInvalid的辅助类。

同样的该类也是一个抽象类。必须有另外一个类派生字该类,并重写受保护的构造器和抽象方法ReleaseHandle方法。.NET FrameWork只提供了很少几个从SafeHandleZeroOrMinusOneIsInvalid派生的类,其中包含SafeFileHandle,SafeRegistryHandle,SafeWaitHandle和SafeBuffer。以下是SafeFileHandle类

 

SafeWaitHandle的实现方法与SafeFileHandle的实现方法一样,内部都是调用Win32Native.CloseHandle();方法相同的代码,实现了不同的类,是因为确保类型安全;编译器不希望将一个文件句柄作为实参传给一个等待句柄的方法。SafeRegistryHandle类的ReleaseHandle方法调用Win 32 regCloseKey函数。

.net framework还提供了一些附加的类来包装本地资源:SafeProcessHandle, SafeThreadHandle,SafeTokenHandle,SafeFileMappingHandle,SafeViewOfFileHandle,SafeLibraryHandle以及SafeLocalAllocHandle.

其实,所有这些类库已经和Framework Class Library(FCL)一道发布。但是这些类没有对外公开,他们全部在MSCorLib.dll和System.dll内部使用,微软之所以没有公开这些类是因为他们不想完整的测试他们,也不想花时间编写他们的文档。如果希望使用这些类,你可以利用反编译工具,将这些类提取出来,放到自己的代码中。

3.使用SafeHandle类型与非托管代码进行交互

如前所诉:SafeHandle派生类非常有用,因为他们能保证在发生垃圾回收的时,本地资源得以释放。除了讨论过的功能,SafeHandle还提供了另两个功能。首先,要和非托管代码进行交互,在这种情况下使用SafeHandle派生类将获得CLR的特殊对待。

在上面的代码中,CreateEventBad方法的原型是返回一个IntPtr,在.NET FRAMEWORK 2.0之前,SafeHandle类是不存在的,所以不得不使用IntPtr类型来标示句柄。CLR团队发现这种代码不够健壮,在调用CreateEventBad之后,在句柄赋值个handle变量之前,可能抛出一个ThreadAbortExcption。虽然这种情况很少发生,但是一旦发生,将造成资源泄露。为了关闭方法创建的事件,唯一的办法是终止进程。

现在,在.NET FrameWork 2.0和以后的版本中,可以使用SafeHandle来修正这个潜在的资源泄露问题,注意,CreateEventGood方法的原型是返回一个SafeWaitHandle,在调用CreateEventGood方法时,CLR调用Win32 CreateEvent函数。CreateEvent返回到托管代码时,CLR知道SafeWaitHandle是从SafeHandle派生得来的。所以会自动构造一个SafeWaitHandle的一个实例,并在构造时传递从CreateEvent返回的句柄值。新的SafeWaitHandle对象的构造以及句柄的赋值是在非托管代码中进行的。不可能被一个ThreadAbortExcption打断。现在托管代码已经不可能泄露这个本地资源了,最后SafeWaitHandle对象被垃圾回收,它的Finalize方法会被调用,确保资源得以释放。

SafeHandle派生类的最后一个功能是防止有人利用潜在的安全漏洞。现在的一个问题是。一个线程可能试图使用一个本地资源,同时另一个线程正在释放该资源。这可能造成一个句柄循环漏洞。SafeHandle类防止这个安全隐患的方法时使用引用计数。在内部,SafeHandle定义一个私有字段类维护一个计数器。一旦某个Safehandle的派生对象被设为一个有效的句柄,计数器就被设置成1,每次将一个SafeHandle派生类对象作为参数传给一个人非托管方法,CLR就会自动递增计数器。类似的,将非托管方法返回托管代码时,CLR会自动递减计数器。当然对计数器的操作是以线程安全的方式去做的,这是怎么促进安全性的呢?当一个线程试图释放SafeHandle对象包装的本地资源时,CLR实际上知道它不能释放该资源,因为该资源正在由一个非托管代码使用。非托管代码返回后,计数器递减为0,资源才能得以释放。

还要注意System.Runtime.InteropServices;的命名空间中还有一个CriticalHandle类,该类不引用计数器功能,其他方面和SafeHandle类相同,该类以及派生类通过牺牲安全性来换取性能。CriticalHandle类也有自己的派生类,

其中有CriticalHandleZeroOrMinusOneIsInvalid ,CriticalHandleMinusOneIsInvalid ,由于微软倾向于构建更安全而不是更快的系统,所以类库中没有提供这两个派生的类。

原文地址:https://www.cnblogs.com/bingbinggui/p/4385221.html