Windows 上 C# 的 FileStream 默认调用 CreateFileW,但在启用异步、使用 SafeFileHandle 或特定标志时会绕过 Win32 层直调 NtCreateFile;File.Copy 对小文件(≤256KB)且同卷 NTFS/ReFS 可触发硬件加速的 copy offload,否则回落传统读写;ReadAsync 真异步需显式指定 FileOptions.Asynchronous,否则伪异步阻塞线程池。

Windows 上 C# 的 FileStream 最终调用的是哪个系统接口?
它走的是 Windows Native API 的 NtCreateFile,不是 CreateFileW。C# 的 FileStream 在底层会通过 Kernel32.CreateFile(即 CreateFileW)封装,但 .NET 运行时(尤其是 CoreCLR 及更高版本)在某些路径下会绕过 Win32 层,直接调用 NT 内核函数 NtCreateFile —— 这取决于是否启用了「fast I/O」路径、文件打开标志(如 FILE_FLAG_NO_BUFFERING)、以及是否使用了 SafeFileHandle 自定义句柄。
-
CreateFileW是 Win32 层入口,做了大量参数校验、安全检查和兼容性适配;NtCreateFile更轻量,但也更“裸”,错误码是NTSTATUS(如STATUS_ACCESS_DENIED),不是 Win32 的ERROR_ACCESS_DENIED - 用
FileStream默认构造时,.NET 通常走CreateFileW;但若传入已打开的SafeFileHandle(比如从 P/Invoke 获取),就可能跳过 Win32 层,直连 NT - 调试时看到堆栈里出现
ntdll!NtCreateFile不代表你手动调用了它——.NET 运行时内部做了这层切换,尤其在高吞吐或低延迟场景下会倾向启用
为什么 File.Copy 有时快得像内存拷贝,有时却卡住十几秒?
因为 Windows 对小文件(通常 ≤ 256 KB)启用了「copy offload」机制,由存储驱动(如 storport 或 NVMe 驱动)在硬件层完成,完全不经过 CPU 和用户内存;而大文件或跨卷复制则回落到传统读-写循环,受缓冲区大小、磁盘队列深度、I/O 调度策略影响极大。
- 同卷复制(如
C:.txt→C:.txt)才可能触发 offload;跨卷(C:→D:)或网络路径(\servershare)一定走传统路径 - 目标卷必须支持
FSCTL_COPY_CHUNK控制码(NTFS、ReFS 支持,exFAT 不支持);源文件还需满足对齐要求(起始偏移和长度需是 4KB 倍数) - 用
Process Monitor观察IRP_MJ_FILE_SYSTEM_CONTROL类型操作,能看到FSCTL_COPY_CHUNK是否被发出;没看到就说明 fallback 到了ReadFile/WriteFile
.NET 的 async 文件 I/O(如 ReadAsync)真的不阻塞线程吗?
在 Windows 上,.NET 的 FileStream.ReadAsync 底层依赖的是「I/O Completion Ports(IOCP)」,属于真正的异步 I/O,不占用线程池线程等待完成;但前提是文件以 FileOptions.Asynchronous 打开 —— 否则即使写了 async,运行时也会退化为同步读+线程池线程阻塞。
- 默认构造
FileStream时不带FileOptions.Asynchronous,此时ReadAsync是「伪异步」:它把同步读包装成 Task,实际仍占一个线程池线程等 I/O 完成 - 必须显式传入:
new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.None, 4096, FileOptions.Asynchronous) - 注意缓冲区大小(第 5 个参数)不能为 0,且建议是 4KB 倍数;否则 IOCP 可能失效,回退到同步模式
- SSD/NVMe 上差异不大,但在 HDD 或高延迟存储(如某些 SMB 共享)上,漏掉
FileOptions.Asynchronous会导致线程池饥饿
从 FileStream 到物理磁盘,中间哪些层可以被绕过或加速?
典型路径是:FileStream → Win32 API → NTOSKRNL (I/O Manager) → Storage Miniport Driver → Physical Device;其中 I/O Manager 和 Miniport 是可干预的关键点,但绕过它们代价极高,且几乎只在内核驱动或特殊存储方案中出现。
-
FILE_FLAG_NO_BUFFERING可绕过系统缓存(即 bypass cache),要求内存地址和传输大小都按扇区对齐(通常是 512B 或 4KB),否则直接报错ERROR_INVALID_PARAMETER -
FILE_FLAG_WRITE_THROUGH禁用设备写缓存,数据真正落盘后才返回,适合日志类场景;但会显著降低吞吐,且部分 SSD 固件可能忽略该标志 - 想跳过 I/O Manager?只能写内核驱动发 IRP;想绕过 miniport?基本不可能——那是厂商实现的硬件抽象层,所有 NVMe/ATA/SATA 指令都经它翻译
- 用户态能做的极限优化,就是控制缓冲策略、对齐方式、IOCP 使用时机,以及避免频繁小 I/O(合并成大块)
真正难的从来不是知道路径有多长,而是哪一层悄悄改了行为——比如某次 Windows Update 升级了 storport.sys,突然让 copy offload 在某个 RAID 卡上失效,而错误日志里只显示 STATUS_NOT_SUPPORTED,连具体哪个驱动拒绝的都不说。










