AOT漫谈专题(第三篇): 怎样获取C#程序的CPU利用率

打印 上一主题 下一主题

主题 881|帖子 881|积分 2643

一:背景

1. 讲故事

上篇聊到了怎样对AOT程序进行轻量级的APM监控,有朋友问我怎样获取AOT程序的CPU利用率,原来我觉得这是一个挺简单的标题,但一研究不是这么一回事,这篇我们简单的聊一聊。
二:怎样获取CPU利用率

1. 熟悉cpuUtilization字段

熟悉.NET底层的朋友应该知道,.NET线程池中有一个cpuUtilization字段就记录了当前呆板的CPU利用率,以是接下来的思路就是怎样把这个字段给挖出来,在挖这个字段之前也要知道 .NET6 为界限出现过两个线程池。
1)win32threadpool.cpp
这是 .NET6 之前不停使用的 .NET线程池,它是由 clr 的 1)win32threadpool.cpp 实现的,参考代码如下:
  1. SVAL_IMPL(LONG,ThreadpoolMgr,cpuUtilization);
复制代码

  • PortableThreadPool.cs
为了更好的跨平台以及高层统一, .NET团队用C#对原来的线程池进行了重构,以是这个字段自然也落到了C#中,参考如下:
  1. internal sealed class PortableThreadPool
  2. {
  3.     private int _cpuUtilization;
  4. }
复制代码

  • WindowsThreadPool.cs
我原以为线程池已经被这两种实现平分天下,看来我还是年轻了,不知道什么时间又塞入了一种线程池实现 WindowsThreadPool.cs,无语了,它是简单的 WindowsThreadPool 的 C#封装,舍去了很多原来的方法实现,比如:
  1. internal static class WindowsThreadPool
  2. {
  3.     public static bool SetMinThreads(int workerThreads, int completionPortThreads)
  4.     {
  5.         return false;
  6.     }
  7.     public static bool SetMaxThreads(int workerThreads, int completionPortThreads)
  8.     {
  9.         return false;
  10.     }
  11.     internal static void NotifyThreadUnblocked()
  12.     {
  13.     }
  14.     internal unsafe static void RequestWorkerThread()
  15.     {
  16.         //todo...
  17.         //提交到 windows线程池
  18.         Interop.Kernel32.SubmitThreadpoolWork(s_work);
  19.     }
  20. }
复制代码
而这个也是 Windows 版的AOT默认实现,由于 Windows线程池是由操纵体系实现,没有源码公开,观察了reactos的开源实现,也未找到类似的cpuUtilization字段,这就比较尴尬了,常见的应对步伐如下:

  • 由于dump大概program中没有现成字段,只能在程序中使用代码获取。
  • 修改windows上的 aot 默认线程池。
2. 如果修改AOT的默认线程池

在微软的官方文档:https://learn.microsoft.com/zh-cn/dotnet/core/runtime-config/threading 上就记录了Windows线程池的一些概况以及怎样切换线程池的方法,截图如下:

这里选择 MSBuild 的方式来设置。
  1. <Project Sdk="Microsoft.NET.Sdk">
  2.         <PropertyGroup>
  3.                 <OutputType>Exe</OutputType>
  4.                 <TargetFramework>net8.0</TargetFramework>
  5.                 <ImplicitUsings>enable</ImplicitUsings>
  6.                 <Nullable>enable</Nullable>
  7.                 <PublishAot>true</PublishAot>
  8.                 <UseWindowsThreadPool>false</UseWindowsThreadPool>
  9.                 <InvariantGlobalization>true</InvariantGlobalization>
  10.         </PropertyGroup>
  11. </Project>
复制代码
接下来写一段简单的C#代码,故意让一个线程死循环。
  1.     internal class Program
  2.     {
  3.         static void Main(string[] args)
  4.         {
  5.             Task.Run(() =>
  6.             {
  7.                 Test();
  8.             }).Wait();
  9.         }
  10.         static void Test()
  11.         {
  12.             var flag = true;
  13.             while (true)
  14.             {
  15.                 flag = !flag;
  16.             }
  17.         }
  18.     }
复制代码
这里要注意的一点是发布成AOT的程序不能以普通的带有元数据的C#程序来套。毕竟前者没有元数据了,那怎么办呢?这就考验你对AOT依赖树的明白,熟悉AOT的朋友都知道,依赖树的构建最终是以有向图的方式存储在 _dependencyGraph 字段中,每个节点由基类 NodeFactory 承载,参考代码如下:
  1. public abstract class Compilation : ICompilation
  2. {
  3.     protected readonly DependencyAnalyzerBase<NodeFactory> _dependencyGraph;
  4. }
  5. public abstract partial class NodeFactory
  6. {
  7.     public virtual void AttachToDependencyGraph(DependencyAnalyzerBase<NodeFactory> graph)
  8.     {
  9.         ReadyToRunHeader = new ReadyToRunHeaderNode();
  10.         graph.AddRoot(ReadyToRunHeader, "ReadyToRunHeader is always generated");
  11.         graph.AddRoot(new ModulesSectionNode(), "ModulesSection is always generated");
  12.         graph.AddRoot(GCStaticsRegion, "GC StaticsRegion is always generated");
  13.         graph.AddRoot(ThreadStaticsRegion, "ThreadStaticsRegion is always generated");
  14.         graph.AddRoot(EagerCctorTable, "EagerCctorTable is always generated");
  15.         graph.AddRoot(TypeManagerIndirection, "TypeManagerIndirection is always generated");
  16.         graph.AddRoot(FrozenSegmentRegion, "FrozenSegmentRegion is always generated");
  17.         graph.AddRoot(InterfaceDispatchCellSection, "Interface dispatch cell section is always generated");
  18.         graph.AddRoot(ModuleInitializerList, "Module initializer list is always generated");
  19.         if (_inlinedThreadStatics.IsComputed())
  20.         {
  21.             graph.AddRoot(_inlinedThreadStatiscNode, "Inlined threadstatics are used if present");
  22.             graph.AddRoot(TlsRoot, "Inlined threadstatics are used if present");
  23.         }
  24.         ReadyToRunHeader.Add(ReadyToRunSectionType.GCStaticRegion, GCStaticsRegion);
  25.         ReadyToRunHeader.Add(ReadyToRunSectionType.ThreadStaticRegion, ThreadStaticsRegion);
  26.         ReadyToRunHeader.Add(ReadyToRunSectionType.EagerCctor, EagerCctorTable);
  27.         ReadyToRunHeader.Add(ReadyToRunSectionType.TypeManagerIndirection, TypeManagerIndirection);
  28.         ReadyToRunHeader.Add(ReadyToRunSectionType.FrozenObjectRegion, FrozenSegmentRegion);
  29.         ReadyToRunHeader.Add(ReadyToRunSectionType.ModuleInitializerList, ModuleInitializerList);
  30.         var commonFixupsTableNode = new ExternalReferencesTableNode("CommonFixupsTable", this);
  31.         InteropStubManager.AddToReadyToRunHeader(ReadyToRunHeader, this, commonFixupsTableNode);
  32.         MetadataManager.AddToReadyToRunHeader(ReadyToRunHeader, this, commonFixupsTableNode);
  33.         MetadataManager.AttachToDependencyGraph(graph);
  34.         ReadyToRunHeader.Add(MetadataManager.BlobIdToReadyToRunSection(ReflectionMapBlob.CommonFixupsTable), commonFixupsTableNode);
  35.     }
  36. }
复制代码
结合上面的代码,我们的 PortableThreadPool 静态类会记录到根地区的 GCStaticsRegion 中,有了这些知识,接下来就是开挖了。
3. 使用 windbg 开挖

用 windbg 启动生成好的 aot程序,接下来用 Example_21_8!S_P_CoreLib_System_Threading_PortableThreadPool::__GCSTATICS 找到类中的静态字段。
  1. 0:007> dp Example_21_8!S_P_CoreLib_System_Threading_PortableThreadPool::__GCSTATICS L1
  2. 00007ff6`e4b7c5d0  000002a5`a4000468
  3. 0:007> dp 000002a5`a4000468+0x8 L1
  4. 000002a5`a4000470  000002a5`a6809ca0
  5. 0:007> dd 000002a5`a6809ca0+0x50 L1
  6. 000002a5`a6809cf0  0000000a
  7. 0:007> ? a
  8. Evaluate expression: 10 = 00000000`0000000a
复制代码
从上面的卦中可以清晰的看到,当前的CPU=10%。这里轻微表明下 000002a5a4000468+0x8 是用来跳过vtable从而取到类实例,后面的 000002a5a6809ca0+0x50 是用来获取 PortableThreadPool._cpuUtilization 字段的,布局参考如下:
  1. 0:012> !dumpobj /d 27bc100b288
  2. Name:        System.Threading.PortableThreadPool
  3. MethodTable: 00007ffc6c1aa6f8
  4. EEClass:     00007ffc6c186b38
  5. Tracked Type: false
  6. Size:        512(0x200) bytes
  7. File:        C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.8\System.Private.CoreLib.dll
  8. Fields:
  9.               MT    Field   Offset                 Type VT     Attr            Value Name
  10. 00007ffc6c031188  4000d42       50         System.Int32  1 instance                10 _cpuUtilization
  11. 00007ffc6c0548b0  4000d43       5c         System.Int16  1 instance               12 _minThreads
  12. 00007ffc6c0548b0  4000d44       5e         System.Int16  1 instance            32767 _maxThreads
复制代码
三:总结

总的来说如果你的AOT使用默认的 WindowsThreadPool,那想获取 cpu利用率基本上是无力回天,当然有达人知道的话可以告知下,如果切到默认的.NET线程池还是有的一拼,即使没有 pdb 符号也可以根据_minThreads和_maxThreads的内容反向搜索。


免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

盛世宏图

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表