要识别etw trace中的io模式,须分析readfile/writefile调用的offset与length序列及时间趋势:顺序读表现为offset稳定递增、间隔短且规律;随机写则offset剧烈跳变、length小且无序,同时需结合filestream缓冲行为、memorymappedfile刷盘特征及自定义eventsource打点综合判断。

怎么从 ETW trace 里看出是顺序读还是随机写
靠看 ReadFile 和 WriteFile 的调用本身没用——它们只告诉你“有IO”,不告诉你“怎么读写的”。真正要看的是每次请求的 Offset(文件偏移)和 Length,再结合时间戳和线程上下文连起来看趋势。
实操建议:
- 用
Windows Performance Recorder (WPR)录制时勾选「File I/O」和「Disk I/O」,否则Offset字段为空 - 在
Windows Performance Analyzer (WPA)中打开 trace 后,加列:Stack、FileName、Offset、Length、TimeStamp - 顺序读的典型特征:
Offset值稳定递增(步长 ≈Length或略大),相邻请求时间间隔短且规律;随机写的特征:Offset跳变剧烈(比如上一次是 0x123456,下一次是 0x987654),Length小(常见 4KB/8KB)但位置完全无序 - 注意:.NET 的
FileStream默认开启缓冲,你看到的 trace 可能是缓冲区 flush 引发的大块写,不是原始写意图——要关掉缓冲或结合WriteFileAsync的 native stack 看底层调用
C# 代码里埋点辅助判断 IO 模式
ETW trace 是事后分析,有时需要在代码里加轻量级标记,把业务语义带进 trace。.NET 5+ 支持 EventSource 写入自定义事件,并能和系统 File I/O trace 关联。
实操建议:
- 定义一个
IoPatternEventSource,在关键路径打点,比如:Log.SequentialRead("config.json", offset: 0L, length: 4096) - 打点时带上
Activity.Current?.Id,WPA 中可用Activity ID列将你的事件和对应ReadFile调用对齐 - 避免在 tight loop 里高频打点(比如每 4KB 就 log 一次),会拖慢吞吐;建议按逻辑块(如“加载一个 section”)或采样(每 10 次写入记 1 次)
- 别依赖
DateTime.Now做时间比对——它精度低且不跨进程同步;统一用Stopwatch.GetTimestamp()保证和 ETW 时间轴对齐
FileStream 的 BufferSize 和 FileOptions 如何影响 trace 表现
同一个 Write 逻辑,在不同配置下,trace 里可能显示为 1 次 64KB 写,也可能拆成 16 次 4KB 写——这不是磁盘行为变了,是 FileStream 层的缓冲策略在“伪装”真实模式。
实操建议:
-
BufferSize = 4096且未指定FileOptions.WriteThrough:小写入攒批,trace 中出现大块、低频、高 offset 对齐的写;容易误判为“顺序写”,其实是缓存合并结果 -
FileOptions.NoBuffering:绕过系统缓存,要求Offset和Length都按扇区对齐(通常 512B/4KB),trace 中能看到原始访问粒度,但开发机上容易报ERROR_INVALID_PARAMETER -
FileOptions.SequentialScan:仅提示 OS 预读策略,不影响 trace 中的Offset序列,但可能让后续读请求在 WPA 中显示更少的Hard Faults - 验证方法:用
handle.exe -p YourProcess.exe | findstr .dat查句柄标志,确认是否启用了NO_BUFFERING或WRITE_THROUGH
为什么 Process Monitor 的 Read/Write 分类经常不准
ProcMon 把所有 NtReadFile 标为「Read」,NtWriteFile 标为「Write」,但它不分析 Offset 走势,也不区分预读、写回、内存映射等场景。很多你以为的「随机写」,其实是 MemoryMappedFile 的脏页刷盘,offset 跳变只是虚拟地址映射的结果。
实操建议:
- ProcMon 适合快速定位「哪个文件被谁动了」,不适合做 IO 模式归因;真要分析模式,必须切到 WPA + ETW
- 如果看到大量
Write请求 offset 在 0~1MB 区间反复跳,先查是否用了MemoryMappedFile创建了小视图(mapSize=1MB),而不是真的在随机写文件 - 留意
IRP_MJ_WRITE的调用栈里有没有ntoskrnl.exe!MiFlushSection——这是内存映射写回的标志,和应用层的「随机写逻辑」无关 - 用
dotnet-dump analyze查当前托管堆里是否有MemoryMappedFile实例,比猜更可靠
IO 模式识别真正的难点不在工具链,而在于「同一组 trace 数据,不同上下文解释完全不同」:托管代码的缓冲、OS 的预读/延迟写、存储驱动的条带化,都会重排甚至掩盖原始访问意图。别只盯着 offset 数字,一定要把 FileName、Stack、Process Name 和业务逻辑一起看。








