一:背景
1. 讲故事
上篇聊到了如何对AOT程序进行轻量级的APM监控,有朋友问我如何获取AOT程序的CPU利用率,本来我觉得这是一个挺简单的问题,但一研究不是这么一回事,这篇我们简单的聊一聊。
二:如何获取CPU利用率
1. 认识cpuUtilization字段
熟悉.NET底层的朋友应该知道,.NET线程池中有一个cpuUtilization
字段就记录了当前机器的CPU利用率,所以接下来的思路就是如何把这个字段给挖出来,在挖这个字段之前也要知道 .NET6 为界限出现过两个线程池。
1)win32threadpool.cpp
这是 .NET6 之前一直使用的 .NET线程池,它是由 clr 的 1)win32threadpool.cpp
实现的,参考代码如下:
SVAL_IMPL(LONG,ThreadpoolMgr,cpuUtilization);
- PortableThreadPool.cs
为了更好的跨平台以及高层统一, .NET团队用C#对原来的线程池进行了重构,所以这个字段自然也落到了C#中,参考如下:
internal sealed class PortableThreadPool { private int _cpuUtilization; }
- WindowsThreadPool.cs
我原以为线程池已经被这两种实现平分天下,看来我还是年轻了,不知道什么时候又塞入了一种线程池实现 WindowsThreadPool.cs
,无语了,它是简单的 WindowsThreadPool 的 C#封装,舍去了很多原来的方法实现,比如:
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默认实现,因为 Windows线程池是由操作系统实现,没有源码公开,观察了reactos的开源实现,也未找到类似的cpuUtilization
字段,这就比较尴尬了,常见的应对措施如下:
- 因为dump或者program中没有现成字段,只能在程序中使用代码获取。
- 修改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#代码,故意让一个线程死循环。
internal class Program { static void Main(string[] args) { Task.Run(() => { Test(); }).Wait(); } static void Test() { var flag = true; while (true) { flag = !flag; } } }
这里要注意的一点是发布成AOT的程序不能以普通的带有元数据的C#程序来套。毕竟前者没有元数据了,那怎么办呢?这就考验你对AOT依赖树的理解,熟悉AOT的朋友都知道,依赖树的构建最终是以有向图的方式存储在 _dependencyGraph 字段中,每个节点由基类 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); } }
结合上面的代码,我们的 PortableThreadPool 静态类会记录到根区域的 GCStaticsRegion 中,有了这些知识,接下来就是开挖了。
3. 使用 windbg 开挖
用 windbg 启动生成好的 aot程序,接下来用 Example_21_8!S_P_CoreLib_System_Threading_PortableThreadPool::__GCSTATICS
找到类中的静态字段。
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 FilesdotnetsharedMicrosoft.NETCore.App8.0.8System.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
三:总结
总的来说如果你的AOT使用默认的 WindowsThreadPool,那想获取 cpu利用率基本上是无力回天,当然有达人知道的话可以告知下,如果切到默认的.NET线程池
还是有的一拼,即使没有 pdb 符号也可以根据_minThreads和_maxThreads的内容反向搜索。