在前一篇中我们看到通过使用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 {
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定义如下:
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,
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