程式範例 - 快速列出 Windows 執行中程式 CPU%、記憶體用量與執行身分
4 | 16,245 |
規劃系統時自己搞出一個冷門需求,打算比照 Windows Task Manager (工作管理員)列舉執行中程式名稱、CPU 使用率、記憶體用量,以便透過 API 方式偵錯遠端系統問題(例如:檢查 IIS 的 w3wp 是否 CPU 滿載或接近 2GB 記憶體上限)。要達到此一目標,我打算取得所有執行中應用程式的名稱、執行帳號(主要用於識別 IIS AppPool)、CPU 使用率以及記憶體用量(Private Working Set):(下圖黃色欄位)
動手前先做了功課。首先,發現一個好用的命令列工具:tasklist
呼叫外部程式截取其執行結果是很省事的解法,tasklist 的輸出結果乍看可滿足程式名稱、執行帳號及記憶體用量三項資訊,只缺 CPU 使用率。細究後卻發現,它的記憶體數字是 Working Set,與我們在工作管理員常用的 Private Working Set 不同,Working Set 是實體記憶體用量沒錯,但包含多程式共用部分。例如: 程序 A、B、C 的 Working Set 各為 100 MB、200 MB 及 300 MB,三者的 Working Set 均有 50MB 屬共用部分,故三支程式實際耗用的實體記憶體為 500 MB ,而非 600 MB,因此 Private Working Set 較能突顯因為此程式耗用的記憶體。延伸閱讀:CyberNotes: Windows Memory Usage Explained
評估後決定用 .NET 程式自幹。經爬文,Process.GetProcesses() 可取得系統所有執行中程序,而 C# 抓 CPU% 及記憶體用量多半是透過 .NET PerformanceCounter 類別取得(延伸閱讀:程式範例-使用 C# 查詢 CPU 與記憶體使用狀況),抓 Process 執行身分則可靠 WMI 的 GetOwner(),所有的資訊都有管道可取。
建立 PerformanceCounter 時有個眉角,new PerformanceCounter(類別, 計數器名稱, 執行個體名稱)中的執行個體名稱(Instance Name),遇同一程式執行多份時,PerformanceCounter 執行個體名稱會以類似 chrome、chrome#1、chrome#2... 的形式自動編號,只能經由列舉比對找出 Process 對應的執行個體名稱。關於比對技巧,可參考 MVP Rick Capturing Performance Counter Data for a Process by Process Id一文。
將查到的做法組裝起來,我寫出第一個版本:
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Management;
using System.Text;
using System.Threading.Tasks;
namespace MyTaskMgr
{
public class ProcInfoModule
{
public static List<ProcInfoDto> QueryProcesses()
{
var sw = new Stopwatch();
sw.Start();
var list = Process.GetProcesses();
var res = list.AsParallel().Select(o =>
{
var p = new ProcInfoDto()
{
ProcessId = o.Id,
Name = o.ProcessName,
};
var pcs = GetProcCountersForProcessId(o.Id, "% Processor Time,Working Set - Private".Split(','));
if (pcs != null)
{
p.ProcTimeInPerc = $"{pcs[0].NextValue() / Environment.ProcessorCount}%";
p.PrivWorkSet = $"{pcs[1].NextValue() / 1024:n0}K";
}
return p;
}).ToList();
sw.Stop();
Console.WriteLine($"Get CPU & Memory Usage {sw.ElapsedMilliseconds:n0}ms");
sw.Restart();
res.AsParallel().ForAll(o => { o.UserName = GetProcOwnerName(o.ProcessId); });
sw.Stop();
Console.WriteLine($"Get Proc Owner {sw.ElapsedMilliseconds:n0}ms");
return res;
}
//REF: https://dotblogs.com.tw/larrynung/2013/03/11/96137
static string GetProcOwnerName(int pid)
{
var query = "Select * From Win32_Process Where ProcessId = " + pid;
var searcher = new ManagementObjectSearcher(query);
var mgmtObj = searcher.Get().Cast<ManagementObject>().FirstOrDefault();
if (mgmtObj != null)
{
var argList = new string[2];
if (Convert.ToInt32(mgmtObj.InvokeMethod("GetOwner", argList)) == 0)
return string.Join(@"\", argList.Reverse().ToArray());
}
return null;
}
//REF: https://weblog.west-wind.com/posts/2014/Sep/27/Capturing-Performance-Counter-Data-for-a-Process-by-Process-Id
public static PerformanceCounter[] GetProcCountersForProcessId(int processId, string[] processCounterNames)
{
string instance = GetInstanceNameForProcessId(processId);
if (string.IsNullOrEmpty(instance))
return null;
return processCounterNames.Select(o => new PerformanceCounter("Process", o, instance)).ToArray();
}
public static string GetInstanceNameForProcessId(int processId)
{
try
{
var process = Process.GetProcessById(processId);
string processName = Path.GetFileNameWithoutExtension(process.ProcessName);
PerformanceCounterCategory cat = new PerformanceCounterCategory("Process");
string[] instances = cat.GetInstanceNames()
.Where(inst => inst.StartsWith(processName))
.ToArray();
foreach (string instance in instances)
{
using (PerformanceCounter cnt = new PerformanceCounter("Process",
"ID Process", instance, true))
{
int val = (int) cnt.RawValue;
if (val == processId)
{
return instance;
}
}
}
}
catch
{
//ignore
}
return null;
}
}
}
測試程式如下,先取得程序列表,印出個數,從中挑出 IIS (w3wp),印出其執行帳號、CPU% 及記憶體用量。(註:程式需以管理者權限執行,否則查 GetOwner() 可能出現存取被拒)
var res = ProcInfoModule.QueryProcesses();
Console.WriteLine(res.Count);
var iis = res.SingleOrDefault(o => o.Name == "w3wp");
Console.WriteLine($"{iis.UserName},{iis.ProcTimeInPerc},{iis.PrivWorkSet}");
Console.Read();
第一版拼裝車可行,但耗時快三分鐘才跑完,慢到令人抓狂。
Get CPU & Memory Usage 146,312ms
Get Proc Owner 25,890ms
356
IIS APPPOOL\\MyWeb,0%,163,268K
分析問題出在每次由 Process ID 重新掃瞄比對 Instance Name 的做法太沒效率,於是先做一次簡單重構,將列舉結果一次轉成 Dictionary<int, string>,後面改用查表,應能大幅改善效能。
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Management;
using System.Text;
using System.Threading.Tasks;
namespace MyTaskMgr
{
public class ProcInfoModule2
{
public static List<ProcInfoDto> QueryProcesses()
{
var sw = new Stopwatch();
sw.Start();
BuildInstanceNameDictionary();
sw.Stop();
Console.WriteLine($"BuildInstanceNameDictionary {sw.ElapsedMilliseconds:n0}ms");
sw.Restart();
var list = Process.GetProcesses();
var res = list.AsParallel().Select(o =>
{
var p = new ProcInfoDto()
{
ProcessId = o.Id,
Name = o.ProcessName,
};
var pcs = GetProcCountersForProcessId(o.Id, "% Processor Time,Working Set - Private".Split(','));
if (pcs != null)
{
p.ProcTimeInPerc = $"{pcs[0].NextValue() / Environment.ProcessorCount}%";
p.PrivWorkSet = $"{pcs[1].NextValue() / 1024:n0}K";
}
return p;
}).ToList();
sw.Stop();
Console.WriteLine($"Get CPU & Memory Usage {sw.ElapsedMilliseconds:n0}ms");
sw.Restart();
res.AsParallel().ForAll(o => { o.UserName = GetProcOwnerName(o.ProcessId); });
sw.Stop();
Console.WriteLine($"Get Proc Owner {sw.ElapsedMilliseconds:n0}ms");
return res;
}
//REF: https://dotblogs.com.tw/larrynung/2013/03/11/96137
static string GetProcOwnerName(int pid)
{
//...省略...
}
//REF: https://weblog.west-wind.com/posts/2014/Sep/27/Capturing-Performance-Counter-Data-for-a-Process-by-Process-Id
public static PerformanceCounter[] GetProcCountersForProcessId(int processId, string[] processCounterNames)
{
//...省略...
}
static Dictionary<int, string> PidToInstanceNameDictionary = new Dictionary<int, string>();
static void BuildInstanceNameDictionary()
{
var proc = Process.GetProcesses().ToDictionary(o => o.Id, o => string.Empty);
PerformanceCounterCategory cat = new PerformanceCounterCategory("Process");
cat.GetInstanceNames()
.AsParallel()
.ForAll(n =>
{
try
{
var pid = (int) new PerformanceCounter(
"Process", "ID Process", n, true).RawValue;
if (proc.ContainsKey(pid)) proc[pid] = n;
}
catch
{
//ignore
}
});
PidToInstanceNameDictionary = proc;
}
public static string GetInstanceNameForProcessId(int processId)
{
if (PidToInstanceNameDictionary.ContainsKey(processId))
return PidToInstanceNameDictionary[processId];
return null;
}
}
}
重構版本果然大有起色,花 10 秒建立對照表,花 15 秒測量 353 個 Process 的 CPU% 及記憶體,再花 25 秒走 WMI 查出 Process 執行身分。
BuildInstanceNameDictionary 10,345ms
Get CPU & Memory Usage 15,821ms
Get Proc Owner 25,967ms
353
IIS APPPOOL\\MyWeb,,0%,138,008K
雖然時間縮短到 1/3,但共耗時 50 秒還是讓我無法接受,WMI 及 PerformanceCounter 並不以效率著稱,屬於先天限制,很難再有突破,必須改換它法。Windows 內建的工作管理或 SysInternal Process Explorer 可以做到一秒一刷,就跟我的 C# 版本有天壤之別,爬文推測應是使用未公開的 Win32 API (延伸閱讀:NtQuerySystemInformation的使用),研究完技術文件,改用該 API 的複雜度超出我的能力範圍,但確立改用 Win32 API 取代 WMI/PeformanceCounter 加速的方向。
尋找資料過程,意外發現 SELECT * FROM Win32\_PerfFormattedData\_PerfProc\_Process
這個 WMI 查詢能一口氣取得 Process ID、Name、CPU %、Private Working Set 資訊,而且速度不慢。缺少的執行帳號,我也找到使用 Win32 API 由 Process Handle 取得執行帳號的做法,於是寫了第三版:
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Management;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
namespace MyTaskMgr
{
public class ProcInfoModule3
{
public static List<ProcInfoDto> QueryProcesses()
{
var sw = new Stopwatch();
sw.Start();
var procs = QueryProcInfoByCIMV2();
sw.Stop();
Console.WriteLine($"QueryProcInfoByCIMV2 {sw.ElapsedMilliseconds:n0}ms");
sw.Restart();
Process.GetProcesses().AsParallel()
.ForAll(o =>
{
try
{
if (procs.ContainsKey(o.Id))
{
procs[o.Id].UserName = ExGetProcUserByHandle(o.Handle);
}
}
catch
{
//Ignore
}
});
sw.Stop();
Console.WriteLine($"Get Proc Owner {sw.ElapsedMilliseconds:n0}ms");
return procs.Values.OrderBy(o => o.Name).ToList();
}
//https://bytes.com/topic/c-sharp/answers/225065-how-call-win32-native-api-gettokeninformation-using-c
public const int TOKEN_QUERY = 0X00000008;
enum TOKEN_INFORMATION_CLASS { TokenUser = 1 }
[StructLayout(LayoutKind.Sequential)]
struct TOKEN_USER
{
public _SID_AND_ATTRIBUTES User;
}
[StructLayout(LayoutKind.Sequential)]
public struct _SID_AND_ATTRIBUTES
{
public IntPtr Sid;
public int Attributes;
}
[DllImport("advapi32")]
static extern bool OpenProcessToken(
IntPtr ProcessHandle, int DesiredAccess, ref IntPtr TokenHandle
);
[DllImport("advapi32", CharSet = CharSet.Auto)]
static extern bool GetTokenInformation(
IntPtr hToken, TOKEN_INFORMATION_CLASS tokenInfoClass, IntPtr TokenInformation,
int tokeInfoLength, ref int reqLength
);
[DllImport("kernel32")]
static extern bool CloseHandle(IntPtr handle);
public static bool DumpUserInfo(IntPtr pToken, out IntPtr SID)
{
int Access = TOKEN_QUERY;
IntPtr procToken = IntPtr.Zero;
bool ret = false;
SID = IntPtr.Zero;
try
{
if (OpenProcessToken(pToken, Access, ref procToken))
{
ret = ProcessTokenToSid(procToken, out SID);
CloseHandle(procToken);
}
return ret;
}
catch (Exception err)
{
return false;
}
}
private static bool ProcessTokenToSid(IntPtr token, out IntPtr SID)
{
TOKEN_USER tokUser;
const int bufLength = 256;
IntPtr tu = Marshal.AllocHGlobal(bufLength);
bool ret = false;
SID = IntPtr.Zero;
try
{
int cb = bufLength;
ret = GetTokenInformation(token, TOKEN_INFORMATION_CLASS.TokenUser, tu, cb, ref cb);
if (ret)
{
tokUser = (TOKEN_USER)Marshal.PtrToStructure(tu, typeof(TOKEN_USER));
SID = tokUser.User.Sid;
}
return ret;
}
catch (Exception err)
{
return false;
}
finally
{
Marshal.FreeHGlobal(tu);
}
}
public static string ExGetProcUserByHandle(IntPtr handle)
{
try
{
IntPtr _SID = IntPtr.Zero;
if (DumpUserInfo(handle, out _SID))
{
return new System.Security.Principal.SecurityIdentifier(_SID)
.Translate(typeof(System.Security.Principal.NTAccount)).Value;
}
}
catch { }
return "Unknown";
}
public static Dictionary<int, ProcInfoDto> QueryProcInfoByCIMV2()
{
//http://wutils.com/wmi/root/cimv2/win32_perfformatteddata_perfproc_process/
ManagementObjectSearcher searcher =
new ManagementObjectSearcher("root\\CIMV2",
"SELECT * FROM Win32_PerfFormattedData_PerfProc_Process");
return searcher.Get().Cast<ManagementObject>().ToList().Select(queryObj =>
{
var pid = Convert.ToInt32(queryObj["IDProcess"]);
if (pid == 0) return null;
return new ProcInfoDto()
{
ProcessId = pid,
Name = queryObj["Name"].ToString(),
PrivWorkSet = $"{Convert.ToUInt64(queryObj["WorkingSetPrivate"]) / 1024:n0}K",
ProcTimeInPerc = $"{Convert.ToInt64(queryObj["PercentProcessorTime"]):n0}%"
};
})
.Where(o => o != null)
.ToDictionary(o => o.ProcessId, o => o);
}
}
}
執行速度由三分鐘一舉推進到 1 秒內,辛苦半天終於達到可接受的水準,特 PO 文留念。(灑花)
QueryProcInfoByCIMV2 534ms
Get Proc Owner 104ms
368
IIS APPPOOL\\MyWeb,5%,109,656K
2018-11-29 補充:網友提到 PowerShell 有個 Get-Process,試了一下,執行速度夠快且一次給足 Process Name、CPU、Memory 與 UserName (如下圖),可惜跟 .NET Process 類別一樣,記憶體數字只有 Working Set 沒有 Private Working Set,仍需事後加工,評估後決定維持現行做法。
It's not difficulte to provide a process list including process name, cpu%, working set private and username with .NET program, but the WMI and performance counter is way too slow. By replacing Win32\_PerfFormattedData\_PerfProc\_Process and Win32 API, the excution time is 170x faster.
Comments
# by Robert chu
請問ProcInfoDto這個類別定義在哪裡?
# by Jeffrey
to Robert chu, ProcInfoDto是我為示範隨意捏造的自訂類別,可換成你想用的資料呈現物件。 若要直接使用ProcInfoDto,可以定義如下 public class ProcInfoDto { public int PID; public string Name; public string PrivWorkSet; public string ProcTimeInPerc; }
# by Balance
可以教一下...為什麼跑出來的結果與工作管理員的處理程序內的值不同(與工作管理員的詳細資料內的值基本是相符的)
# by Jeffrey
to Balance,不知是哪個值不相同,如果擷圖範例大家可以幫想。