工作管理員的「程序」頁籤能顯示每個進程的 CPU 佔用百分比。本文揭示其核心算法:取樣間隔內,目標進程核心+使用者模式時間增量 / 系統總 CPU 時間增量,並提供以 NtQuerySystemInformation(SystemProcessInformation) 非文檔 API 實作的 C/C++ 函數,傳入 PID 即時回傳使用率。
typedef struct _THREAD_INFO {
LARGE_INTEGER CreateTime;
DWORD dwUnknown1, dwStartAddress, StartEIP;
DWORD dwOwnerPID, dwThreadId;
DWORD dwCurrentPriority, dwBasePriority;
DWORD dwContextSwitches, Unknown, WaitReason;
} THREADINFO, *PTHREADINFO;
typedef struct _UNICODE_STRING {
USHORT Length, MaxLength;
PWSTR Buffer;
} UNICODE_STRING;
typedef struct _PROCESS_INFO {
DWORD dwOffset; // 下一個 PROCESS_INFO 相對位移
DWORD dwThreadCount;
DWORD dwUnused1[6];
LARGE_INTEGER CreateTime;
LARGE_INTEGER UserTime; // 使用者模式 CPU 時間 (100ns 單位)
LARGE_INTEGER KernelTime; // 核心模式 CPU 時間
UNICODE_STRING ProcessName;
DWORD dwBasePriority, dwProcessID, dwParentProcessId;
DWORD dwHandleCount, dwUnused3[2];
DWORD dwVirtualBytesPeak, dwVirtualBytes;
ULONG dwPageFaults;
DWORD dwWorkingSetPeak, dwWorkingSet;
DWORD dwQuotaPeakPagedPoolUsage, dwQuotaPagedPoolUsage;
DWORD dwQuotaPeakNonPagedPoolUsage, dwQuotaNonPagedPoolUsage;
DWORD dwPageFileUsage, dwPageFileUsagePeak;
DWORD dCommitCharge;
THREADINFO ThreadSysInfo[1]; // 變長陣列
} PROCESSINFO, *PPROCESSINFO;
NtQuerySystemInformation(5, buffer, size, 0) 取得所有進程鏈表UserTime + KernelTime → TotalProcessCPUUsageUserTime + KernelTime → CurrentProcessCPUUsageTotalDelta = TotalProcessCPUUsage - LastTotalProcessCPUUsage;
CurrentDelta = CurrentProcessCPUUsage - LastCurrentProcessCPUUsage;
100 * CurrentDelta / TotalDelta(TotalDelta > 0)Last* 變數供下次取樣int GetProcessCpuUsage(DWORD dwPID) {
static __int64 LastTotal = 0, LastCurrent = 0;
int cpuUsage = 0;
// 動態取得 NtQuerySystemInformation
typedef LONG (NTAPI *PFN_NtQSI)(ULONG, PVOID, ULONG, PULONG);
static PFN_NtQSI pNtQSI = nullptr;
if (!pNtQSI) {
HMODULE hNtdll = GetModuleHandle(_T("ntdll.dll"));
pNtQSI = (PFN_NtQSI)GetProcAddress(hNtdll, "NtQuerySystemInformation");
if (!pNtQSI) return -1;
}
const DWORD BUFSIZE = 0x20000; // 128KB,視進程數調整
BYTE* pBuf = new BYTE[BUFSIZE];
ULONG retLen = 0;
LONG status = pNtQSI(5, pBuf, BUFSIZE, &retLen); // SystemProcessInformation = 5
if (status != 0) { delete[] pBuf; return -1; }
PPROCESSINFO pInfo = (PPROCESSINFO)pBuf;
__int64 total = 0, current = 0;
while (true) {
__int64 procTime = pInfo->UserTime.QuadPart + pInfo->KernelTime.QuadPart;
total += procTime;
if (pInfo->dwProcessID == dwPID) current += procTime;
if (pInfo->dwOffset == 0) break;
pInfo = (PPROCESSINFO)((BYTE*)pInfo + pInfo->dwOffset);
}
__int64 totalDelta = total - LastTotal;
__int64 curDelta = current - LastCurrent;
if (totalDelta > 0) cpuUsage = (int)(100 * curDelta / totalDelta);
LastTotal = total;
LastCurrent = current;
delete[] pBuf;
return cpuUsage; // 0~100
}
int main() {
DWORD pid = 1234; // 目標 PID,0 表示系統閒置進程
while (true) {
int cpu = GetProcessCpuUsage(pid);
printf("PID %u CPU: %d%%\n", pid, cpu);
Sleep(1000); // 1 秒取樣間隔
}
return 0;
}
| 議題 | 說明 |
|---|---|
| 非文檔 API | NtQuerySystemInformation 微軟保留變更權利,但 Windows 2000~11 穩定相容 |
| 權限 | 需 PROCESS_QUERY_LIMITED_INFORMATION,通常標準使用者即可查詢自身/同會話進程 |
| 緩衝區大小 | 進程數超過 128KB 會返回 STATUS_INFO_LENGTH_MISMATCH,需迴圈加倍重試 |
| 多核心正規化 | 此算法回傳「相對單核心 100%」,如 4 核心滿載單進程可達 400%,如需「相對總核心 100%」再除以邏輯處理器數 |
| PID=0 特殊含義 | 系統閒置進程,CPU 使用率 = 100 - 總使用率 |
| 替代方案 | PDH (PdhAddCounter \Process(%)\% Processor Time)、WMI、GetProcessTimes(需自行排程取樣) |
以 NtQuerySystemInformation 遍歷系統進程資訊塊,雙取樣差分計算 CPU 使用率,是任務管理器、效能監視器、APM 探針的共同基石。雖屬非文檔介面,但在 Windows 生態極為穩定;現代專案建議優先使用 PDH/ETW 文檔化 API 以降低維護風險。