createfile更适合绕过.net文件锁和控制缓存,因其可显式指定file_flag_no_buffering等标志,而filestream不支持;需手动closehandle、正确设置共享权限与unicode路径。

为什么直接用 CreateFile 比 FileStream 更适合绕过 .NET 文件锁或控制缓存行为
.NET 的 FileStream 默认启用内核缓冲(FILE_FLAG_SEQUENTIAL_SCAN 隐式生效),且无法直接指定 FILE_FLAG_NO_BUFFERING 或 FILE_FLAG_WRITE_THROUGH。当你需要强制直写磁盘、跳过系统缓存,或以独占/备份方式打开被其他进程锁定的文件(比如正在被记事本编辑的 .log),就必须用 P/Invoke 调用原始 CreateFile。
关键点:
-
CreateFile返回的是 WindowsHANDLE,不是 .NETSafeHandle,必须手动配对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 判定。










