AOT漫談: 如何獲取C#程序的CPU利用率
一、背景
1. 講故事
上篇聊到了如何對AOT程序進行輕量級的APM監(jiān)控,有朋友問我如何獲取AOT程序的CPU利用率,本來我覺得這是一個挺簡單的問題,但一研究不是這么一回事,這篇我們簡單的聊一聊。
二、如何獲取CPU利用率
1. 認識cpuUtilization字段
熟悉.NET底層的朋友應該知道,.NET線程池中有一個cpuUtilization字段就記錄了當前機器的CPU利用率,所以接下來的思路就是如何把這個字段給挖出來,在挖這個字段之前也要知道 .NET6 為界限出現(xiàn)過兩個線程池。
1)win32threadpool.cpp
這是 .NET6 之前一直使用的 .NET線程池,它是由 clr 的 1)win32threadpool.cpp 實現(xiàn)的,參考代碼如下:
SVAL_IMPL(LONG,ThreadpoolMgr,cpuUtilization);
為了更好的跨平臺以及高層統(tǒng)一, .NET團隊用C#對原來的線程池進行了重構(gòu),所以這個字段自然也落到了C#中,參考如下:
internal sealed class PortableThreadPool
{
private int _cpuUtilization;
}
我原以為線程池已經(jīng)被這兩種實現(xiàn)平分天下,看來我還是年輕了,不知道什么時候又塞入了一種線程池實現(xiàn) WindowsThreadPool.cs,無語了,它是簡單的 WindowsThreadPool 的 C#封裝,舍去了很多原來的方法實現(xiàn),比如:
internal static class WindowsThreadPool
{
public static bool SetMinThreads(int workerThreads, int completionPortThreads)
{
return false;
}
public static bool SetMaxThreads(int workerThreads, int completionPortThreads)
{
return false;
}
internal static void NotifyThreadUnblocked()
{
}
internal unsafe static void RequestWorkerThread()
{
//todo...
//提交到 windows線程池
Interop.Kernel32.SubmitThreadpoolWork(s_work);
}
}
而這個也是 Windows 版的AOT默認實現(xiàn),因為 Windows線程池是由操作系統(tǒng)實現(xiàn),沒有源碼公開,觀察了reactos的開源實現(xiàn),也未找到類似的cpuUtilization字段,這就比較尷尬了,常見的應對措施如下:
- 因為dump或者program中沒有現(xiàn)成字段,只能在程序中使用代碼獲取。
- 修改windows上的 aot 默認線程池。
2. 如果修改AOT的默認線程池
在微軟的官方文檔:https://learn.microsoft.com/zh-cn/dotnet/core/runtime-config/threading 上就記錄了Windows線程池的一些概況以及如何切換線程池的方法,截圖如下:
圖片
這里選擇 MSBuild 的方式來配置。
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<PublishAot>true</PublishAot>
<UseWindowsThreadPool>false</UseWindowsThreadPool>
<InvariantGlobalization>true</InvariantGlobalization>
</PropertyGroup>
</Project>
接下來寫一段簡單的C#代碼,故意讓一個線程死循環(huán)。
internal class Program
{
static void Main(string[] args)
{
Task.Run(() =>
{
Test();
}).Wait();
}
static void Test()
{
var flag = true;
while (true)
{
flag = !flag;
}
}
}
這里要注意的一點是發(fā)布成AOT的程序不能以普通的帶有元數(shù)據(jù)的C#程序來套。畢竟前者沒有元數(shù)據(jù)了,那怎么辦呢?這就考驗你對AOT依賴樹的理解,熟悉AOT的朋友都知道,依賴樹的構(gòu)建最終是以有向圖的方式存儲在 _dependencyGraph 字段中,每個節(jié)點由基類 NodeFactory 承載,參考代碼如下:
public abstract class Compilation : ICompilation
{
protected readonly DependencyAnalyzerBase<NodeFactory> _dependencyGraph;
}
public abstract partial class NodeFactory
{
public virtual void AttachToDependencyGraph(DependencyAnalyzerBase<NodeFactory> graph)
{
ReadyToRunHeader = new ReadyToRunHeaderNode();
graph.AddRoot(ReadyToRunHeader, "ReadyToRunHeader is always generated");
graph.AddRoot(new ModulesSectionNode(), "ModulesSection is always generated");
graph.AddRoot(GCStaticsRegion, "GC StaticsRegion is always generated");
graph.AddRoot(ThreadStaticsRegion, "ThreadStaticsRegion is always generated");
graph.AddRoot(EagerCctorTable, "EagerCctorTable is always generated");
graph.AddRoot(TypeManagerIndirection, "TypeManagerIndirection is always generated");
graph.AddRoot(FrozenSegmentRegion, "FrozenSegmentRegion is always generated");
graph.AddRoot(InterfaceDispatchCellSection, "Interface dispatch cell section is always generated");
graph.AddRoot(ModuleInitializerList, "Module initializer list is always generated");
if (_inlinedThreadStatics.IsComputed())
{
graph.AddRoot(_inlinedThreadStatiscNode, "Inlined threadstatics are used if present");
graph.AddRoot(TlsRoot, "Inlined threadstatics are used if present");
}
ReadyToRunHeader.Add(ReadyToRunSectionType.GCStaticRegion, GCStaticsRegion);
ReadyToRunHeader.Add(ReadyToRunSectionType.ThreadStaticRegion, ThreadStaticsRegion);
ReadyToRunHeader.Add(ReadyToRunSectionType.EagerCctor, EagerCctorTable);
ReadyToRunHeader.Add(ReadyToRunSectionType.TypeManagerIndirection, TypeManagerIndirection);
ReadyToRunHeader.Add(ReadyToRunSectionType.FrozenObjectRegion, FrozenSegmentRegion);
ReadyToRunHeader.Add(ReadyToRunSectionType.ModuleInitializerList, ModuleInitializerList);
var commonFixupsTableNode = new ExternalReferencesTableNode("CommonFixupsTable", this);
InteropStubManager.AddToReadyToRunHeader(ReadyToRunHeader, this, commonFixupsTableNode);
MetadataManager.AddToReadyToRunHeader(ReadyToRunHeader, this, commonFixupsTableNode);
MetadataManager.AttachToDependencyGraph(graph);
ReadyToRunHeader.Add(MetadataManager.BlobIdToReadyToRunSection(ReflectionMapBlob.CommonFixupsTable), commonFixupsTableNode);
}
}
結(jié)合上面的代碼,我們的 PortableThreadPool 靜態(tài)類會記錄到根區(qū)域的 GCStaticsRegion 中,有了這些知識,接下來就是開挖了。
3. 使用 windbg 開挖
用 windbg 啟動生成好的 aot程序,接下來用 Example_21_8!S_P_CoreLib_System_Threading_PortableThreadPool::__GCSTATICS 找到類中的靜態(tài)字段。
0:007> dp Example_21_8!S_P_CoreLib_System_Threading_PortableThreadPool::__GCSTATICS L1
00007ff6`e4b7c5d0 000002a5`a4000468
0:007> dp 000002a5`a4000468+0x8 L1
000002a5`a4000470 000002a5`a6809ca0
0:007> dd 000002a5`a6809ca0+0x50 L1
000002a5`a6809cf0 0000000a
0:007> ? a
Evaluate expression: 10 = 00000000`0000000a
從上面的卦中可以清晰的看到,當前的CPU=10%。這里稍微解釋下 000002a5a4000468+0x8 是用來跳過vtable從而取到類實例,后面的 000002a5a6809ca0+0x50 是用來獲取 PortableThreadPool._cpuUtilization 字段的,布局參考如下:
0:012> !dumpobj /d 27bc100b288
Name: System.Threading.PortableThreadPool
MethodTable: 00007ffc6c1aa6f8
EEClass: 00007ffc6c186b38
Tracked Type: false
Size: 512(0x200) bytes
File: C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.8\System.Private.CoreLib.dll
Fields:
MT Field Offset Type VT Attr Value Name
00007ffc6c031188 4000d42 50 System.Int32 1 instance 10 _cpuUtilization
00007ffc6c0548b0 4000d43 5c System.Int16 1 instance 12 _minThreads
00007ffc6c0548b0 4000d44 5e System.Int16 1 instance 32767 _maxThreads
三、總結(jié)
總的來說如果你的AOT使用默認的 WindowsThreadPool,那想獲取 cpu利用率基本上是無力回天,當然有達人知道的話可以告知下,如果切到默認的.NET線程池還是有的一拼,即使沒有 pdb 符號也可以根據(jù)_minThreads和_maxThreads的內(nèi)容反向搜索。