前言
有一天,我在工作的闲暇之余,被上级被要求做出一个基于ETW的性能监控工具 DEMO。作为一种存在于 Windows 系统十多年的事件记录机制,本以为相关资料遍地开花,随便搜搜便轻松上手。结果却发现ETW的资料可谓是杂而晦涩,入门之路颇为曲折,于是我便做了此文记录 ETW 的基本使用。
ETW 基础
什么是ETW?
ETW,全称 Event Trace For Windows,是微软在Windows中提供的一种高效事件记录机制。简单来讲,可以把它看成一个大型日志系统,能够从程序、内核、驱动收集各种运行信息。
ETW的特性:
-
轻量化:ETW在运行时对系统性能的影响微乎其微,因此可以安全地用于生产环境。
-
实时性:你可以实时捕获和分析事件,而不需要重启系统或应用。
-
灵活性:支持自定义事件,开发者可以根据需要定义自己的事件并收集数据。
ETW的主要用途:
-
性能分析: ETW可以捕获系统和应用程序的性能指标,从而做到优化程序的运行效率。
-
故障排除: 借助ETW,可以通过程序的UI Delay时间、StackWalk等信息来捕获崩溃信息。
-
安全检查: ETW 还能记录一些与安全相关的事件,例如登录尝试、系统资源访问等。
ETW 的核心组件
ETW 的核心组件有三大部分,分别为:
-
Provider(事件提供者):
Provider 是 ETW 的数据生产者。它负责定义并生成事件数据。Provider 可以是 Windows 系统(如内核、驱动程序等),也可以是开发者自定义的应用程序。在实际使用中,每个 Provider 都有一个唯一的标识符(GUID)和一组定义的事件。
-
Controller(控制器):
Controller 主要负责控制 ETW 会话的行为。它的职责包括启动或停止会话、定义跟踪参数,如记录的事件类型、日志大小等。
-
Consumer(事件消费者):
日志数据的消费者,根据情况,接收日志信息的对象。
了解设计模式的同学,可以把它看成一种生产消费模型:
-
Provider 是生产 商品(日志数据) 的工人。
-
Session 是放置商品的 商场(Buffer)
-
Controller 是商场的管理员,负责商品的管理,进货、出货。
-
Consumer 是顾客,负责消费商品,但在购买前需要与 管理员(Controller) 协商。
注意: ETW 支持将Session的数据保存为.etl (EventTraceLog) , 如果要从etl文件读取,则不需要经过session.
利用工具获取Providers与Session信息
上面讲了这么多,终于要使用工具了。
logman
logman 是一款控制台的性能监控器,可以用于创建/开启/搜集会话信息,由于其功能较多,这里只介绍比较常用的,感兴趣可以到微软官网查看。
# 无参数则显示所有提供者
logamn query providers
# 提供参数则显示提供者的事件类型信息
logman query providers "Provider Name"
# 显示出所有实时session会话。
logman -ets
# 显示出session会话中providers的信息
logman -ets "Session Name"
# 如果要对实时会话session进行操作,都需要加上 -ets
性能监控器(Perfmon)
Perfmon 在新版Win10以上,应该是系统自带的工具,可以在GUI界面查看session信息。
ETW 筛选数据
在实际使用时,我们必须对会话进行配置,才能获取到想要的数据。为了方便,这里就从perfmon里讲解,Controller分为两个部分——提供程序与属性。
-
提供程序:
提供程序是日志数据的提供者,一个会话可以拥有多个提供者。
-
属性:
- 关键字(keyword):
用于筛选数据的来源事件,分为any、all匹配,也就是|与 &,一般只使用any匹配。
- 等级(level):
用于筛选日志最低等级。
只有当事件的关键字和等级符合配置的条件时,日志数据才会被输出。
代码实践
Windows 平台使用ETW的方式有两种,一种是使用C++,一种是C#。两种方法各有优缺点,可以根据项目要求而使用合适的。
C++ Version
C++ 是 ETW 的原生支持语言,可以直接调用底层 API 实现高性能的事件捕获和处理。这种方式适合对性能要求极高或需要进行精细控制的场景,但开发复杂度较高。
如果只能选择以C++进行开发,那么我推荐学习PresentMon的控制台项目源码,来学习如何封装底层的ETW。
关键结构
-
EVENT_RECORD:
这是事件记录的核心结构,在事件回调中使用,用于获取事件的元数据、属性等。
-
EVENT_DESCRIPTOR:
嵌套在EVENT_RECORD中,用于获取事件的属性,如version、Task等信息。
-
EVENT_TRACE_PROPERTIES:
用于配置 ETW 会话的属性,包括日志文件模式、缓冲区大小等。
关键函数
-
StartTrace:
用于创建一个 ETW 会话,使用EVENT_TRACE_PROPERTIES配置。
-
EnableTraceEx2:
用于启用提供程序(Provider)并设置其事件级别和关键字筛选。
-
OpenTrace:
打开一个 ETW 会话,用于读取事件。
-
ProcessTrace:
处理已打开的 ETW 会话中的事件。
#include <initguid.h>
#include <cassert>
#include <csignal>
#include <windows.h>
#include <evntrace.h>
#include <evntprov.h>
#include <tdh.h>
#include <evntcons.h>
#include <iostream>
#include <vector>
// 内核会话名称,用于启动和管理 ETW 会话
LPCWSTR SESSION_NAME = KERNEL_LOGGER_NAMEW;
//// 事件GUID
// 内核会话名称,用于启动和管理 ETW 会话
DEFINE_GUID(ProcessGUID,
0x3d6fa8d0, 0xfe05, 0x11d0, 0x9d, 0xda, 0x00, 0xc0, 0x4f, 0xd7, 0xba, 0x7c);
////
struct TraceProperties : public EVENT_TRACE_PROPERTIES // From PresentMon
{
wchar_t SessionName[MAX_PATH];
};
void CALLBACK EventCallback(PEVENT_RECORD event)
{
auto descriptor = event->EventHeader.EventDescriptor;
auto header = event->EventHeader;
auto eventType = descriptor.Opcode;
if (!IsEqualGUID(header.ProviderId, ProcessGUID) ||
(eventType != EVENT_TRACE_TYPE_START &&
eventType != EVENT_TRACE_TYPE_STOP ))
return;
const wchar_t * wProcessPID = L"ProcessId";
const wchar_t * wProcessName = L"ImageFileName";
UINT32 pid = 0;
ULONG bufferSize;
std::string processName;
// 这里使用了debug库来使用数据的读取,如果知道数据的结构,也可以定义结构体来套用userdata
PROPERTY_DATA_DESCRIPTOR desc = {(ULONGLONG)wProcessPID, ULONG_MAX, 0};
TdhGetProperty(event,0, nullptr, 1, &desc, sizeof(UINT32), (PBYTE)&pid);
desc.PropertyName = (ULONGLONG)wProcessName;
TdhGetPropertySize(event, 0, nullptr, 1, &desc, &bufferSize);
processName.resize(bufferSize);
TdhGetProperty(event, 0, nullptr, 1, &desc, bufferSize, (PBYTE)processName.data());
printf("进程%s(%d) %s \n", processName.c_str(),
pid,
eventType == EVENT_TRACE_TYPE_START ? "启动" : "关闭");
fflush(stdout);
}
void CloseSession(const wchar_t* name)
{
TraceProperties traceProperties{};
traceProperties.Wnode.BufferSize = sizeof(traceProperties);
traceProperties.LoggerNameOffset = offsetof(TraceProperties, SessionName);
ControlTraceW(0, name, &traceProperties, EVENT_TRACE_CONTROL_STOP);
}
int main()
{
TRACEHANDLE sessionHandle = NULL;
TRACEHANDLE traceHandle = NULL;
UINT64 anyKey = 0;
UINT8 level = 0;
// 定义会话属性
TraceProperties sessionProperties = {};
signal(SIGINT, [](int sig)
{
CloseSession(SESSION_NAME);
});
sessionProperties.Wnode.BufferSize = (ULONG)sizeof(sessionProperties);
sessionProperties.Wnode.Flags = WNODE_FLAG_TRACED_GUID;
sessionProperties.Wnode.ClientContext = 1; // QPC clock
sessionProperties.Wnode.Guid = SystemTraceControlGuid; // Windows Kernel Trace
sessionProperties.EnableFlags = EVENT_TRACE_FLAG_PROCESS; // 在使用系统事件提供者追踪时使用
sessionProperties.LogFileMode = EVENT_TRACE_REAL_TIME_MODE; // 实时模式
sessionProperties.LoggerNameOffset = offsetof(TraceProperties, SessionName);
// 创建会话信息
ULONG status = StartTraceW(&sessionHandle, SESSION_NAME, &sessionProperties);
if (status != ERROR_SUCCESS) {
std::cerr << "Failed to start trace session. Error: " << status << '\n';
if (status == ERROR_ALREADY_EXISTS)
{
std::cerr << "Session already exists!\n";
CloseSession(SESSION_NAME);
StartTraceW(&sessionHandle, SESSION_NAME, &sessionProperties);
}
else
{
return status;
}
}
// 当使用非SystemProvider时,需要使用该函数来配置事件提供者的属性.
// status = EnableTraceEx2();
// assert(status == ERROR_SUCCESS);
// 配置ETW数据源信息
EVENT_TRACE_LOGFILEW traceProperties = {};
traceProperties.ProcessTraceMode = PROCESS_TRACE_MODE_EVENT_RECORD;
traceProperties.LoggerName = (wchar_t*)SESSION_NAME;
traceProperties.EventRecordCallback = EventCallback;
// 获取ETW会话句柄
traceHandle = OpenTraceW(&traceProperties);
if (traceHandle == INVALID_PROCESSTRACE_HANDLE)
{
std::cerr << "Failed to open trace handle. Error: " << GetLastError();
return ERROR_INVALID_HANDLE;
CloseSession(SESSION_NAME);
return status;
}
// 处理会话
(void)ProcessTrace(&traceHandle, 1, nullptr, nullptr);
return status;
}
C# Version
C# 提供了更高层次的封装,微软的 TraceEvent 库简化了 ETW 的使用,降低了开发难度。它支持动态解析事件,特别适合需要快速上手或对底层性能不敏感的场景。
C# 版本有着一个比较致命的缺点,如果要使用ETW进行底层开发,那么就必须使用C# P/Invoke 对所有系统API进行封装,层层的封装又会让处理速度减慢,导致事件丢失率上升。
using Microsoft.Diagnostics.Tracing;
using Microsoft.Diagnostics.Tracing.Parsers;
using Microsoft.Diagnostics.Tracing.Parsers.Kernel;
using Microsoft.Diagnostics.Tracing.Session;
internal class Program
{
static void Test(TraceEvent data) // 回调
{
Console.WriteLine($"{data.ProviderName}-${data.ProviderGuid}");
Console.WriteLine($"{data.EventName}: {data.ProcessName}");
for (var i = 0; i < data.PayloadNames.Length; i++) // 自动解析userdata的内容
{
Console.WriteLine($"\t{data.PayloadNames[i]}: {data.PayloadValue(i)}");
}
}
public static void Main(string[] args)
{
using var session = new TraceEventSession("EtwTest");
session.EnableKernelProvider(KernelTraceEventParser.Keywords.Process);
session.Source.Kernel.ProcessStart += Test;
Console.CancelKeyPress += (_, e) =>
{
e.Cancel = true;
session.Stop();
};
session.Source.Process();
}
}
总结
在Windows平台上,ETW 的学习资料少也不是不无道理,作为一个日志系统,它过于复杂。在如今第三方日志工具层出不穷的情况下,除非是要进行内核、驱动级别的日志跟踪,否则几乎不会有人使用这个系统。希望后人看到这篇文章,能少走点弯路。
参考资料