使用 ETW 检测代码

Windows 事件跟踪 (ETW) 是一个内置于 Windows 中的高速跟踪设施。 使用在操作系统内核中实现的缓冲和日志记录机制,ETW 为用户模式(应用)和内核模式组件(驱动程序)引发的事件提供基础结构。 ETW 可用于系统和应用诊断、故障排除和性能监视。

从历史上看,跟踪用于诊断硬件和应用中的意外行为。 但是,近年来,在管理和监视系统稳定性和性能以满足业务需求方面,需求越来越大。 因此,开发和生产环境中的性能分析已成为计算领域的一个关键部分。 与故障和错误相比,与性能相关的问题很难发现和诊断,因为它们通常取决于配置和工作负载。 生产环境中的跟踪为检测根源性的性能问题,以及容量计划和评估提供了有价值的数据。

借助 ETW 机制,可以动态地控制跟踪会话,从而可以在生产环境中捕获详细的跟踪,无需重新启动系统或重启应用。

下一节演示了如何使用 ETW 进行精确的性能测量和分析:

  • 内核模式驱动程序代码
  • 传统桌面进程和服务
  • Microsoft Store 应用 (C#)

概述

下面的列表显示了 ETW 的一些有益特征:

Robust

它提供了高效的缓冲和日志记录机制。 跟踪缓冲区是由内核管理的。 通过 ETW 进行跟踪不受应用崩溃和挂起的影响。 如果系统发生故障,可以在内存转储文件中访问未保存的事件。

动态

无需重新启动系统或重启应用即可动态启动、停止、重新配置和暂停跟踪会话。 ETW 提供了多种模式,可以满足各种需求。

内置在 Windows 中

除了控制器应用外,不需要其他工具。 Windows 有一些内置的控制器和使用者应用。

轻型

由于历史跟踪和保存的日志文件的开销经过高度优化,它们不会影响应用或系统性能。 日志记录机制使用内核模式缓冲区,这些缓冲区由一个单独的编写器线程写入磁盘,因此跟踪的开销是有限的。

在 Windows 2000 之前,Windows 中仅提供基于文本的基本跟踪机制:DbgPrint() 和 DebugPrint() API。 它们需要调试器,并且通常无法动态控制。 随着时间的推移,Windows 跟踪机制不断发展;现今,有四种不同的跟踪机制可用。 ETW 和事件日志 API 集已合并到 Windows Vista 中的统一事件日志记录 API 集,这为用户和开发人员提供了一个统一的机制来引发事件。

有三种类型的事件:

  1. Windows 软件跟踪预处理器 (WPP) 和经典 ETW

  2. 托管对象格式 (MOF):MOF 是一种描述 WMI 对象以及启用和解码事件的方法。

  3. 基于清单:在 Windows Vista 中引入了一个基于 XML 的统一跟踪定义。 XML 文件包含提供程序写入的事件的元素。 有关详细信息,请参阅编写检测清单

注意

本节中的指南仅侧重于基于清单的事件检测。

ETW 具有以下重要特征:

  • 开发人员可以根据预期用途选择合适的实现集(例如,很容易添加像 WPP 实现这样的 Printf,以用于调试目的的事件)。

  • 基础结构管理常用信息,如时间戳、函数名称和源文件行号。

  • 相同的实现用于用户模式应用和内核模式组件。

  • ETW 可在故障转储和实时调试中进行访问。

  • ETW 可以被重定向到内核调试器以进行实时查看。

  • ETW 具有实时视图。

  • 日志文件保存在二进制日志文件(ETL 文件)中。

  • ETW 支持多进程日志记录。

  • ETW 具有高吞吐量。

  • 可以在另一台计算机上查看日志文件。

  • ETW 支持循环缓冲,以实现持续日志记录和监视。

  • ETW 可以根据目标受众分组到一个通道中。

ETW 体系结构

ETW 中有四个主要组件:提供程序、会话、控制器和使用者。

ETW 体系结构的四个主要组件的示意图

提供程序

提供程序是一个工具化组件,可以生成事件。 提供程序可以是用户模式应用、内核模式驱动程序,或 Windows 内核本身。 除了固定的事件数据(标头)之外,事件还可以携带用户数据。

事件是基于事件的数据表示形式。 数据可用于深度分析。 事件还可用于生成计数器。 计数器 提供基于示例的数据视图。 它们通常包含一小部分数据来显示当前状态,例如每秒 I/O 字节数和每秒中断数。

提供程序必须在 ETW 中注册,并通过调用 ETW 日志记录 API 发送事件。 提供程序为启用和禁用通知注册了一个回调函数,这样就可以动态地启用和禁用跟踪。

会话

ETW 会话基础结构充当中间代理,将事件从一个或多个提供程序中继给使用者。 会话是一个内核对象,它将事件收集到内核缓冲区中,并将其发送到指定的文件或实时使用者进程。 可以将多个提供程序映射到单个会话,让用户能够从多个源收集数据。

控制器

控制器启动、停止或更新跟踪会话。 会话是用于跟踪的单元。 提供程序被映射(或启用)到特定会话。 控制器启用和禁用提供程序,使它们可以开始向 ETW 发送事件。 可以使用 Microsoft 提供的工具调用控制器功能,你也可以编写自己的应用。

Logman.exe 是一个内置的控制器应用。 Windows Performance Toolkit 中的 Windows Performance Recorder (WPR) 是推荐的控制器进程。

使用者

使用者是一个应用,它读取已记录的跟踪文件(ETL 文件),或者实时捕获活动跟踪会话中的事件,并处理事件。 事件查看器和资源监视器都是内置的 ETW 使用者应用。

Windows Performance Toolkit 中的 Windows Performance Analyzer (WPA) 是推荐的使用者进程。

实施 ETW 检测

规划检测

确定在代码中记录 ETW 事件的位置。 此日志记录应与你需要测量、分析和最终改进的重要用户场景或频繁的用例相关联。 下面的列表显示了可检测项目的一些示例:

  • 状态更改
  • 重要操作的开始/结束
  • 资源创建/删除
  • 与性能或可靠性相关的其他事件
  • 调试事件

创建清单文件并实现提供程序

通过使用一个“事件清单”XML 文件,可以在用户模式应用(包括服务)和内核模式组件(如驱动程序)中实现基于清单的 ETW 事件。 有关详细信息,请参阅事件跟踪函数

事件清单分为以下几个部分:

提供程序定义: < 提供程序 >

包含要创建的提供程序的名称和 GUID,以及检测的二进制文件的位置, (最终包含 ETW 框架) 所需的检测资源。

事件有效负载: <模板>

包含将作为有效负载包含在事件中的数据类型的定义。 可用的类型包括:

  • 有符号和无符号 8 位、16 位、32 位和 64 位整数

  • ANSI 和 Unicode 字符串

  • Float 和 double

  • 布尔值、二进制、GUID、指针、FILETIME、SYSTEMTIME、SID 和 HexInt32

静态事件数据
用于帮助解释、排序和分组事件。
  • 定义正在检测的操作 (或任务) 的名称。
  • 定义要为事件创建的操作类型,例如 Start 事件、用于时间分隔操作的 Stop 事件、用于记录调试数据的信息事件等。
  • 事件定义: <事件>

    将有效负载和静态数据联系在一起。 代码将发出此部分所列内容所定义的事件。

下面是事件清单的一个示例:

<provider
    guid="{3877cf22-0702-4dfc-965e-7fdc7780cd74}"
    name="MyEventProvider"
    symbol="MY_EVENT_PROVIDER"
    messageFileName="%temp%\MyProviderBinary.exe"
    resourceFileName="%temp%\MyProviderBinary.exe“
    >
  <templates>
    <template tid="T_MyProvider_1">
      <data inType="win:Int32" name="Operation Id" />
      <data inType="win:Int32" name="Memory Allocated (MB)" />
    </template>
  </templates>
  <opcodes>
    <opcode name="DebugInfo" symbol="_DebugInfo" value="10"/>  
  </opcodes>
  <tasks>
    <task name="OpMemAllocation" symbol="OpMemAllocation_Task" value="1“
          eventGUID="{87ebca33-bf25-442c-9256-82ba484586e8}"/>
  </tasks>
  <events>
    <event symbol="DebugInfo" template="T_MyProvider_1" value="200" 
       task="OpMemAllocation" opcode="DebugInfo" />
  </events>

要编写清单文件,可以使用:

  • 清单生成器 (ECManGen.exe),在 Platform SDK 中提供

  • Visual Studio (Eventman.xsd),在 Platform SDK 中提供

编译事件清单

下一步是使用 Platform SDK 中提供的消息编译器工具 (mc.exe) 来编译清单。 此工具会生成检测、编译和生成已检测代码所需的几个文件:

ManifestFileName.h
包含要在代码中使用的事件描述符。
ManifestFileName.rc
一个资源编译器脚本。
MSG00001.bin
一种语言资源。
ManifestFileNameTEMP.bin
一种模板资源(提供程序和元数据)。

要编译用户模式代码,请键入以下内容:

      mc.exe -um [ManifestFileName]

要编译内核模式代码,请键入以下内容:

      mc.exe -km [ManifestFileName]

要编译托管代码或 JavaScript 代码,请键入以下内容:

      mc.exe -cs [ManifestFileName]
      mc.exe -css [ManifestFileName]
      mc.exe -generateProjections [ManifestFileName]

更新代码

下一步是将检测添加到代码。 运行 Visual Studio,添加消息编译器生成的标头文件,并将资源文件生成到程序中。

在标头中搜索以下内容,查找要在代码中调用的宏(或类方法):

  • EventRegister<YourProviderName>

    用于注册提供程序(在应用启动时)。

  • EventUnregister<YourProviderName>

    用于取消注册提供程序(在应用结束时)。

  • EventWrite

每个事件的一个宏 (或方法) 在清单 (事件>节点) 中<定义。

正确检测代码后,可以生成二进制文件。

对于驱动程序,请查看 MSDN 上提供的 EventDrv 示例。 使用 ETW 内核模式 EtwRegister 函数将驱动程序注册为事件提供程序:

  • 在 DriverEntry 例程中,在创建和初始化设备对象的代码后面添加此函数。

  • 在驱动程序的 Unload 例程中,将对 EtwRegister 函数的调用与对 EtwUnregister 的调用相匹配。

记录并可视化事件

拥有正确检测的组件后,就可以在测试系统上开始记录事件了。 首先需要通过使用内置工具 wevtutil 注册提供程序,准备该系统以进行日志记录。

  1. 将组件复制到由 resourceFileName 属性在清单中指定的位置:

    xcopy /yMyProviderBinary.exe%temp%

  2. 注册提供程序:

    wevtutil umetwmanifest.man
    wetvutil imetwmanifest.man

  3. 验证提供程序是否可见:

    logman query providers 

    提供程序名称/GUID 将显示在列表中。

请注意,事件元数据存储在检测的二进制文件中,而不是存储在清单文件中。 使用 wevtutil 在电脑上安装清单,会将一个链接放入注册表中,将提供程序 GUID 连接到包含事件元数据的二进制文件。 该二进制文件的名称和路径取自提供的清单文件。 之后便可以丢弃清单文件。

由于它位于用于解码的计算机上,因此包含事件元数据的二进制文件也需要可供访问和加载。 WPR/xperf 通过在跟踪中注入元数据,使该过程更具可移植性。

在此系统上正确安装提供程序后,可以启动跟踪会话,以将组件中的事件收集到 ETL 文件中。 可以使用 Windows Performance Recorder (WPR) 或命令行工具 Xperf,两者都可从 Windows Performance Toolkit 中获取:

  1. 开始跟踪:

    xperf-startMySession-onMyEventProvider-fMySession.etl

    在该命令行中,-start 为事件收集会话提供了一个名称,而 -on 告诉 ETW 你希望在此会话中收集来自提供程序的事件。 (可以有多个 -on 参数。)

  2. 执行工作负载。

  3. 停止跟踪:

    xperf-stopMySession

有了 ETL 文件后,便可以使用 Windows Performance Analyzer 工具打开它,并使用通用事件图形和表格可视化事件。

Windows Performance Advisor (WPA) 中通用事件的图形和图表