异步设备操作

 
Jeffrey Richter                                                
                                                  
在我的上期专栏中,我演示了如何实现两种基类,即 AsyncResultNoResult 和 AsyncResult<TResult>。这两种类均实现了 IAsyncResult 接口,该接口位于公共语言运行库 (CLR) 异步编程模型 (APM) 的核心。在本期专栏中,我将以允许您异步执行硬件设备操作的方式来使用 AsyncResult<TResult> 类实现 APM。通过阅读本期专栏,您还将深入了解 Microsoft ® .NET Framework 自身是如何执行异步文件和网络 I/O 操作的。所有这些代码都保存在我的 Power Threading 库中,该库位于 wintellect.com

CreateFile 和 DeviceIoControl
在基于 Win32 ® 的编码中,当需要处理一个文件时,首先要通过调用 Win32 CreateFile 函数打开此文件,然后通过调用诸如 WriteFile 和 ReadFile 之类的函数在此文件上执行操作。最终,当完成对文件的处理后,通过调用 CloseHandle 函数来关闭它。
在 .NET Framework 中,在内部构建 FileStream 对象将调用 CreateFile。在内部调用 FileStream 的 Read 和 Write 方法将分别调用 Win32 WriteFile 和 ReadFile 函数。而在内部调用 FileStream 的 Close 或 Dispose 方法则调用 Win32 CloseHandle 函数。当处理文件时,大多数应用程序只需将数据写入文件并从文件读取数据,因此 FileStream 类即可提供您所需的大部分操作。
但是,Windows ® 实际上提供了更多可在文件上执行的操作。对于较常见的操作,Windows 提供了特定的 Win32 函数,如 WriteFile、ReadFile、FlushFileBuffers 和 GetFileSize。但是,对于不经常使用的操作,Win32 并没有提供特定的函数,相反,它只提供一个函数 DeviceIoControl,该函数负责让应用程序直接与负责操作文件的设备驱动程序(如 NTFS 磁盘驱动程序)通信。不经常使用的文件操作的示例包括伺机锁定 ( microsoft.com/msj/0100/win32/win320100.aspx)、操作更改日志 ( microsoft.com/msj/0999/journal/journal.aspx)、压缩磁盘卷/文件、创建连接点、格式化/重新分区磁盘以及整理碎片文件。
此外,任何应用程序均可直接使用 DeviceIoControl 与任何硬件设备驱动程序通信。通过使用 DeviceIoControl,应用程序可查询计算机的电池状态,查询或更改 LCD 的亮度,操作 CD 或 DVD 换片机、查询和弹出 USB 设备等等。

Win32 设备通信
让我们快速回顾一下在 Win32 中,应用程序如何与设备通信,然后封装此功能以便可以从托管代码使用它。
在 Win32 中,打开设备的方式是通过调用 CreateFile 函数。CreateFile 的第一个参数是识别待打开设备的字符串。通常会为此字符串指定路径名称,从而使 CreateFile 打开文件。但是,您可以向 CreateFile 传递特殊字符串,使其打开设备。 图 1 显示了一些潜在的字符串,并说明了这些字符串可使 CreateFile 打开的设备的类型。请注意,这些设备中有些仅限系统或管理员组成员使用,因此除非您的应用程序是使用所要求的运行权限在运行,否则可能无法打开其中的某些设备。

传递至 CreateFile 的字符串说明
@“\\.\PhysicalDrive1”打开硬盘驱动器 1,允许访问其所有扇区。
@“\\.\C:”打开 C: 磁盘卷,允许您操作此卷的更改日志。
@“\\.\Changer0”打开换片机 0,允许您移动碟片并执行其他操作。
@“\\.\Tape0”打开磁带驱动器 0,允许您备份和恢复文件。
@“\\.\COM2”打开通信端口 2,允许您发送和接收字节。
@“\\.\LCD”打开 LCD 设备,允许您查询和调整亮度。
                                        
现在您已经知道如何打开设备,就让我们来看一下 Win32 函数 DeviceIoControl:
                复制代码                      
BOOL DeviceIoControl(
   HANDLE hDevice,           // Handle returned from CreateFile
   DWORD  dwIoControlCode,   // Operation control code
   PVOID  pInBuffer,         // Address of input buffer
   DWORD  nInBufferSize,     // Size, in bytes, of input buffer
   PVOID  pOutBuffer,        // Address of output buffer
   DWORD  nOutBufferSize,    // Size, in bytes, of output buffer
   PDWORD pBytesReturned,    // Gets number of bytes written to 
                             // pOutBuffer
   POVERLAPPED pOverlapped); // For asynchronous operation
                                        
当调用此函数时,句柄参数指向通过调用 CreateFile 获得的文件、目录或设备驱动程序。dwIoControlCode 参数指示要在设备上执行的操作。只需借助此 32 位整数代码就可轻易标识每个操作。如果某操作需要参数,参数将置于数据结构的字段中,并将此结构的地址传递给 pInBuffer 参数。结构大小被传入 nInBufferSize 参数中。同样,如果操作返回一些数据,那么输出数据将置于必须由调用方分配的缓冲区中。此缓冲区的地址被传入 pOutBuffer 参数中。此缓冲区的大小被传入 nOutBufferSize 参数中。当 DeviceIoControl 返回时,它在 pBytesReturned 参数(通过引用传递)内写入实际上写入输出缓冲区的字节数。最后,当执行同步操作时,将 NULL 传递给 pOverlapped 参数。但是,当执行异步操作时,必须传入一个 OVERLAPPED 结构的地址。我将在本期专栏后面详细介绍这一点。
现在您已经大致了解如何通过 Win32 与设备通信,让我们开始为这些 Win32 函数编写一个托管包装,以便您可以用 .NET 语言编写托管代码,从而可直接与硬件设备通信。

托管代码中的同步设备 I/O
首先,我们重点讲解如何执行同步设备操作。随后,我将添加执行异步操作所必需的代码。在 C# 中,我定义了一个静态 DeviceIO 类,它是 Win32 CreateFile 和 DeviceIoControl 函数外部的一个友好包装。 图 2 显示了此类的公共对象模型。
复制代码                      
public static class DeviceIO {
   public static SafeFileHandle OpenDevice(String deviceName, 
      FileAccess access, FileShare share, Boolean useAsync);

   public static void Control(SafeFileHandle device, 
      DeviceControlCode deviceControlCode);

   public static void Control(SafeFileHandle device, 
      DeviceControlCode deviceControlCode, Object inBuffer);

   public static TResult GetObject<TResult>(SafeFileHandle device, 
      DeviceControlCode deviceControlCode) where TResult: new();

   public static TResult GetObject<TResult>(SafeFileHandle device,
      DeviceControlCode deviceControlCode, Object inBuffer) 
          where TResult : new();

   public static TElement[] GetArray<TElement>(SafeFileHandle device, 
      DeviceControlCode deviceControlCode, Object inBuffer, 
      Int32 maxElements) where TElement : struct;
}
                                        
                                        
在托管应用程序中,您可调用 OpenDevice,传入类似 图 1 所示的字符串,以及所需的访问和共享。OpenDevice 将内部调用 CreateFile,向设备返回句柄。返回时,该句柄实际上是包装在 SafeFileHandle 对象中,以确保它最终会被关闭,以及获得通过所有 SafeHandle 派生类的方式提供的其他好处。此 SafeFileHandle 对象可作为第一个参数传递给 DeviceIO 的任何其他静态方法,并且在调用 Win32 DeviceIoControl 函数时,它最终作为第一个参数被传递。
传递给所有其他方法的第二个参数是 DeviceControlCode,这是一种只包含一个私有实例字段 Int32 的简单结构(值类型),其中 Int32 负责标识您想要发送至设备的命令代码。我发现将 Int32 命令代码包装进自身的类型不仅能提高代码的可读性和类型安全性,而且还能从 Visual Studio ® 内的 IntelliSense ® 获益。DeviceControlCode 实例内包含的 Int32 值在内部作为第二个参数被传递给 Win32 DeviceIoControl 函数。
现在,当研究设备控制代码时,您将发现有一些常用模式,我决定提供友好方法以使这些常用模式的使用更为简便。所有这些方法都同步执行操作,也就是说,在操作完成之前,调用线程不会返回。此外,如果要调用这些方法中的任何一种,在您调用 OpenDevice 时均须为 useAsync 参数指定 false。
Control 方法封装了一种模式,其中您向设备发送的控制代码会促使设备执行一些操作,并且设备不返回结果数据。有一种 Control 方法只接受一个 SafeFileHandle、一个控制代码和一个接受附加 Object 参数的重载。附加的 Object 参数允许您向设备传递一些它操作时使用的其他数据。当您查看 Win32 SDK 文档中的控制代码时,该文档将指出您必须为每个控制代码传递的其他信息(如果需要)。如果您需要其他数据,您必须定义等同于 Win32 数据类型的托管类型、构造其实例、初始化其字段,并向 inBuffer 参数传递此实例的引用。
GetObject<TResult> 方法封装了一个模式,其中您向设备发送的控制代码会促使设备向您的应用程序返回一些数据。为得到此返回数据,您必须定义一个等同于 Win32 数据类型的托管类型,并在调用 GetObject 时将此类型指定为通用类型 TResult。GetObject 将在内部创建此类型的一个实例(这就是通用类型适用新约束的原因),设备将初始化 DeviceIoControl 调用内部的字段,并且 GetObject 将向您返回经初始化的对象。在调用 GetObject 时,有两种重载允许您有选择性地通过 inBuffer 参数将其他数据传递至设备。
GetArray<TElement> 方法封装了最后一个模式,其中您向设备发送的控制代码会促使设备向您的应用程序返回一个元素数组。要得到这些元素,您必须定义一个等同于 Win32 元素的数据类型的托管类型,并在调用 GetArray 时将此类型指定为通用类型 TElement。托管数据类型必须定义为结构(值类型),以便按 DeviceIoControl 的要求线性排列数组内存;这就是将 TElement 限定为 struct 的原因。此外,在调用 GetArray 时,您还必须指定您想通过 maxElements 参数从设备返回的最大元素数量。
GetObject 将在内部创建这些元素的数组,并将该数组的地址传递给将初始化单个数组元素的 DeviceIoControl。在 DeviceIoControl 返回时,它返回实际置于数组中的字节数。GetArray 使用此值收缩(如果需要)数组至准确大小,以便数组内的元素数量等于经初始化的元素的数量。这样使用起来非常方便,因为任何使用此数组(从 GetArray 返回)的代码只需简单地查询数组的 Length 属性,便可获得项目数量,并在其循环内使用此值来处理返回的元素。
图 3 显示了 Win32 DisplayBrightness 结构的托管等价物及其帮助器 DisplayPowerFlags 枚举类型。 图 4 显示了使用我的静态 DeviceIO 类以不断上下调整您的 LCD 亮度的 AdjustBrightness 方法。我必须指出,在现实生活中,这并不是一个很实用的应用程序,它只不过是用来说明基本概念的简单示例。
                复制代码                      
public static void AdjustBrightness(6) {
   // The code for setting LCD brightness; obtained by looking up 
   // IOCTL_VIDEO_SET_DISPLAY_BRIGHTNESS in Platform SDK documentation
   DeviceControlCode s_SetBrightness =
      new DeviceControlCode(DeviceType.Video, 0x127, 
         DeviceMethod.Buffered, DeviceAccess.Any);

   // Open the device
   using (SafeFileHandle lcd = DeviceIO.OpenDevice(@”\\.\LCD”, 
      FileAccess.ReadWrite, FileShare.ReadWrite, false)) {

      for (Int32 times = 0; times < 10; times++) {
         for (Int32 dim = 0; dim <= 100; dim += 20) {
            // Initialize the equivalent of a Win32 DISPLAY_BRIGHTNESS 
            // structure
            DisplayBrightness db = 
               new DisplayBrightness(DisplayPowerFlags.ACDC, 
                  (Byte)((times % 2 == 0) ? dim : 100 - dim));

            // Tell the LCD device to adjust its brightness
            DeviceIO.Control(lcd, s_SetBrightness, db);

            // Sleep for a bit and then adjust it again
            Thread.Sleep(150);
         }
      }
   }
}
                                        
                                        
                复制代码                      
[Flags] 
internal enum DisplayPowerFlags : byte {
   None = 0x00000000, AC = 0x00000001, DC = 0x00000002, ACDC = AC | DC
}

internal struct DisplayBrightness {
   public DisplayPowerFlags m_Power;
   public Byte m_ACBrightness; // 0-100
   public Byte m_DCBrightness; // 0-100

   public Win32DisplayBrightnessStructure(
         DisplayPowerFlags power, Byte level) {
      m_Power = power;
      m_ACBrightness = m_DCBrightness = level;
   }
}
                                        
                                        
托管代码中的异步设备 I/O
有些设备操作,如更改 LCD 亮度,其实并非为 I/O 操作,因此,在执行此类操作时,同步调用 DeviceIoControl 很有意义。但是,对 DeviceIoControl 的多数调用都会产生 I/O(因此有了这种函数名称),并且因此需要异步执行这些操作。在本部分,我将解释如何从托管代码中 P/Invoke 至 Win32 DeviceIoControl 函数,从而对文件、磁盘和其他硬件设备执行异步 I/O 操作。
我的静态 DeviceIO 类通过 CLR 的 APM 方式提供异步版的 Control、GetObject 和 GetArray 方法。 图 5 显示了支持 APM 的此类的公共方法(此前未公布)。
                复制代码                      
public static class DeviceIO {
   public static IAsyncResult BeginControl(SafeFileHandle device,
      DeviceControlCode deviceControlCode, Object inBuffer,
      AsyncCallback asyncCallback, Object state);

   public static void EndControl(IAsyncResult result);

   public static IAsyncResult BeginGetObject<TResult>(
      SafeFileHandle device, DeviceControlCode deviceControlCode, 
      Object inBuffer, AsyncCallback asyncCallback, Object state) 
      where TResult: new();

   public static TResult EndGetObject<TResult>(IAsyncResult result)
      where TResult: new();

   public static IAsyncResult BeginGetArray<TElement>(
      SafeFileHandle device, DeviceControlCode deviceControlCode, 
      Object inBuffer, Int32 maxElements, AsyncCallback asyncCallback, 
      Object state) where TElement: struct;

   public static TElement[] EndGetArray<TElement>(IAsyncResult result) 
      where TElement: struct;
}
                                        
                                        
如您所看到的,所有 BeginXxx 方法都遵循 CLR 的 APM 模式;也就是说,它们全部都返回一个 IAsyncResult,并且最后两个参数是 AsyncCallback 和 Object。附加参数与每个方法的同步对应部分都相匹配。同样,所有 EndXxx 方法均接受单一参数 IAsyncResult,并且每个方法都返回与此方法同步对应部分相同的数据类型。
为了调用这些异步方法中的任何一种,必须完成两件事情。首先,必须告诉 Windows 您想在设备上异步执行操作。这可通过传递 FILE_FLAG_OVERLAPPED 标志(其数值为 0x40000000)至 CreateFile 函数来实现。此标志的托管等价物是 FileOptions.Asynchronous(它当然也具有数值 0x40000000)。其次,必须告诉设备驱动程序将操作完成条目发布到 CLR 的线程池。这可通过调用 ThreadPool 的静态 BindHandle 方法来实现,该方法接受一个参数,即对 SafeHandle 派生对象的引用。
当您对其 useAsync 参数传递 true 时,DeviceIO 的 OpenDevice 方法会同时执行这两种操作。以下是我的 OpenDevice 方法的实现过程:
                复制代码                      
public static SafeFileHandle OpenDevice(String deviceName, 
   FileAccess access, FileShare share, Boolean useAsync) {

   SafeFileHandle device = CreateFile(deviceName, access, share, 
      IntPtr.Zero, FileMode.Open, 
      useAsync ? FileOptions.Asynchronous : FileOptions.None, 
      IntPtr.Zero);

   if (device.IsInvalid) throw new Win32Exception();
   if (useAsync) ThreadPool.BindHandle(device);

   return device;
}
                                        
现在,让我们看一下 BeginGetObject 和 EndGetObject 的内部工作过程。其他异步方法的工作过程都很类似,通过了解 BeginGetObject 和 EndGetObject 之后,您将轻松理解它们是如何工作的。所有 BeginXxx 方法都很简单;如果操作返回一些返回值或元素数组,它们将构造一个对象或数组,然后立即调用内部帮助器方法 AsyncControl,其实现过程如 图 6 所示。
复制代码                      
private static DeviceAsyncResult<T> AsyncControl<T>(
    SafeFileHandle device, 
   DeviceControlCode deviceControlCode, Object inBuffer, T outBuffer, 
   AsyncCallback asyncCallback, Object state) {

   SafePinnedObject inDeviceBuffer = new SafePinnedObject(inBuffer);
   SafePinnedObject outDeviceBuffer = new SafePinnedObject(outBuffer);
   DeviceAsyncResult<T> asyncResult = new DeviceAsyncResult<T>(
      inDeviceBuffer, outDeviceBuffer, asyncCallback, state);

   unsafe {
      Int32 bytesReturned;
      NativeControl(device, deviceControlCode, inDeviceBuffer, 
          outDeviceBuffer, out bytesReturned, 
          asyncResult.GetNativeOverlapped());
   }
   return asyncResult;
}
                                        
                                        
在执行操作时,您可将包含其他输入数据的缓冲区的地址传递给 DeviceIoControl。如果 DeviceIoControl 返回数据,则在调用 DeviceIoControl 之前必须分配该数据的内存,这样 DeviceIoControl 才能初始化该数据;然后,在操作完成时,会检查该数据。问题在于理论上操作可能需要数小时才能完成,在这期间可能会发生移动该数据的垃圾回收。结果,传递给 DeviceIoControl 的地址将不再引用所需的缓冲区,并因此发生内存损坏。
您可通过固定对象来告诉垃圾回收 (GC) 不要移动此对象。我的 SafePinnedObject 类是一个固定内存中对象、防止 GC 移动对象的帮助器类。但是,由于 SafePinnedObject 派生于 SafeHandle,因此它具有 SafeHandle 派生类型通常具有的所有优点,包括保证对象在将来某些时间会被解除固定。 图 7 向您说明了 SafePinnedObject 类是如何实现的(我已删除部分验证代码以节约空间)。有关 SafeHandle 派生类型和固定内存中的对象的详细信息,请参见我的著作《CLR via C#》(Microsoft Press ®,2006 年)。
                复制代码                      
public sealed class SafePinnedObject : SafeHandleZeroOrMinusOneIsInvalid {
   private GCHandle m_gcHandle;  // Handle of pinned object (or 0)

   public SafePinnedObject(Object obj) : base(true) {
      // If obj is null, we create this object but it pins nothing
      if (obj == null) return;

      // If specified, pin the buffer and save its memory address
      m_gcHandle = GCHandle.Alloc(obj, GCHandleType.Pinned);
      SetHandle(m_gcHandle.AddrOfPinnedObject());
   }

   protected override Boolean ReleaseHandle() {
      SetHandle(IntPtr.Zero); // Just for safety, set the address to null
      m_gcHandle.Free();      // Unpin the object
      return true;
   }

   // Returns the object of a pinned buffer or null if not specified
   public Object Target {
      get { return IsInvalid ? null : m_gcHandle.Target; }
   }

   // Returns the number of bytes in a pinned object or 0 if not specified
   public Int32 Size {
      get {
         Object target = Target;

         // If obj was null, return 0 bytes
         if (target == null) return 0;

         // If obj is not an array, return the size of it
         if (!target.GetType().IsArray) return Marshal.SizeOf(target);

         // obj is an array, return the total size of the array
         Array a = (Array)target;
         return a.Length * Marshal.SizeOf(a.GetType().GetElementType());
      }
   }
}
                                        
                                        
在固定了输入和输出缓冲器后,AsyncControl 会构造一个 DeviceAsyncResult<TResult> 对象,并将两个 SafePinnedObject 对象,AsyncCallback 和 an Object 传入其中。DeviceAsyncResult<TResult> 是我的实现内部的另一个类型。此类型派生于 AsyncResult<TResult>(我在上一期专栏中讨论过),这意味着此类型将实现 APM 的 IAsyncResult 接口。构造时,DeviceAsyncResult 只需将所有传递给它的参数保存到私有字段中存,然后返回。

DeviceIoControl 的重叠参数
现在已经完成所有准备工作,AsyncControl 准备调用 DeviceIoControl 了。它的方法是调用 NativeControl 并向其传递设备句柄、控制代码、输入缓冲区、输出缓冲区和对 Int32 bytesReturned 变量的引用(由于该方法将在操作完成之前返回,因此基本上将被该方法忽略)。最重要的参数是最后一个:当执行异步操作时,必须向 DeviceIoControl 传递 NativeOverlapped 结构(Win32 OVERLAPPED 结构的等价物)的地址。AsyncControl 通过调用在我的 DeviceAsyncResult 类中定义的帮助器方法 GetNativeOverlapped 来获得此地址。此帮助器方法的实现过程如下:
                复制代码                      
// Create and return a NativeOverlapped structure to be passed 
// to native code
public unsafe NativeOverlapped* GetNativeOverlapped() {
   // Create a managed Overlapped structure that refers to our 
   // IAsyncResult (this)
   Overlapped o = new Overlapped(0, 0, IntPtr.Zero, this);

   // Pack the managed Overlapped structure into a NativeOverlapped 
   // structure
   return o.Pack(CompletionCallback, 
      new Object[] { m_inBuffer.Target, m_outBuffer.Target });
}
                                        
此方法构造一个 System.Threading.Overlapped 类的实例,并对它进行初始化。这是一个托管帮助器类,它允许您建立和操作重叠结构。但是,您不能将对此对象的引用传递给本机代码。您必须转而先将 Overlapped 对象包装到 NativeOverlapped 对象中;然后再将包装后的 NativeOverlapped 对象的引用传递给本机代码。在您调用 Pack 时,您向它传递一个引用回调方法的委托,在操作完成时,进程池进程将执行此回调方法。在我的代码中,它就是 CompletionCallback 方法。
调用 Overlapped 类的 Pack 方法要完成几件事情。它从托管堆中为本机 OVERLAPPED 结构分配内存并固定它,这样一旦发生 GC,就可以保证不会清除内存。然后,它从托管 Overlapped 对象中设置的字段中初始化 NativeOverlapped 结构的字段。这包括为 Overlapped 对象引用的 IAsyncResult 对象创建一个普通 GCHandle,以确保 IAsyncResult 对象在操作期间处于活动状态。
Pack 随后固定回调方法 (CompletionCallback) 委托,这样固定地址就可被传递给本机代码,从而允许本机代码在此操作完成时回调托管代码。Pack 还固定其第二个参数(即 Object 数组)所引用的任何其他对象。在我的示例中,inBuffer 和 outBuffer 对象已经被固定,因为我用我的 SafePinnedObject 类包装了它们。我必须自己固定它们,这样我就可以通过调用 GCHandle 的 AddrOfPinnedObject 方法获得它们的地址。
尽管 Pack 方法会再次固定它们,但却使用特殊方式。在卸载 AppDomain 时,CLR 通常会自动解除固定所有对象,允许它们被回收并因此防止内存泄漏。但是,Pack 会固定对象直到异步操作完成。因此,如果启动某个异步操作,然后再卸载 AppDomain,则被 Pack 固定的对象不会解除固定。在操作完成后,这些对象将被解除固定,允许回收它们;这样就可以防止内存损坏。
Pack 还会记录调用 Pack 的 AppDomain 以确保调用 CompletionCallback 方法的线程池线程在同一个 AppDomain 中执行。最后,Pack 会捕获代码访问安全权限的堆栈,以便在执行回调方法时,它执行所用的安全权限和初始化异步操作的代码所用的安全权限相同。如果不希望传播安全权限的堆栈,您可调用 Overlapped 类的 UnsafePack 方法。
Pack 返回被固定的 NativeOverlapped 结构的地址;此地址可被传递给希望将此地址传递给 Win32 OVERLAPPED 结构的任何本地函数。在我的 AsyncControl 方法中,NativeOverlapped 结构的地址被传递给 NativeControl(请参见 图 8),后者再将它传递给 Win32 DeviceIoControl 函数。
                复制代码                      
private static unsafe void NativeControl(SafeFileHandle device, 
   DeviceControlCode deviceControlCode, 
   SafePinnedObject inBuffer, SafePinnedObject outBuffer, 
   out Int32 bytesReturned, NativeOverlapped* nativeOverlapped) {

   Boolean ok = DeviceIoControl(device, deviceControlCode.Code,
      inBuffer, inBuffer.Size, outBuffer, outBuffer.Size, 
      out bytesReturned, nativeOverlapped);
   if (ok) return;

   Int32 error = Marshal.GetLastWin32Error();
   const Int32 c_ErrorIOPending = 997;
   if (error == c_ErrorIOPending) return;
   throw new InvalidOperationException(
      String.Format(“Control failed (code={0})”, error));
}
                                        
                                        
在执行异步操作时,DeviceIoControl 立即并最终返回 DeviceAsyncResult 对象,由该对象实现从 DeviceIO 的 BeginObject<TResult> 返回的 IAsyncResult 接口。
操作完成时,设备驱动程序将条目放到 CLR 线程池的队列中。您会想起驱动程序知道这么做,因为当打开设备以允许异步访问时,DeviceIO 的 OpenDevice 方法会从内部调用 ThreadPool 的 BindHandle 方法。最后,线程池线程将从队列中提取此项,采用任何保存的权限集,跳转到适当的 AppDomain,并调用 CompletionCallback 方法(请参见 图 9)。不要忘了,CompletionCallback 方法是在派生于 AsyncResult<TResult> 类的 DeviceAsyncResult<TResult> 类内部定义的。
复制代码                      
// Called by a thread pool thread when native overlapped I/O completes
private unsafe void CompletionCallback(UInt32 errorCode, UInt32 numBytes,
   NativeOverlapped* nativeOverlapped) {
   // Release the native OVERLAPPED structure and 
   // let the IAsyncResult object (this) be collectable.
   Overlapped.Free(nativeOverlapped);

   try {
      if (errorCode != 0) {
         // An error occurred, record the Win32 error code
         base.SetAsCompleted(new Win32Exception((Int32)errorCode), false);
      } else {
         // No error occurred, the output buffer contains the result
         TResult result = (TResult)m_outBuffer.Target;

         // If the result is an array of values, resize the array 
         // to the exact size so that the Length property is accurate
         if ((result != null) && result.GetType().IsArray) {
            // Only resize if the number of elements initialized in the 
            // array is less than the size of the array itself
            Type elementType = result.GetType().GetElementType();
            Int64 numElements = numBytes / Marshal.SizeOf(elementType);
            Array origArray = (Array)(Object)result;
            if (numElements < origArray.Length) {
               // Create new array (size equals number of initialized 
               // elements)
               Array newArray = Array.CreateInstance(
                   elementType, numElements);

               // Copy initialized elements from original array to new 
               // array
               Array.Copy(origArray, newArray, numElements);
               result = (TResult)(Object)newArray;
            }
         }
         // Record result and call AsyncCallback method passed to BeginXxx 
         // method
         base.SetAsCompleted(result, false);
      }
   }
   finally {
      // Make sure that the input and output buffers are unpinned
      m_inBuffer.Dispose();
      m_outBuffer.Dispose();
      m_inBuffer = m_outBuffer = null;   // Allow early GC
   }
}
                                        
                                        
CompletionCallback 方法所做的第一件事是释放 nativeOverlapped 对象。这一步极其重要,因为它释放 IAsyncResult 对象上的 GCHandle,解除固定所有被传递给 Overlapped 类的 Pack 方法的对象,并且还解除固定 NativeOverlapped 对象本身,允许它被垃圾回收。忘记释放 NativeOverlapped 对象会导致泄漏多个对象。
CompletionCallback 的其余代码都非常简单。要做的仅仅是在 AsyncResult<T> 基类的状态内记录操作是否失败,或者记录操作的结果(如果调用 DeviceIO 的 BeginGetArray 以初始化操作,则调整数组的大小)。在 CompletionCallback 的最后阶段,有一些代码用来释放 SafePinnedObject 对象,确保输入和输出缓冲区不再被固定,并允许通过垃圾回收对其进行压缩,并最终当应用程序不再使用这些缓冲区时由垃圾回收收回它们的内存。
有时候,应用程序代码将调用 DeviceIO 的 EndControl、EndGetObject 或 EndGetArray 方法,传入从相应的 Begin 方法返回的 DeviceAsyncResult<TResult> 对象。从内部来说,所有这些 End 方法都仅调用 AsyncResult<TResult> 的 EndInvoke 方法,该方法要么将异常情况集引发到其内部的 CompletionCallback,要么将值集返回其内部。

伺机锁定
伺机锁定向您提供一个演示异步设备操作的简单示例。 图 10 显示了一个静态 OpLockDemo 类,其中的 Main 方法会创建一个文件(使用 FileOptions.Asynchronous 标志)。嵌入 FileStream 内部的 SafeFileHandle 随后被传递给 BeginFilter,后者会从内部调用 DeviceIO 的 BeginControl 方法,向构成请求筛选的代码传递文件伺机锁定。现在文件系统设备驱动程序将进行监视,查看是否有任何其他应用程序在试图打开该文件,如果有,该设备驱动程序将在 CLR 线程池队列中添加一个项。随后将调用一个匿名方法,它将 s_earlyEnd 字段设置为 1(表明其他应用程序想要访问该文件)。然后,第一个应用程序将关闭文件,允许其他应用程序继续运行并能够访问该文件。
                复制代码                      
internal static class OpLockDemo {
   private static Byte s_endEarly = 0;

   public static void Main() {
      String filename = Path.Combine(
         Environment.GetFolderPath(Environment.SpecialFolder.Desktop), 
         @”FilterOpLock.dat”);

      // Attempt to open/create the file for processing (must be 
      // asynchronous)
      using (FileStream fs = new FileStream(
             filename, FileMode.OpenOrCreate,
         FileAccess.ReadWrite, FileShare.ReadWrite, 8096,
         FileOptions.Asynchronous)) {

         // Request the Filter Oplock on the file.
         // When another process attempts to access the file, the system 
         // will 
         //    1. Block the other process until we close the file
         //    2. Call us back notifying us that another process wants to 
         // access the file
         BeginFilter(fs.SafeFileHandle, 
            delegate(IAsyncResult result) {
               EndFilter(result);
               Console.WriteLine(“Another process wants to access “ +
                                “the file or the file closed”);
               Thread.VolatileWrite(ref s_endEarly, 1);  // Tell Main 
                                                         // thread to end 
                                                         // early
            }, null);

         // Pretend we’re accessing the file here
         for (Int32 count = 0; count < 100; count++) {
            Console.WriteLine(“Accessing the file ({0})...”, count);

            // If the user hits a key or if another application 
            // wants to access the file, close the file.
            if (Console.KeyAvailable || 
               (Thread.VolatileRead(ref s_endEarly) == 1)) break;
            Thread.Sleep(150);
         }
      }  // Close the file here allows the other process to continue 
         // running
   }

   // Establish a Request filter opportunistic lock
   private static IAsyncResult BeginFilter(SafeFileHandle file, 
      AsyncCallback asyncCallback, Object state) {

      // See FSCTL_REQUEST_FILTER_OPLOCK in Platform SDK’s WinIoCtl.h file
      DeviceControlCode RequestFilterOpLock =
         new DeviceControlCode(DeviceType.FileSystem, 23, 
            DeviceMethod.Buffered, DeviceAccess.Any);

      return DeviceIO.BeginControl(file, RequestFilterOpLock, null, 
         asyncCallback, state);
   }

   private static void EndFilter(IAsyncResult result) { 
      DeviceIO.EndControl(result); 
   }
}
                                        
                                        
要测试这一整个过程,请启动 OpLockDemo 应用程序。然后回到桌面,并尝试删除该应用程序创建的 FilterOpLock.dat 文件。这将导致调用匿名方法,从而结束示例应用程序。

结束语
Windows 及其设备驱动程序提供了 .NET Framework 类库未包含的许多功能。但幸运的是,.NET Framework 确实提供了适当的机制,允许您 P/Invoke 以访问这些有用的功能。您可异步执行这些操作的事实意味着您可以构建能充分利用这些功能的强大、可靠和可伸缩的应用程序。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值