尽管使用Windows shell(传说中的“命令行”)移除一个USB是非常容易的,但是想通过编程实现却非常恼火。你必须去了解很多内核驱动开发的底层概念,才能完成一个简单的任务。当我着手做这件事的时候,我真的不知道该从何入手。我很确定我不想在内核驱动控制代码,Windows Setup和Configuration Manager API,WMI...中转来转去了。
好吧,这就是我写这篇文章的主要原因,我认为这个主题的文章真的有必要。最后你将看到一个.Net WinForm的Demo,和一个可复用的代码。
首先,让我来谈谈将要用到的windows API(Win32就免了吧):
1. The Windows DDK (Driver Development Kit):一个驱动开发包,提供编程环境,工具,开发案例,和相关的支持文档。
2. The Setup API:很少被人所知的Windows APi。其中一个原因是它已经包含在DDK中了,但是,事实上,还是有很多常用的东西的,其中有些可以用于安装应用程序的时候,并且还有很多有趣的东西,就像CM_Request_Device_Eject函数,它就是我们这个UsbEject程序的核心所在。
3. The DeviceIoControl function:发送一个控制代码直接到指定的设备驱动程序,导致相应的设备来执行相应的操作。它可以完成很多事情,但是文档不全。
4. WMI (Windows Management Instrumentation):事实上,UsbEject 并没有使用WMI,因为据我所知,WMI并不能解决设备的移除。
5. Windows消息: WM_DEVICECHANGE消息,当有设备变化(usb插入或者移除)的时候,产生WM_DEVICECHANGE消息。因此我们可以通过这个消息来刷新页面显示当前设备。
现在让我来介绍一些概念。这些概念是我自己定义的,并不是官方的(官方的定义确实很难找)。
6. Physical Disks:正如其名,它是用户正在操作的一块硬件。就像我们说Usb Disk,那就是一个U盘。
7. Volumes:事实上,有两种“卷”:一种是由操作系统本身理解的,像我们看到的从A到Z的驱动器号,也被称作逻辑磁盘,一个卷可以跨越几个物理磁盘。
8. Devices:在操作系统的DDK中,以上两个都被定义为Device(设备)。
我的代码中反映了上述情况,Devices是基类,Volumes继承于它。
(译者注:您很有必要理解上述概念,因为本文的后续部分将多次使用该概念。)
在工程的Library文件夹中,是代码的核心。其中的类的关系如下图:
这里有5个主要的类:
1. DeviceClass:一个代表物理设备的抽象类,它包含一个Device的链表。
2. DiskDeviceClass:代表系统中所有的磁盘设备。
3. VolumeDeviceClass:代表系统中所有的卷
4. Device:代表一个通用设备的任何类型(磁盘,卷.....),注意没有Diskclass,因为在这个项目中,与Device相比,磁盘没有特定的属性。还请注意,代码已经被设计成可以会扩大到其他的设备,不仅是卷和磁盘。
5. Volume:代表一个操作系统的卷,一个卷分配了一个逻辑驱动器号(如C,D)。
你可以简单的移除USB像下面的例子那样:
VolumeDeviceClass volumeDeviceClass = new VolumeDeviceClass();
foreach (Volume device in volumeDeviceClass.Devices)
{
// is this volume on USB disks?
if (!device.IsUsb)
continue;
// is this volume a logical disk?
if ((device.LogicalDrive == null) || (device.LogicalDrive.Length == 0))
continue;
device.Eject(true); // allow Windows to display any relevant UI
}
这是一个SetupSpi 函数用来移除一个设备(它可以移除任何设备)。它需要从第一个参数传入一个设备句柄(device instance handle)。函数会根据第二参数是否提供产生不同的效果。
如果提供,当移除USB的时候,不会产生弹出任何对话框(程序会默默的执行,不管移除成功还是失败)。相反,如果不提供该参数,当移除USB的时候,系统会以对话框或者气泡的方式提醒用户。
现在的主要问题是:对于一个给定的磁盘号(C,D,..H),如何找到哪个设备是将要移除的呢?
可以移除的设备一定是一个磁盘设备,而不是卷(至少对于USB磁盘是这样)。
1. Environment.GetLogicalDrives,使用该函数获得所有的逻辑磁盘。
2. 遍历所有的逻辑磁盘(根据磁盘号C,D......),根据GetVolumeNameForVolumeMountPoint确定真正的卷名(“卷名”将在下面讲到)。
3. 对于每一个卷,确定是否有一个逻辑磁盘号对应。
4. 对于每一个卷,确定其物理磁盘的组成(因为可以多个磁盘对应一个卷),可以使用DDK的IOCTL_VOLUME_GET_VOLUME_DISK_EXTENTS 。
5. 因为即插即用(PNP)配置管理器构建了一个设备层次结构,一个物理磁盘设备不一定就是要移除的设备,于是对于每一个物理磁盘设备,应该移除它的上层设备。被移除的设备具有CM_DEVCAP_REMOVABLE 属性的上层设备。
(译者注:关于这5点,确实有点难懂,毕竟涉及到底层知识,只有根据代码慢慢体会了)
为了可以根据逻辑磁盘(logical disk)找到相应的卷(volumes ),我们有一个小技巧。首先,我们要知道所有Windows的卷可以唯一地被指定,语法的格式为:"\\?\Volume{GUID}\",
GUID就是用来识别卷的。
在枚举所有的卷的时候,上述格式的字符串可以通过调用GetVolumeNameForVolumeMountPoint函数获得。
为了便于理解,我把作者使用该函数的代码放出来:
foreach (string drive in Environment.GetLogicalDrives())
{
StringBuilder sb = new StringBuilder(1024);
if (Native.GetVolumeNameForVolumeMountPoint(drive, sb, sb.Capacity))
{
_logicalDrives[sb.ToString()] = drive.Replace("\\", "");
}
}
下面是我给出的一段.Net互操作枚举给定类型的所有设备的代码:
int index = 0;
while (true)
{
// enumerate device interface for a given index
SP_DEVICE_INTERFACE_DATA interfaceData = new SP_DEVICE_INTERFACE_DATA();
if (!SetupDiEnumDeviceInterfaces(
_deviceInfoSet, null, ref _classGuid, index, interfaceData))
{
int error = Marshal.GetLastWin32Error();
// this is not really an error...
if (error != Native.ERROR_NO_MORE_ITEMS)
throw new Win32Exception(error);
break;
}
SP_DEVINFO_DATA devData = new SP_DEVINFO_DATA();
int size = 0;
// get detail for all the device interface
if (!SetupDiGetDeviceInterfaceDetail(
_deviceInfoSet, interfaceData, IntPtr.Zero, 0, ref size, devData))
{
int error = Marshal.GetLastWin32Error();
if (error != Native.ERROR_INSUFFICIENT_BUFFER)
throw new Win32Exception(error);
}
// allocate unmanaged Win32 buffer
IntPtr buffer = Marshal.AllocHGlobal(size);
SP_DEVICE_INTERFACE_DETAIL_DATA detailData =
new SP_DEVICE_INTERFACE_DETAIL_DATA();
detailData.cbSize = Marshal.SizeOf(
typeof(Native.SP_DEVICE_INTERFACE_DETAIL_DATA));
// copy managed struct buffer into unmanager win32 buffer
Marshal.StructureToPtr(detailData, buffer, false);
if (!SetupDiGetDeviceInterfaceDetail(
_deviceInfoSet, interfaceData, buffer, size, ref size, devData))
{
Marshal.FreeHGlobal(buffer); // don't forget to free memory
throw new Win32Exception(Marshal.GetLastWin32Error());
}
// a bit of voodoo magic. This code is not 64 bits portable :-)
IntPtr pDevicePath = (IntPtr)((int)buffer + Marshal.SizeOf(typeof(int)));
string devicePath = Marshal.PtrToStringAuto(pDevicePath);
Marshal.FreeHGlobal(buffer);
index++;
}
有很多方法可以完成这件事,但是我选择了一个最简单的方法:捕获WM_DEVICECHANGE消息。你需要做的仅仅是override 默认的窗体函数,如下:
protected override void WndProc(ref Message m)
{
if (m.Msg == Native.WM_DEVICECHANGE)
{
if (!_loading)
{
LoadItems(); // do the refresh work here
}
}
base.WndProc(ref m);
}
这个和usb移除没有太大关系了,但是我还是想谈一点点这个,因为很多人问过关于它的问题。对于操作系统的卷或者磁盘,通常有至少有两个Serial Numbers:
卷的软件序列号:在格式化过程中被指定。这个32位的值可以很很轻松的通过win32 api GetVolumeInformation 获得。
磁盘的出厂硬件序列号:这个序列号是设置的供应商生产过程中。它是一个字符串。当然,它不能被改变。不幸的是,你必须知道,序列号是可选的,所以USB存储棒可能没有,事实上,很多都没有。
WMI: 到目前为止,它是完成此事的最简单的办法。
下面给出WMI获得Serial Numbers的代码:
// browse all USB WMI physical disks
foreach(ManagementObject drive in new ManagementObjectSearcher(
"select * from Win32_DiskDrive where InterfaceType='USB'").Get())
{
// associate physical disks with partitions
foreach(ManagementObject partition in new ManagementObjectSearcher(
"ASSOCIATORS OF {Win32_DiskDrive.DeviceID='" + drive["DeviceID"]
+ "'} WHERE AssocClass =
Win32_DiskDriveToDiskPartition").Get())
{
Console.WriteLine("Partition=" + partition["Name"]);
// associate partitions with logical disks (drive letter volumes)
foreach(ManagementObject disk in new ManagementObjectSearcher(
"ASSOCIATORS OF {Win32_DiskPartition.DeviceID='"
+ partition["DeviceID"]
+ "'} WHERE AssocClass =
Win32_LogicalDiskToPartition").Get())
{
Console.WriteLine("Disk=" + disk["Name"]);
}
}
// this may display nothing if the physical disk
// does not have a hardware serial number
Console.WriteLine("Serial="
+ new ManagementObject("Win32_PhysicalMedia.Tag='"
+ drive["DeviceID"] + "'")["SerialNumber"]);
}
第一次发表译文,最近一直在研究U盘的插入识别和编程实现移除U盘,网上查了很多资料,最后找到了这篇文章,研究了一下,发现国内这方面的介绍实在太少,因此就有了将它翻译一下的想法,希望给大家一些启发,少走弯路吧。翻译得不好还请大家多多批评指正,但是请勿人身攻击哦。
原文出处:http://www.codeproject.com/KB/system/usbeject.aspx
由于原文代码已经无法下载,我把它整理了一下,用vs2012编译,在win7下运行成功(需要管理员权限),您可以在以下地址获得源代码:http://download.csdn.net/detail/kxloveh/4740293