使用DeviceIoControl时为何出现文件访问错误?
在Windows系统中递归遍历文件夹结构,获取每个文件和文件夹的唯一文件系统标识符。针对文件夹,使用WinAPI的CreateFile()和DeviceIoControl()函数获取ID,但部分有效路径(可在资源管理器浏览,System.IO.Path.Exists()确认存在且可访问)调用DeviceIoControl()时返回错误码2(ERROR_FILE_NOT_FOUND)。已排除权限问题,且错误为“文件未找到”而非“路径未找到”(错误码3),System.IO.File.Exists()能正确区分文件和文件夹,部分路径调用DeviceIoControl()可正常返回结果。
需明确:DeviceIoControl()对哪些因素敏感,为何会在有效路径上出错?
环境信息
- Windows 10
- Visual Studio Express 2022
- .NET 8.0
WinAPI调用定义
Imports System.Runtime.InteropServices Imports Microsoft.Win32.SafeHandles Public Class WinAPI Public Shared GENERIC_READ As Integer = &H80000000 Public Shared FILE_FLAG_BACKUP_SEMANTICS As Integer = &H2000000 Public Shared OPEN_EXISTING As Integer = &H3 <StructLayout(LayoutKind.Sequential)> Public Structure FILE_OBJECTID_BUFFER Public Structure Union <MarshalAs(UnmanagedType.ByValArray, SizeConst:=16)> Public BirthVolumeID() As Byte <MarshalAs(UnmanagedType.ByValArray, SizeConst:=16)> Public BirthObjectId As Byte() <MarshalAs(UnmanagedType.ByValArray, SizeConst:=16)> Public DomainID As Byte() End Structure <MarshalAs(UnmanagedType.ByValArray, SizeConst:=16)> Public ObjectID As Byte() Public BirthInfo As Union '<MarshalAs(UnmanagedType.ByValArray, SizeConst:=48)> 'Public ExtendedInfo As Byte() End Structure Public Structure FILETIME Public dwLowDateTime As UInteger Public dwHighDateTime As UInteger End Structure <StructLayout(LayoutKind.Sequential)> Public Structure BY_HANDLE_FILE_INFORMATION Public FileAttributes As UInteger Public CreationTime As FILETIME Public LastAccessTime As FILETIME Public LastWriteTime As FILETIME Public VolumeSerialNumber As UInteger Public FileSizeHigh As UInteger Public FileSizeLow As UInteger Public NumberOfLinks As UInteger Public FileIndexHigh As UInteger Public FileIndexLow As UInteger End Structure <DllImport("kernel32.dll", SetLastError:=True)> Public Shared Function DeviceIoControl( hDevice As SafeFileHandle, dwIoControlCode As UInteger, lpInBuffer As IntPtr, nInBufferSize As UInteger, lpOutBuffer As IntPtr, nOutBufferSize As Integer, ByRef lpBytesReturned As UInteger, lpOverlapped As IntPtr ) As Boolean End Function <DllImport("kernel32.dll", SetLastError:=True)> Public Shared Function CreateFile( fileName As String, dwDesiredAccess As Integer, dwShareMode As System.IO.FileShare, securityAttrs_MustBeZero As IntPtr, dwCreationDisposition As System.IO.FileMode, dwFlagsAndAttributes As Integer, hTemplateFile_MustBeZero As IntPtr ) As SafeFileHandle End Function <DllImport("kernel32.dll", SetLastError:=True)> Public Shared Function GetFileInformationByHandle( hFile As IntPtr, ByRef lpFileInformation As BY_HANDLE_FILE_INFORMATION) As Boolean End Function End Class
实现函数代码
Imports System.Runtime.InteropServices Imports Microsoft.Win32.SafeHandles Public Class FileAccess Shared FSCTL_GET_OBJECT_ID As UInteger = &H9009C Public Shared Function GetFileId(path As String) As UInt128 Using fs = IO.File.Open( path, IO.FileMode.OpenOrCreate, IO.FileAccess.ReadWrite, IO.FileShare.ReadWrite) Dim info As WinAPI.BY_HANDLE_FILE_INFORMATION WinAPI.GetFileInformationByHandle(fs.Handle, info) Dim fileID As UInt128 = info.FileIndexHigh fileID <<= 32 fileID = fileID Or info.FileIndexLow Return fileID 'Return String.Format("{0:x}", ((info.FileIndexHigh << 32) Or info.FileIndexLow)) End Using End Function Public Shared Function GetFolderID(path As String) As UInt128 Dim result As UInt128 Dim buffer As WinAPI.FILE_OBJECTID_BUFFER = GetFolderIdBuffer(path) result = BitConverter.GetUint128(buffer.ObjectID) Return result End Function Private Shared Function GetFolderIdBuffer(path As String) As WinAPI.FILE_OBJECTID_BUFFER Using hFile As SafeFileHandle = WinAPI.CreateFile( path, WinAPI.GENERIC_READ, IO.FileShare.Read, IntPtr.Zero, WinAPI.OPEN_EXISTING, WinAPI.FILE_FLAG_BACKUP_SEMANTICS, IntPtr.Zero ) If hFile Is Nothing Or hFile.IsInvalid Then Throw New System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error()) Dim buffer As New WinAPI.FILE_OBJECTID_BUFFER Dim nOutBufferSize As Integer = Marshal.SizeOf(buffer) Dim lpOutBuffer As IntPtr = Marshal.AllocHGlobal(nOutBufferSize) Dim lpBytesReturned As UInteger Dim result As Boolean = WinAPI.DeviceIoControl( hFile, FSCTL_GET_OBJECT_ID, IntPtr.Zero, 0, lpOutBuffer, nOutBufferSize, lpBytesReturned, IntPtr.Zero ) If Not result Then Throw New System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error()) Dim type As Type = GetType(WinAPI.FILE_OBJECTID_BUFFER) buffer = Marshal.PtrToStructure(lpOutBuffer, type) Marshal.FreeHGlobal(lpOutBuffer) Return buffer End Using End Function End Class
调用代码
Dim folderID As UInt128 = FileAccess.GetFolderID(path)
DeviceIoControl()返回ERROR_FILE_NOT_FOUND的核心原因
你使用的FSCTL_GET_OBJECT_ID控制码存在严格的适用限制,以下是关键敏感因素:
文件系统兼容性限制
FSCTL_GET_OBJECT_ID仅支持NTFS文件系统。如果目标文件夹位于FAT32、exFAT等非NTFS分区,即使CreateFile()成功打开文件夹,DeviceIoControl()也会返回ERROR_FILE_NOT_FOUND——这些文件系统根本不支持Object ID特性。Object ID未被系统分配
NTFS不会自动为所有文件夹分配Object ID,仅当系统或特定应用(如索引服务、备份软件)需要时才会生成。从未被此类功能访问过的文件夹没有Object ID,此时调用FSCTL_GET_OBJECT_ID会触发文件未找到错误。结构封送的潜在问题
你的FILE_OBJECTID_BUFFER注释掉了ExtendedInfo字段,导致Marshal.SizeOf(buffer)计算的缓冲区大小不足。完整的FILE_OBJECTID_BUFFER应为64字节,缺失字段会引发缓冲区不匹配,间接导致错误。特殊路径场景
如果路径指向符号链接或 junction点,CreateFile()打开的是链接本身而非目标文件夹,FSCTL_GET_OBJECT_ID无法识别这种间接对象,从而返回错误。
修正方案
- 前置检查文件系统:调用前通过
DriveInfo.DriveFormat判断分区是否为NTFS,跳过不支持的分区。 - 改用通用ID获取方式:使用
GetFileInformationByHandle()返回的FileIndexHigh+FileIndexLow组合作为标识符,该方式适用于所有Windows支持的文件系统,无需依赖Object ID。 - 修复结构定义:恢复
FILE_OBJECTID_BUFFER中的ExtendedInfo字段,确保缓冲区大小正确。 - 处理特殊路径:对符号链接/junction点,调用
GetFinalPathNameByHandle()解析实际路径后再操作。
内容的提问来源于stack exchange,提问作者J Collins

