为什么直接用 CreateFile
比 FileStream
更适合绕过 .NET 文件锁或控制缓存行为
.NET 的
FileStream默认启用内核缓冲(
FILE_FLAG_SEQUENTIAL_SCAN隐式生效),且无法直接指定
FILE_FLAG_NO_BUFFERING或
FILE_FLAG_WRITE_THROUGH。当你需要强制直写磁盘、跳过系统缓存,或以独占/备份方式打开被其他进程锁定的文件(比如正在被记事本编辑的 .log),就必须用 P/Invoke 调用原始
CreateFile。
关键点:
CreateFile返回的是 Windows
HANDLE,不是 .NET
SafeHandle,必须手动配对
CloseHandle,否则资源泄漏 文件路径必须是 Unicode(
lpFileName传
string即可,P/Invoke 自动转 UTF-16) 若要打开正在被其他程序以
FILE_SHARE_DELETE以外方式共享的文件,需显式传
FILE_SHARE_READ | FILE_SHARE_WRITE权限标志(
dwDesiredAccess)不能只传
GENERIC_READ就去写——写操作必须含
GENERIC_WRITE,哪怕你后续只调
WriteFile
CopyFileEx
如何实现带进度回调和可取消的大文件拷贝
.NET 的
File.Copy是阻塞式、无进度、不可中断的。而 Windows 原生
CopyFileEx支持回调函数(
LPPROGRESS_ROUTINE)和取消句柄(
hCancel),适合 UI 场景。
实操要点:
回调函数必须标记为[UnmanagedFunctionPointer(CallingConvention.StdCall)],否则在 x64 下崩溃 回调中不能调用任何托管堆分配操作(如
Console.WriteLine),推荐只更新线程安全变量(如
Interlocked)或发
BeginInvoke到 UI 线程
hCancel是一个手动重置事件(
CreateEvent创建),调用方在需要中止时
SetEvent(hCancel)必须传
COPY_FILE_FAIL_IF_EXISTS等标志位来控制覆盖逻辑,.NET 的
overwrite: true不会自动映射
用 GetFileInformationByHandle
读取 NTFS 文件 ID 和硬链接数
想获取文件唯一标识(比如判断两个路径是否指向同一文件实体)、或检查是否为硬链接/符号链接,
FileInfo完全没提供这些字段。Windows 内核对象的
BY_HANDLE_FILE_INFORMATION结构体包含
nFileIndexLow/High(即 File ID)和
nNumberOfLinks(硬链接计数)。
注意细节:
必须先用CreateFile以
FILE_READ_ATTRIBUTES权限打开句柄,不能用
File.OpenHandle(它返回的是封装过的
SafeFileHandle,不暴露原生 HANDLE)
nFileIndexLow + nFileIndexHigh组合才是完整 128-bit File ID,仅比对 low part 可能冲突
nNumberOfLinks == 1不代表没有硬链接——某些场景(如卷影复制)下该值可能不准,建议结合
GetFinalPathNameByHandle验证 结构体中的
dwVolumeSerialNumber必须与当前卷序列号一致,才能确认 File ID 有效(跨卷移动后 ID 会变)
常见崩溃点:字符串编码、结构体对齐、错误码误判
P/Invoke 调用失败不抛托管异常,而是靠
Marshal.GetLastWin32Error()拿错误码。但这个值极易被中间的托管调用污染(比如日志写入、GC 触发)。
务必做到:
所有 P/Invoke 声明加SetLastError = true,并在调用后**立刻**调用
Marshal.GetLastWin32Error()
StringBuilder传给 API(如
GetFinalPathNameByHandle)前,必须先
.Capacity预设足够空间(NTFS 路径最长 32767 字符),否则缓冲区溢出 自定义结构体(如
BY_HANDLE_FILE_INFORMATION)必须加
[StructLayout(LayoutKind.Sequential, Pack = 4)],否则 x64 下字段偏移错乱 不要用
string接收宽字符输出参数(如
GetVolumePathName),必须用
StringBuilder并指定
CharSet = CharSet.Unicode
最常被忽略的是:
INVALID_HANDLE_VALUE是 -1,但 C# 中
IntPtr的
-1和
(IntPtr)(-1)在某些运行时版本下比较行为不一致,应统一用
handle.ToInt64() == -1判定。
