在前一篇中我们看到通过使用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对象来代表光驱。如下例:
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 { get; private 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定义如下:
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在头文件中的定义:
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个异步方法:
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对应的托管类型定义。下面是弹出光驱的代码:
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, null, null, null));
41
42 ar.SetAsCompleted(null, false);
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