在现代计算机系统中,经常需要安全地移除存储介质,如硬盘、USB驱动器、ZIP/JAZ驱动器、读卡器等。这些设备支持热插拔,但为了确保数据的完整性和系统的稳定性,需要按照一定的步骤来操作。本文将介绍如何安全地移除这些存储介质。
移除存储介质通常是通过调用DeviceIoControl
函数并使用IOCTL_STORAGE_EJECT_MEDIA
命令来实现的。然而,直接使用这种方法可能会造成数据丢失,因为它甚至在驱动器上有打开的文件时也会执行弹出操作。因此,更好的做法是在移除之前先清空文件缓存、锁定和卸载文件系统。
这里介绍的是一个简化版的命令行工具EjectMedia
。它需要传入要移除的驱动器字母作为参数。对于非CD/DVD驱动器,第一步是通过FlushFileBuffers
API调用来清空文件缓存。这需要一个具有写入权限的句柄。因此,为了清空整个存储卷,需要以写入权限打开它,这可能会因为受限用户而失败:
char szVolumeAccessPath[] = "\\\\.\\X:";
szVolumeAccessPath[4] = DriveLetter;
HANDLE hVolWrite = CreateFile(szVolumeAccessPath,
GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE,
NULL, OPEN_EXISTING, 0, NULL);
if (hVolWrite != INVALID_HANDLE_VALUE) {
FlushFileBuffers(hVolWrite);
CloseHandle(hVolWrite);
}
接下来,尝试锁定和卸载。这里只需要读取权限,即使是受限用户也可以获得。如果该卷上没有打开的文件句柄,锁定就会成功。即使锁定失败,卸载也可以成功,并且卸载后所有打开的文件句柄仍然打开但无效,任何尝试使用它们进行读写操作都会导致ERROR_NOT_READY
错误。因此,即使锁定失败,也可以选择卸载,这就是'Force'布尔值的含义:
bool Force = false;
HANDLE hVolRead = CreateFile(szVolumeAccessPath,
GENERIC_READ,
FILE_SHARE_READ | FILE_SHARE_WRITE,
NULL, OPEN_EXISTING, 0, NULL);
if (hVolRead != INVALID_HANDLE_VALUE) {
int Locked = DeviceIoControl(hVolRead, FSCTL_LOCK_VOLUME, NULL, 0, NULL, 0, &dwRet, NULL);
if (Locked || Force) {
res = DeviceIoControl(hVolRead, FSCTL_DISMOUNT_VOLUME, NULL, 0, NULL, 0, &dwRet, NULL);
}
}
最后,执行弹出操作:
res = DeviceIoControl(hVolRead, IOCTL_STORAGE_EJECT_MEDIA, NULL, 0, NULL, 0, &dwRet, NULL);
由于采取友好的方式,如果获得了锁定,会释放它:
if (Locked) {
DeviceIoControl(hVolRead, FSCTL_UNLOCK_VOLUME, NULL, 0, NULL, 0, &dwRet, NULL);
}
顺便说一句,弹出的相反操作是加载介质。这真的只对CD/DVD驱动器有效:
res = DeviceIoControl(hVolRead, IOCTL_STORAGE_LOAD_MEDIA, NULL, 0, NULL, 0, &dwRet, NULL);
为什么没有检查驱动器类型DRIVE_CDROM
和DRIVE_REMOVABLE
?
Windows在这里不关心驱动器类型。弹出请求被传递到驱动器的驱动程序,然后传递到硬件。最终是由硬件给出答案。有些USB硬盘支持弹出。见过安全移除失败但弹出成功的案例。
什么是卸载?
卸载意味着将文件系统从存储卷上分离。这保证了在另一个驱动程序(例如,在系统休眠时,从可启动CD启动)挂载它之前,文件系统不会被保持在不一致的状态,这很重要。
这和删除驱动器字母(mountvol X: /D)是一样的吗?
不,绝对不是!/D不代表卸载,它代表删除挂载点。它只做这个。没有清空,没有卸载,没有句柄被关闭或使无效。
如何重新挂载一个卷?
如果锁定被释放,那么尝试打开卷上的一个文件。Windows会自动挂载该卷。在卸载之前打开的句柄仍然无法用于读写,它们永久性地生成ERROR_NOT_READY
,即使卷再次被挂载。
为什么锁定有时会花费一些时间?
锁定似乎也会清空文件缓存,这可能需要一点时间。此外,应用程序可以注册自定义通知(RegisterDeviceNotification
),然后DBT_CUSTOMEVENT
会带有GUID_IO_VOLUME_LOCK
。FSCTL_LOCK_VOLUME
不会在所有注册了该卷自定义通知的应用程序处理完消息之前返回。