光之远征团跨服战

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

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

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

问题产生的根源

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

解决方案

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

方案一:利用 NtQueryInformationThread 获取

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

原理和作用:

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

代码示例:

#include

#include // 包含 NtQueryInformationThread 的定义

#include

#include

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

#include

#include

#include

#include

#include // 用于输出格式化

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 GetThreadsTimes() {

std::vector 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) <

}

}

}

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

return 0;

}

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

#include

#include

#include

#include // 包含 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() <

HRESULT hr = PdhGetLastError(); //获取错误信息

if (hr != ERROR_SUCCESS)

{

std::cerr << "PDH Error : " << std::hex << hr <

}

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

#include

#include

#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 核心占满问题, 可能组合使用效果最好.

微波炉加热食物时间表
开一家鞋店要多少钱 鞋店加盟费是多少


最新发表

友情链接