Windows ETW 入门 【Windows系统编程】

1,366 阅读7分钟

前言

有一天,我在工作的闲暇之余,被上级被要求做出一个基于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分为两个部分——提供程序与属性。

  • 提供程序:

    提供程序是日志数据的提供者,一个会话可以拥有多个提供者。

  • 属性:

    1. 关键字(keyword):

    用于筛选数据的来源事件,分为any、all匹配,也就是|与 &,一般只使用any匹配。

    1. 等级(level):

    用于筛选日志最低等级。

    只有当事件的关键字和等级符合配置的条件时,日志数据才会被输出。

代码实践

Windows 平台使用ETW的方式有两种,一种是使用C++,一种是C#。两种方法各有优缺点,可以根据项目要求而使用合适的。

C++ Version

C++ 是 ETW 的原生支持语言,可以直接调用底层 API 实现高性能的事件捕获和处理。这种方式适合对性能要求极高或需要进行精细控制的场景,但开发复杂度较高。

如果只能选择以C++进行开发,那么我推荐学习PresentMon的控制台项目源码,来学习如何封装底层的ETW。

关键结构

  1. EVENT_RECORD

    这是事件记录的核心结构,在事件回调中使用,用于获取事件的元数据、属性等。

  2. EVENT_DESCRIPTOR

    嵌套在EVENT_RECORD中,用于获取事件的属性,如version、Task等信息。

  3. EVENT_TRACE_PROPERTIES

    用于配置 ETW 会话的属性,包括日志文件模式、缓冲区大小等。

关键函数

  1. StartTrace

    用于创建一个 ETW 会话,使用EVENT_TRACE_PROPERTIES配置。

  2. EnableTraceEx2

    用于启用提供程序(Provider)并设置其事件级别和关键字筛选。

  3. OpenTrace

    打开一个 ETW 会话,用于读取事件。

  4. 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 的学习资料少也不是不无道理,作为一个日志系统,它过于复杂。在如今第三方日志工具层出不穷的情况下,除非是要进行内核、驱动级别的日志跟踪,否则几乎不会有人使用这个系统。希望后人看到这篇文章,能少走点弯路。

参考资料