65.9K
CodeProject 正在变化。 阅读更多。
Home

如何准备 U 盘以安全移除

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.90/5 (91投票s)

2006 年 4 月 19 日

CPOL

8分钟阅读

viewsIcon

925471

downloadIcon

11752

显示驱动器盘符、其磁盘编号及其磁盘设备实例之间的链接

引言

使用 Windows 系统托盘图标移除 USB 驱动器很容易,尤其是如果您单击一次,但有时,在程序中执行此操作会很有用。

背景

有一些示例,但我看到的示例是在搜索卷后调用两次 CM_Get_Parent 来获取要弹出(eject)的 USB 设备。这种方法仅适用于声称具有可移动媒体的驱动器。此类驱动器(驱动器类型:DRIVE_REMOVABLE)在 W2K 和 XP 下的处理方式与基本磁盘(DRIVE_FIXED)不同。可移动驱动器在卷和磁盘之间具有一对一的关系,其中磁盘是卷的父设备。CDROM 驱动器也是如此。

不带可移动媒体的 USB 驱动器被当作基本磁盘处理,因此它们可以有多个分区,并且卷的父设备不是磁盘!在 Vista 中,可移动驱动器也是如此,但仍然不允许有多个分区。顺便说一句,USB 磁盘类型还存在更多差异。 这里有一些信息。

存储卷与其磁盘之间的魔鬼链接是设备编号。可以通过调用 DeviceIoControl 并附带 IOCTL_STORAGE_GET_DEVICE_NUMBER 来获取它。此调用的一端用于存储卷的句柄,另一端用于磁盘、软盘和 CDROM 驱动器。

存储卷可能跨越多个磁盘。因此,对于安全移除存储卷,可能需要准备多个磁盘设备进行安全移除。通过 IOCTL_VOLUME_GET_VOLUME_DISK_EXTENTS 获取此类存储卷的设备编号列表。我忽略了这一点,因为我认为在外置驱动器上使用 RAID 是个奇怪的想法。

设备编号仅在一个设备类中是唯一的。处理驱动器盘符时,我们需要区分设备接口 GUID_DEVINTERFACE_DISKGUID_DEVINTERFACE_FLOPPYGUID_DEVINTERFACE_CDROM。直到 2006 年 10 月底,软盘才被考虑在内,因此理论上 USB 软盘会搞砸一切。在实际生活中,USB 软盘的设备编号通常为 0,而任何其他 USB 驱动器的编号都较高,因此没有真正的问题。

顺便说一句:在 W2K 和 XP 中,旧的软盘不属于 GUID_DEVINTERFACE_FLOPPY 枚举。

示例

此示例是我命令行工具 RemoveDrive 的简化版本。它将驱动器盘符作为参数来准备安全移除。它打开卷并获取其设备编号。

// "X:\"    -> for GetDriveType
char szRootPath[] = "X:\\";
szRootPath[0] = DriveLetter;

// "X:"     -> for QueryDosDevice
char szDevicePath[] = "X:";
szDevicePath[0] = DriveLetter;

// "\\.\X:" -> to open the volume
char szVolumeAccessPath[] = "\\\\.\\X:";
szVolumeAccessPath[4] = DriveLetter;

long DeviceNumber = -1;

HANDLE hVolume = CreateFile(szVolumeAccessPath, 0,
                    FILE_SHARE_READ | FILE_SHARE_WRITE,
                    NULL, OPEN_EXISTING, 0, NULL);
if (hVolume == INVALID_HANDLE_VALUE) {
  return 1;
}

STORAGE_DEVICE_NUMBER sdn;
DWORD dwBytesReturned = 0;
long res = DeviceIoControl(hVolume,
                    IOCTL_STORAGE_GET_DEVICE_NUMBER,
                    NULL, 0, &sdn, sizeof(sdn),
                    &dwBytesReturned, NULL);
if ( res ) {
  DeviceNumber = sdn.DeviceNumber;
}
CloseHandle(hVolume);

if ( DeviceNumber == -1 ) {
  return 1;
}

UINT DriveType = GetDriveType(szRootPath);

// get the dos device name (like \device\floppy0)
// to decide if it's a floppy or not
char szKernelName[MAX_PATH];
res = QueryDosDevice(szDevicePath, szKernelName, MAX_PATH);
if ( !res ) {
  return 1;
}

DEVINST DevInst = GetDrivesDevInstByDeviceNumber(DeviceNumber,
                  DriveType, szKernelName);
if ( ! DevInst ) {
  return 1;
}

根据卷的驱动器类型和内核名称,它然后使用设置 API 枚举所有磁盘、软盘或 CD-ROM。将驱动器的设备编号与上面提到的设备编号进行匹配,以获取正确驱动器的设备实例。

//---------------------------------------------------------
DEVINST GetDrivesDevInstByDeviceNumber(long DeviceNumber,
          UINT DriveType, char* szKernelName)
{
  bool IsFloppy = (strstr(szKernelName,
       "\\Floppy") != NULL); // is there a better way?

  GUID* guid;

  switch (DriveType) {
  case DRIVE_REMOVABLE:
    if ( IsFloppy ) {
      guid = (GUID*)&GUID_DEVINTERFACE_FLOPPY;
    } else {
      guid = (GUID*)&GUID_DEVINTERFACE_DISK;
    }
    break;
  case DRIVE_FIXED:
    guid = (GUID*)&GUID_DEVINTERFACE_DISK;
    break;
  case DRIVE_CDROM:
    guid = (GUID*)&GUID_DEVINTERFACE_CDROM;
    break;
  default:
    return 0;
  }

  // Get device interface info set handle
  // for all devices attached to system
  HDEVINFO hDevInfo = SetupDiGetClassDevs(guid, NULL, NULL,
                    DIGCF_PRESENT | DIGCF_DEVICEINTERFACE);

  if ( hDevInfo == INVALID_HANDLE_VALUE )  {
    return 0;
  }

  // Retrieve a context structure for a device interface
  // of a device information set.
  DWORD dwIndex = 0;
  BOOL bRet = FALSE;

  BYTE Buf[1024];
  PSP_DEVICE_INTERFACE_DETAIL_DATA pspdidd =
     (PSP_DEVICE_INTERFACE_DETAIL_DATA)Buf;
  SP_DEVICE_INTERFACE_DATA         spdid;
  SP_DEVINFO_DATA                  spdd;
  DWORD                            dwSize;

  spdid.cbSize = sizeof(spdid);

  while ( true )  {
    bRet = SetupDiEnumDeviceInterfaces(hDevInfo, NULL,
           guid, dwIndex, &spdid);
    if ( !bRet ) {
      break;
    }

    dwSize = 0;
    SetupDiGetDeviceInterfaceDetail(hDevInfo,
      &spdid, NULL, 0, &dwSize, NULL);

    if ( dwSize!=0 && dwSize<=sizeof(Buf) ) {
      pspdidd->cbSize = sizeof(*pspdidd); // 5 Bytes!

      ZeroMemory((PVOID)&spdd, sizeof(spdd));
      spdd.cbSize = sizeof(spdd);

      long res =
        SetupDiGetDeviceInterfaceDetail(hDevInfo, &
                                        spdid, pspdidd,
                                        dwSize, &dwSize,
                                        &spdd);
      if ( res ) {
        HANDLE hDrive = CreateFile(pspdidd->DevicePath,0,
                      FILE_SHARE_READ | FILE_SHARE_WRITE,
                      NULL, OPEN_EXISTING, 0, NULL);
        if ( hDrive != INVALID_HANDLE_VALUE ) {
          STORAGE_DEVICE_NUMBER sdn;
          DWORD dwBytesReturned = 0;
          res = DeviceIoControl(hDrive,
                        IOCTL_STORAGE_GET_DEVICE_NUMBER,
                        NULL, 0, &sdn, sizeof(sdn),
                        &dwBytesReturned, NULL);
          if ( res ) {
            if ( DeviceNumber == (long)sdn.DeviceNumber ) {
              CloseHandle(hDrive);
              SetupDiDestroyDeviceInfoList(hDevInfo);
              return spdd.DevInst;
            }
          }
          CloseHandle(hDrive);
        }
      }
    }
    dwIndex++;
  }

  SetupDiDestroyDeviceInfoList(hDevInfo);

  return 0;
}
//---------------------------------------------------------

磁盘、软盘或 CD-ROM 的父设备是要弹出的 USB 设备。CM_Request_Device_Eject 应用于仅具有 SurpriseRemovalOK 标志的设备。否则,应使用 CM_Query_And_Remove_SubTree。请参阅 MSDN 此处此处

结果是,移除的设备仍然存在,但具有一个通常为 47(CM_PROB_HELD_FOR_EJECT)的问题代码。但如果对 USB 设备(具有 SurpriseRemovalOK 标志)使用 CM_Query_And_Remove_SubTree,则问题代码为 21(CM_PROB_WILL_BE_REMOVED)。这似乎也“安全”,因为 USB 设备的驱动器和卷随后消失了。这里的主要区别在于此类设备可以被重新激活!我已经为此制作了一个工具:RestartSrDev

但是,CM_Query_And_Remove_SubTree 对受限用户不起作用;在这种情况下,它返回 CR_ACCESS_DENIED,而不太推荐使用的 CM_Request_Device_Eject 对受限用户有效。令人惊讶的是,CM_Request_Device_Eject 在服务或 GINA 中不起作用,这里 CM_Query_And_Remove_SubTree 是正确的选择。

使用 CM_Query_And_Remove_SubTree 时,我们必须添加 CM_REMOVE_NO_RESTART 标志,否则刚刚移除的设备可能会被立即重新检测。这发生在 Vista 中,但也报告说在 W2K 和 XP 中有时会发生。它 文档中说明“从 Windows XP 开始”,但该标志在带有 SP4 的 W2K 中也有效(并且是必需的)。

我选择了简单的方式,现在在这个示例中仅使用 CM_Request_Device_Eject

讨论

如果您将其用于 PATA 驱动器,则主驱动器和从属驱动器都会被移除!但是,两者都可以通过 DEVCON RESCAN 恢复。

如果使用 NULL/0 作为 veto 参数调用这些函数,则 XP 会显示“现在安全了”的气球提示,W2K 会显示消息框,Vista 则什么也不显示。正如 McCoy 曾经说过的:“我知道工程师。他们喜欢改变东西。”

我记得我见过 CM_Query_And_Remove_SubTreeCM_Request_Device_Eject 在 XP 上调用失败(vetoed)但仍然返回 CR_SUCCESS。我无法重现,但我确信我看到过,也许是在 XP RTM 或 SP1 下。因此,最好检查函数返回的 veto 值。

在 Windows 2000 中,这两个函数的 ANSI 版本未实现。它们返回 CR_CALL_NOT_IMPLEMENTED,因此我们使用 Unicode 版本。

这两个函数通常需要几秒钟才能返回,因此将它们放入自己的线程中是个好主意。事实上,在 XP 上延迟最多可能达到 30 秒,自 Vista 起最多可能达到 15 秒。这是因为某个进程已注册接收移除请求但未将其返回。为了保持此示例的简单性,没有为调用 CM_Request_Device_Eject 创建额外的线程。

ULONG Status = 0;
ULONG ProblemNumber = 0;
PNP_VETO_TYPE VetoType = PNP_VetoTypeUnknown;
WCHAR VetoNameW[MAX_PATH];
bool bSuccess = false;

// get drives's parent, e.g. the USB bridge,
// the SATA port, an IDE channel with two drives!
DEVINST DevInstParent = 0;
res = CM_Get_Parent(&DevInstParent, DevInst, 0);

for ( long tries=1; tries>=3; tries++ ) {
// sometimes we need some tries...

  VetoNameW[0] = 0;

  res = CM_Request_Device_EjectW(DevInstParent,
          &VetoType, VetoNameW, MAX_PATH, 0);

  bSuccess = ( res==CR_SUCCESS &&
                    VetoType==PNP_VetoTypeUnknown );
  if ( bSuccess )  {
    break;
  }

  Sleep(500); // required to give the next tries a chance!
}

通常会发现移除第一次尝试失败,但第二次尝试成功。因此,我只尝试三次。

什么导致移除失败

只要有对磁盘或存储卷的打开句柄,安全移除的准备就会失败。当然,您无法从要移除的驱动器运行此 EXE。要做到这一点,您需要另一个驱动器上的临时副本。ProcessExplorer 非常适合发现哪个进程持有驱动器的打开句柄。按 Ctrl+F 并输入驱动器盘符,例如 U:。通常它无法解析驱动器盘符,因此您需要搜索驱动器的内核名称。它应该类似于 \Device\Harddisk4\DP(1)0-0+11。一个重要的部分,例如 'disk4',通常就足够了。但有时,即使是驱动器驱动的 ProcessExplorer 也无法找到讨厌的句柄。

安全移除后重新激活 USB 驱动器

当准备安全移除并具有问题代码 47(CM_PROB_HELD_FOR_EJECT)时,USB 设备无法重新激活。唯一的解决办法是停用并然后重新激活连接到它的 USB 集线器。这适用于标准集线器和根集线器。当然,这会循环使用连接到该集线器的所有 USB 设备。

演示项目

演示项目是用 VS6 创建的。它需要 Microsoft Windows Platform SDK 和 DDK/WDK 的 LIB 和头文件。如果您使用的是较新的 Visual Studio,只需尝试编译它。如果它抱怨缺少包含文件或 LIB 文件,只需获取最新的 SDK。如果仍有内容缺失,它可能随 Windows Driver Kit (WDK) 一起提供:此处

使用 Visual Studio 6.0

许多用户仍然喜欢使用 VS6,我也是。这是因为它轻巧、快速,并且安装迅速。事实上,对于简单的 Win32 应用程序,复制其文件夹并导入其注册表设置就足够了。缺点是编译器老旧,缺少 x64 支持,并且与新的 SDK 和 DDK/WDK 不兼容。

与 VS6 集成并完美工作的最新 SDK 版本是 2003 年 2 月的版本。它称为“Platform SDK for Windows Server 2003”(2003 年 2 月版,Build 3790.0),仍然可以在 Microsoft 下载,请参阅 此处。不幸的是,cfg.hcfgmgr32.lib 没有包含在此 SDK 中,甚至 cfgmgr32.h 也包含 cfg.hcfg.h 和也缺失的 cfgmgr32.lib 在 Windows DDK 中找到。都可以从任何 DDK 或 WDK 中使用,即使是新版本,例如来自 WDK Build 6000。构建更复杂的项目时,您会遇到麻烦,因为来自较新 SDK 和 DDK/WDK 的其他头文件和库文件将无法与 VS6 一起使用!可下载的最旧 DDK 是“Windows Server 2003 SP1 DDK”(Build 3790.1830):1830_usa_ddk.iso。大多数文件与 VS6 兼容,但编译调试版本时,某些库文件(例如 uuid.lib)不兼容。uuid.lib 没问题,因为 VS6 自带了一个兼容的版本。在这种情况下,只需重命名新文件,使 VS6 使用其自己的 LIB 文件夹中的旧文件。VS6 的正确 DDK 是“Windows XP SP1 DDK”(Build 2600.1106),但这在 Microsoft 上不可下载。

如果您不想为了单个文件下载整个 DDK,那么您可以使用来自 ReactOS 项目CFG.h,它至少与此演示兼容。只需将下载的 cfg.h 放到 cfgmgr32.h 所在的文件夹中。

SDK+DDK 头文件和库文件的集成通常是通过手动将其添加到includelib文件夹列表来完成的。

来自德语 VS6 的屏幕截图:IncludesLibs

历史

  • 2007 年 1 月 15 日 - 更新下载
  • 2007 年 1 月 27 日 - 更新了文章和下载;不再使用 CM_Query_And_Remove_SubTree
  • 2007 年 5 月 16 日 - 更新了文章和下载;删除了 SetupDiEnumInterfaceDevice,因为它只是 SetupDiEnumDeviceInterfaces 的一个宏定义,而后者在前面几行已被调用
  • 2007 年 11 月 7 日 - 修正了关于使用 VS6 所需 SDK 的提示
  • 2009 年 1 月 21 日 - 文章的一些小改动
  • 2009 年 3 月 30 日 - 修正了关于使用 VS6 所需 SDK 和 DDK 的提示
  • 2010 年 2 月 15 日 - 文章的一些小改动
  • 2010 年 5 月 13 日 - 文章的一些小改动
  • 2010 年 5 月 16 日 - 添加了一些关于结果设备问题代码的信息
  • 2011 年 4 月 6 日 - 更新了旧 SDK 和 DDK 的链接
© . All rights reserved.