Windows 如何确定线程运行的 CPU 核心?
2025-03-09 16:10:54
如何确定 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 核心。
原理和作用:
- 周期性采样: 每隔一小段时间(比如几毫秒),获取所有线程的执行时间。
- 性能计数器: 使用 Windows 性能计数器,特别是与 CPU 利用率相关的计数器。
- 关联分析: 如果某个核心的利用率持续很高,而某个线程的执行时间也持续增长,那么这个线程很可能就在这个核心上运行。
操作步骤:
- 选择性能计数器: 可以使用
\Processor Information(_Total)\% Processor Time
(所有处理器的总时间百分比,看总体CPU利用率)、\Processor Information(X)\% Processor Time
(X 是具体处理器索引,查看单个处理器的使用率),以及线程相关的计数器,比如\Thread(Process/ThreadID)\% Processor Time
(进程/线程标识符)。 - 编写采样程序:
- 使用
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
)来跟踪线程在不同核心之间的切换。
操作步骤:
- 启动 ETW 会话: 使用工具(如
tracelog
或xperf
)或编程方式启动一个 ETW 会话,并配置要捕获的事件提供者和。 对于线程调度,需要启用Microsoft-Windows-Kernel-Processor-Power
提供者以及相关的关键字(如ThreadDispatchReady
)。 - 分析事件: 使用工具(如 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 核心占满问题, 可能组合使用效果最好.