返回

Windows 如何确定线程运行的 CPU 核心?

windows

如何确定 Windows 线程在哪个 CPU 核心上运行?

遇到了个麻烦事,系统里有个进程把 CPU 核心给跑满了,但又很难直接定位是哪个进程干的。 任务管理器能看到 CPU 核心占用率高, 但因为我的电脑是32核,一个核心被占满也就占用了总体的 3% 左右,很难从进程详细信息里直接揪出元凶。所以, 想写个程序来找出到底是哪个线程霸占着这个 CPU 核心。

问题产生的根源

默认情况下,Windows 线程可以在任何可用的处理器核心上运行。系统会调度线程,让它们在不同的核心之间切换。因此,线程通常没有设置固定的关联性掩码(Affinity Mask),也就是说, 查询得到关联性掩码通常是...FFFFFFF。这就导致很难直接通过关联性掩码确定线程当前在哪个核心上运行。我手头已经有一个线程的句柄(HANDLE) 了, 而且知道用GetCurrentProcessorNumber()可以获取当前线程所在的处理器编号, 可我想要的是根据已有HANDLE 获取任意线程。

解决方案

既然直接获取关联性掩码不行, 我们得换几个思路来解决。下面是几种可行的方案。

方案一:利用 NtQueryInformationThread 获取

这种方法利用了 Windows 的一个 Native API 函数 NtQueryInformationThread,它可以获取线程的各种信息,包括最近一次运行所在的处理器核心。

原理和作用:

NtQueryInformationThread 是一个底层函数,它可以访问内核级别的信息。通过使用 ThreadLastCpuNumber (这是个内部未文档化的信息类别), 我们可以拿到想要的。

代码示例:

#include <windows.h>
#include <winternl.h> // 包含 NtQueryInformationThread 的定义
#include <iostream>
#include <processthreadsapi.h>

typedef NTSTATUS(NTAPI* pNtQueryInformationThread)(
    HANDLE          ThreadHandle,
    THREADINFOCLASS ThreadInformationClass,
    PVOID           ThreadInformation,
    ULONG           ThreadInformationLength,
    PULONG          ReturnLength
);

// 内部未文档化类别定义
#ifndef ThreadLastCpuNumber
#define ThreadLastCpuNumber 24 //这个可能会随 Windows 版本变化
#endif

DWORD GetThreadLastProcessorNumber(HANDLE hThread) {
    static pNtQueryInformationThread NtQueryInformationThread = nullptr;

    if (!NtQueryInformationThread)
        NtQueryInformationThread = (pNtQueryInformationThread)GetProcAddress(GetModuleHandle(TEXT("ntdll.dll")), "NtQueryInformationThread");

    if (!NtQueryInformationThread) {
    	std::cerr << "获取 NtQueryInformationThread 地址失败" << std::endl;
        return -1;
    }
    
	ULONG processorNumber = 0;
    NTSTATUS status = NtQueryInformationThread(
        hThread,
        (THREADINFOCLASS)ThreadLastCpuNumber,  //转换为 THREADINFOCLASS
        &processorNumber,
        sizeof(processorNumber),
        nullptr
    );

    if (NT_SUCCESS(status)) {
        return processorNumber;
    }
    else {
      	std::cerr << "NtQueryInformationThread 调用失败, 状态码: " << std::hex << status << std::endl;
        return -1;
    }
}
int main()
{
	//示例: 获取当前线程 ID
	DWORD threadId = GetCurrentThreadId();
	//打开线程,获取线程句柄,这需要 THREAD_QUERY_INFORMATION 权限.
	HANDLE hThread = OpenThread(THREAD_QUERY_INFORMATION, FALSE, threadId);

    if (hThread == NULL)
    {
        std::cerr << "OpenThread failed." << std::endl;
        return 1;
    }
	DWORD core = GetThreadLastProcessorNumber(hThread);
     if(core != -1){
		std::cout << "Thread " << threadId << " last ran on core: " << core << std::endl;
    }
	CloseHandle(hThread);
}

注意事项:

  • ThreadLastCpuNumber 的值(这里是 24)可能会随着 Windows 版本的变化而变化,所以这个方案有一定的脆弱性。
  • 因为NtQueryInformationThread是一个未文档化的API, 它的行为可能改变或消失。

方案二:周期性采样 + 性能计数器

这种方法比较“稳妥”,通过周期性地采样每个线程的执行时间,结合性能计数器,来推断哪个线程最可能占用了特定的 CPU 核心。

原理和作用:

  1. 周期性采样: 每隔一小段时间(比如几毫秒),获取所有线程的执行时间。
  2. 性能计数器: 使用 Windows 性能计数器,特别是与 CPU 利用率相关的计数器。
  3. 关联分析: 如果某个核心的利用率持续很高,而某个线程的执行时间也持续增长,那么这个线程很可能就在这个核心上运行。

操作步骤:

  1. 选择性能计数器: 可以使用 \Processor Information(_Total)\% Processor Time (所有处理器的总时间百分比,看总体CPU利用率)、\Processor Information(X)\% Processor Time(X 是具体处理器索引,查看单个处理器的使用率),以及线程相关的计数器,比如\Thread(Process/ThreadID)\% Processor Time (进程/线程标识符)。
  2. 编写采样程序:
    • 使用 GetThreadTimes 获取每个线程的内核模式时间、用户模式时间和总运行时间。
    • 使用 PdhCollectQueryData 和相关函数收集性能计数器数据。
    • 周期性地执行采样。
    • 计算两次采样之间每个线程的执行时间差,以及性能计数器的变化。

代码示例(简化框架, 获取线程时间):

#include <windows.h>
#include <iostream>
#include <vector>
#include <thread>
#include <chrono>
#include <iomanip> // 用于输出格式化

struct ThreadTimes {
    DWORD threadId;
    FILETIME creationTime;
    FILETIME exitTime;
    FILETIME kernelTime;
    FILETIME userTime;
};
// 将FILETIME 转换为  ULONGLONG.
ULONGLONG FileTimeToULONGLONG(const FILETIME& ft)
{
    ULARGE_INTEGER uli;
    uli.LowPart = ft.dwLowDateTime;
    uli.HighPart = ft.dwHighDateTime;
    return uli.QuadPart;
}

std::vector<ThreadTimes> GetThreadsTimes() {
    std::vector<ThreadTimes> result;

    DWORD processes[1024], needed, cProcesses;
    if (EnumProcesses(processes, sizeof(processes), &needed))
    {
        cProcesses = needed / sizeof(DWORD);
        for (unsigned int i = 0; i < cProcesses; i++)
        {
            HANDLE hProcess = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, processes[i]);
              if(hProcess == NULL) continue;
            DWORD  threads[1024], cThreads, cbNeeded;
             if (EnumProcessThreads(hProcess, threads, sizeof(threads), &cbNeeded))
              {
                  cThreads = cbNeeded/ sizeof(DWORD);

                  for(unsigned int j = 0; j < cThreads; j++){
                    ThreadTimes tt;
                    tt.threadId = threads[j];

                      HANDLE hThread = OpenThread(THREAD_QUERY_INFORMATION, FALSE, threads[j]);

                      if(hThread != NULL)
                       {
                           if (GetThreadTimes(hThread, &tt.creationTime, &tt.exitTime, &tt.kernelTime, &tt.userTime))
                              {
                                  result.push_back(tt);
                              }
                              CloseHandle(hThread);

                        }
                    }

              }
                CloseHandle(hProcess);
        }

    }
	return result;
}

int main() {

   auto times1 =  GetThreadsTimes();
	std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 采样间隔
   auto times2 = GetThreadsTimes();
    for (const auto& t1 : times1) {
          for (const auto& t2: times2)
           {
                 if(t1.threadId == t2.threadId)
                  {
                        ULONGLONG kernelTimeDiff = FileTimeToULONGLONG(t2.kernelTime) - FileTimeToULONGLONG(t1.kernelTime);
                        ULONGLONG userTimeDiff = FileTimeToULONGLONG(t2.userTime) - FileTimeToULONGLONG(t1.userTime);

                        ULONGLONG totalTimeDiff =  kernelTimeDiff + userTimeDiff;
                      //  这里的时间差是  100 纳秒的倍数,
                      //   如果你想看大概百分比, 还要考虑CPU 核心数,采样间隔时长

                      std::cout << "Thread ID: " << std::setw(6) <<t1.threadId << ", Kernel Time Diff: "<<std::setw(10) <<kernelTimeDiff  << ", User Time Diff: " << std::setw(10) << userTimeDiff<<",  TotalDiff: "<< std::setw(10) << totalTimeDiff <<std::endl;

                  }
            }

     }

     //需要将此处获取的时间差 和  性能监视器的采样数据 结合,得到完整的解决方案

    return 0;
}

性能监视器代码(PDH 示例框架):


#include <windows.h>
#include <iostream>
#include <pdh.h>
#include <pdhmsg.h> // 包含 PDH 错误码

#pragma comment(lib, "pdh.lib")

int main() {
    PDH_HQUERY hQuery;
    PDH_HCOUNTER hCounter;
	//创建查询
    if (PdhOpenQuery(nullptr, 0, &hQuery) != ERROR_SUCCESS) {
        std::cerr << "PdhOpenQuery failed." << std::endl;
        return 1;
    }
		 // 添加计数器  (例如:查看处理器0的 CPU 利用率)
      // 注意:  PDH 计数器路径可能会有所不同
    if (PdhAddEnglishCounter(hQuery, TEXT("\\Processor Information(0)\\% Processor Time"), 0, &hCounter) != ERROR_SUCCESS) {
      std::cerr << "PdhAddEnglishCounter failed." << GetLastError() <<std::endl;

       HRESULT hr =  PdhGetLastError(); //获取错误信息
          if (hr != ERROR_SUCCESS)
          {
         	std::cerr << "PDH Error : " << std::hex << hr <<std::endl;
           }
        PdhCloseQuery(hQuery);

        return 1;
    }

    // 收集一次数据.
     if (PdhCollectQueryData(hQuery) != ERROR_SUCCESS) {
           std::cerr << "PdhCollectQueryData failed." << std::endl;

            PdhCloseQuery(hQuery);
        return 1;
     }

     //获取格式化的计数器数据
    DWORD dwType;
    PDH_FMT_COUNTERVALUE counterValue;
    if(PdhGetFormattedCounterValue(hCounter, PDH_FMT_DOUBLE, &dwType, &counterValue) != ERROR_SUCCESS)
    {
      	 std::cerr << "PdhGetFormattedCounterValue failed." << std::endl;
            PdhCloseQuery(hQuery);
        return 1;
    }
      // 获取数据成功
    std::cout << "CPU Core 0 Utilization: " << counterValue.doubleValue << "%" << std::endl;

     PdhCloseQuery(hQuery);
      return 0;

}

完整集成: 需要把线程时间采样, PDH 性能计数器查询集成到一个程序里, 通过对两次采样之间的 CPU利用率, 线程运行时间增加的综合比较, 推算出"最有可能"的线程。

安全建议:

  • 频繁采样会增加系统开销,要适当调整采样间隔。
  • 对性能计数器的访问可能需要管理员权限。
  • 不要长时间高频地采样,可能会影响系统的整体性能。
    进阶技巧
    可以创建一个性能计数器集合, 一次获取所有CPU核心,所有进程,所有线程的数据。
    这样可以简化采样部分逻辑。

方案三: 使用 ETW (Event Tracing for Windows)

这是一个强大的 Windows 事件跟踪机制,可以详细地记录各种系统事件,包括线程调度事件,从而可以准确地知道线程在哪个核心上运行。

原理和作用:

ETW 通过内核级的事件提供者来记录事件。我们可以使用与线程调度相关的事件(比如 Thread/CSwitch)来跟踪线程在不同核心之间的切换。

操作步骤:

  1. 启动 ETW 会话: 使用工具(如 tracelogxperf)或编程方式启动一个 ETW 会话,并配置要捕获的事件提供者和。 对于线程调度,需要启用 Microsoft-Windows-Kernel-Processor-Power 提供者以及相关的关键字(如 ThreadDispatchReady)。
  2. 分析事件: 使用工具(如 Windows Performance Analyzer (WPA))或编写程序来解析 ETW 产生的事件数据(.etl 文件)。

使用 tracelog (命令行)示例:

:: 启动一个 ETW 会话 (以管理员权限运行)
tracelog -start MySession -f C:\temp\MyTrace.etl -guid #Microsoft-Windows-Kernel-Processor-Power  -flag 0x20 -level 0x4

:: 等待一段时间, 让问题重现......

:: 停止 ETW 会话
tracelog -stop MySession

:: 使用  WPA  或者其他工具分析 MyTrace.etl

需要安装 Windows SDK 以获得 tracelog 。-guid #Microsoft-Windows-Kernel-Processor-Power 指定了 Provider。 -flag 0x20表示收集 Thread Dispatch ready 事件 ,-level表示信息级别.

编程方式 (C++ 框架):

需要用到 evntrace.h 提供的 API.

#include <windows.h>
#include <evntrace.h>
#include <iostream>

#pragma comment(lib, "tdh.lib") //需要链接 tdh.lib

// ...  省略很多  ETW 初始化, 事件处理的代码...
// 核心在于 ProcessEvent 回调中处理 EventRecord 数据
// 需要解析出  EventRecord->EventHeader.ThreadId 和  EventRecord->BufferContext.ProcessorNumber

//启动时
//要启用 Microsoft-Windows-Kernel-Processor-Power 提供者

//示例  事件处理 回调
void WINAPI ProcessEvent(PEVENT_RECORD pEvent)
{
    if (pEvent->EventHeader.ProviderId.Data1 == 0x9e9161a6)  // 根据 Event Header 判断是 线程事件
	  {

        printf("Thread ID: %lu, Processor Number: %u\n",
            pEvent->EventHeader.ThreadId,
            pEvent->BufferContext.ProcessorNumber);

	  }

}

int main() {
  // ... 大量的  ETW 会话 初始化代码

    //处理事件直到会话停止...

  //  ...  清理
 return 0;

}

安全建议:

  • ETW 会话如果配置不当,可能会产生大量的事件数据,占用磁盘空间,并影响系统性能。
  • 需要管理员权限才能启动内核级事件的 ETW 会话。
  • 解析ETW数据通常比较复杂,可能需要对Windows内部机制有一定的了解。

进阶用法

可以使用 Windows Performance Analyzer (WPA), 这提供图形化分析界面。

总结

三种方法各有优劣。第一种方案简单直接,但不一定在所有 Windows 版本上都有效;第二种方案较为通用,但实现起来稍复杂;第三种方案最为强大,但需要对 ETW 有一定了解。选择哪种方案取决于实际的需求和对系统底层知识的掌握程度. 解决这次 CPU 核心占满问题, 可能组合使用效果最好.