前一篇中我们看到通过使用PowerThreading中的AsyncResult<T>类,我们可以很方便的将一个同步操作封装成异步的方式。同时使用这种方法和PInvoke,我们也可以为现有的C++设备库,如蓝牙设备提供一个.Net的异步类库。这样我们可以实现大部分对设备访问的.Net异步类库。

但当我们有特殊要求时,如果调整LCD亮度时,就需要调用Window API中的DeviceIoControl函数。PInvoke加上使用IO完成端口是件繁琐且容易出错的事情,幸运地是,我们有Richard,通过使用PowerThreading中的DeviceIO类,我们可以很方便的实现一个异步设备操作类。下面通过实现一个简单的异步打开光驱的方法,来看看如何使用Wintellect.IO.DeviceIO。

获得光驱句柄

在Win32中要访问一个设备,首先我们要调用CreateFile方法来获得该设备的文件句柄。PowerTheading中DeviceIO类对CreateFile进行了封装。我们可以容易通过创建一个DeviceIO对象来代表光驱。如下例:

 1 public class CDDrive : IDisposable
 2 {
 3     private const Int32 LOCK_TIMEOUT = 10000// 10s
 4     private const Int32 LOCK_RETRIES = 20;
 5     private const String DRIVE_PATH_TPL = @"{0}:";
 6     private const String FILE_PATH_TPL = @"\\.\{0}:";
 7 
 8     public Char DriveLetter { getprivate set; }
 9     private DeviceIO m_cdrom = null;
10 
11     public CDDrive(Char driveletter)
12     {
13         if (!Char.IsLetter(driveletter))
14         {
15             throw new ArgumentException("无效盘符""letter");
16         }
17 
18         // 判断该盘符是否是光驱
19 var tmp = Win32Functions.GetDriveType(
20                 String.Format(DRIVE_PATH_TPL, driveletter));
21 
22         if ((tmp != DriveType.CDRom))
23         {
24             throw new ArgumentException("无效盘符""letter");
25         }
26 
27         DriveLetter = driveletter;
28 
29         this.Open();
30     }
31 
32     public void Open()
33     {
34         this.Close();
35 
36         // 异步方式打开CDROM文件句柄,如失败抛出Win32Exception异常
37         m_cdrom = new DeviceIO(
38                 String.Format(FILE_PATH_TPL, DriveLetter),
39                 FileAccess.ReadWrite, FileShare.ReadWrite, true);
40     }
41 
42     public void Close()
43     {
44         this.Dispose(true);
45     }
46 
47     public void Dispose()
48     {
49         this.Dispose(true);
50     }
51 
52     ~CDDrive()
53     {
54         Dispose(false);
55     }
56 
57     protected virtual void Dispose(Boolean disposing)
58     {
59         if (disposing)
60         {
61             if (m_cdrom != null)
62             {
63                 m_cdrom.Dispose();
64                 m_cdrom = null;
65             }
66         }
67         GC.SuppressFinalize(this);
68     }
69 }

 

DeviceIO有两个构造函数,主要区别在于第一个参数。我们可以传入文件路径,或者传入一个已获得的文件句柄。如果文件路径不对,或者访问方式不对,会抛出Win32Exception的异常。DeviceIO实现了IDisposable接口,当不需要使用时可以调用Dispose方法来释放文件句柄。CDDrive也实现了IDisposable接口,及Finalizer以确保光驱的句柄能够释放。

设备操作控制码

在Win32中,如果我们要向IO设备发出指令,需要调用DeviceIoControl方法。该方法需要设备文件句柄,操作控制码,传入数据及返回数据的Buffer。文件句柄在前面已经获得,而这个操作控制码在Win32中,是一个DWORD类型,代表了不同的操作,如打开光驱,调亮LCD等等。PowerThreading库中的DeviceControlCode 类可以帮助我们创建一个托管的结构。如弹出光驱的操作控制码IOCTL_STORAGE_EJECT_MEDIA定义如下:

 1 public static class IoControlCode
 2 {
 3     public static DeviceControlCode IOCTL_STORAGE_EJECT_MEDIA =
 4         new DeviceControlCode(
 5             DeviceType.MassStorage, 0x0202,
 6             DeviceMethod.Buffered, DeviceAccess.Read);
 7 
 8     public static DeviceControlCode FSCTL_LOCK_VOLUME =
 9         new DeviceControlCode( 
10             DeviceType.FileSystem, 0x6,
11             DeviceMethod.Buffered, DeviceAccess.Any);
12 
13     public static DeviceControlCode FSCTL_DISMOUNT_VOLUME =
14         new DeviceControlCode(
15             DeviceType.FileSystem, 0x8,
16             DeviceMethod.Buffered, DeviceAccess.Any);
17 
18     public static DeviceControlCode IOCTL_STORAGE_MEDIA_REMOVAL =
19         new DeviceControlCode(
20             DeviceType.MassStorage, 0x201,
21             DeviceMethod.Buffered, DeviceAccess.Read);
22 }

 

操作控制码分为4个部分:设备类型,操作,设备方法及访问方式。这个与WinSDK中Winioctl.h中定义相同,我们可以根据这个头文件很容易的创建对应的DeviceControlCode结构。下面是IOCTL_STORAGE_EJECT_MEDIA在头文件中的定义:

1 #define IOCTL_STORAGE_EJECT_MEDIA             
2 CTL_CODE(IOCTL_STORAGE_BASE, 0x0202, METHOD_BUFFERED, FILE_READ_ACCESS)

在上面的IoControlCode静态类中,定义了4个操作控制码。这是因为如果我们弹出光驱或Eject可移动媒体,我们不能简单发出IOCTL_STORAGE_EJECT_MEDIA操作控制码,首先我们得发出FSCTL_LOCK_VOLUME锁住光驱以防止其他人写入,在发出FSCTL_DISMOUNT_VOLUME卸载卷,在发出IOCTL_STORAGE_MEDIA_REMOVAL移除媒体,最后才发出IOCTL_STORAGE_EJECT_MEDIA弹出光驱。具体可参考Microsoft KB 165721

发出设备操作

在第一步中,通过创建DeviceIO对象,我们已经获得了光驱的文件句柄。DeviceIO类提供了同步和异步的两套3个方法,来帮助我们发送操作到设备。同步方法内部实际调用的异步方法,所以下面我们看看3个异步方法:

 1     public IAsyncResult BeginControl(
 2         DeviceControlCode deviceControlCode, object inBuffer, 
 3         AsyncCallback asyncCallback, object state);
 4     public void EndControl(IAsyncResult result);
 5 
 6     public IAsyncResult BeginGetArray<TElement>(
 7         DeviceControlCode deviceControlCode, object inBuffer, 
 8         int maxElements, AsyncCallback asyncCallback, 
 9         object state) where TElement : struct;
10     public TElement[] EndGetArray<TElement>(IAsyncResult result) where TElement : struct;
11 
12     public IAsyncResult BeginGetObject<TResult>(
13         DeviceControlCode deviceControlCode, object inBuffer, 
14         AsyncCallback asyncCallback, 
15         object state) where TResult : new();
16     public TResult EndGetObject<TResult>(IAsyncResult result) where TResult : new();
17 

如果操作不需要返回数据时,可以使用BeginControl;如有返回数据可调用BeginGetObject,TResult对应返回数据的类型;而当返回的数据是数组是可使用BeginGetArray,TElement是返回数组的元素类型。我们可以在http://pinvoke.net/ 网站或google上查找TResult和TElement对应的托管类型定义。下面是弹出光驱的代码:

 1     public IAsyncResult BeginEject(
 2                 AsyncCallback callback, Object state)
 3     {
 4         AsyncResult ar = new AsyncResult(callback, state);
 5 
 6         ThreadPool.QueueUserWorkItem(
 7                 new WaitCallback(delegate {
 8 
 9                     // 调用IOCTL_STORAGE_EJECT_MEDIA,尝试Lock光驱
10                     for (Int32 tryCount = 0; tryCount < LOCK_RETRIES; tryCount++)
11                     {
12                         try
13                         {
14                             m_cdrom.Control(IoControlCode.FSCTL_LOCK_VOLUME);
15                             break;
16                         }
17                         catch (Exception ex)
18                         {
19                             // 如最后一次仍不能获得光驱,则返回异常
20                             if (tryCount == LOCK_RETRIES - 1)
21                             {
22                                 ar.SetAsCompleted(ex, false);
23                                 return;
24                             }
25                         }
26                         Thread.Sleep(LOCK_RETRIES);
27                     }
28 
29                     try
30                     {
31                         // 调用FSCTL_DISMOUNT_VOLUME卸载卷
32                         m_cdrom.Control(IoControlCode.FSCTL_DISMOUNT_VOLUME);
33 
34                         // 调用IOCTL_STORAGE_MEDIA_REMOVAL移除媒体
35                         m_cdrom.Control(IoControlCode.IOCTL_STORAGE_MEDIA_REMOVAL,
36                             new PREVENT_MEDIA_REMOVAL(false));
37 
38                         // 调用IOCTL_STORAGE_EJECT_MEDIA异步方法弹出光驱
39                         m_cdrom.EndControl(m_cdrom.BeginControl(
40                             IoControlCode.IOCTL_STORAGE_EJECT_MEDIA, nullnullnull));
41 
42                         ar.SetAsCompleted(nullfalse);
43 
44                     }
45                     catch (Exception ex)
46                     {
47                         ar.SetAsCompleted(ex, false);
48                     }
49                 }),
50                 null);
51 
52         return ar;
53     }
54 
55     public void EndEject(IAsyncResult asyncResult)
56     {
57         AsyncResult ar = (AsyncResult)asyncResult;
58         ar.EndInvoke();
59     }

上面的代码中,我们只使用了Control方法,其中IOCTL_STORAGE_MEDIA_REMOVAL需要传入数据。GetObject和GetArray的使用方法类似。

由于我们得循环的检查Lock是否成功,我们不得不使用线程池中的线程顺序的发出操作指令,因而该线程并没有最优化。如果我们都采取异步方式,我们不得不写很多回调或匿名函数,代码将变得很难看。这也是APM代码很麻烦的一个原因。后面的文章我们在来看看有没有更好的办法。

参考:

Asynchronous Device Operations by Jeffery Richard

KB165721: How To Ejecting Removable Media in Windows NT/Windows 2000/Windows XP by Microsoft