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

news/2024/10/14 10:41:10

一:背景

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);
  1. PortableThreadPool.cs

为了更好的跨平台以及高层统一, .NET团队用C#对原来的线程池进行了重构,所以这个字段自然也落到了C#中,参考如下:


internal sealed class PortableThreadPool
{private int _cpuUtilization;
}
  1. 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字段,这就比较尴尬了,常见的应对措施如下:

  1. 因为dump或者program中没有现成字段,只能在程序中使用代码获取。
  2. 修改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=16%。这里稍微解释下 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

三:总结

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.ryyt.cn/news/71366.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈,一经查实,立即删除!

相关文章

【GIS前沿技术】什么是网格化(Gird)GIS?

文章目录 网格GIS的主要特点 网格GIS的优势 网格GIS工作原理 网格GIS实现技术 网格GIS是一种将地理空间数据组织成网格(格网)形式的地理信息系统。每个网格单元(通常称为“像元”或“格网单元”)代表特定的空间范围,具有固定的大小和形状。这种结构使得数据的存储、处理和…

请问公司网站如何修改电话

要修改公司网站上的联系电话,你可以按照以下步骤操作:登录网站后台:如果你的公司网站有CMS(内容管理系统)如WordPress或Drupal,你需要登录到该系统的管理后台。找到页面编辑器:在后台管理界面中,找到包含联系电话信息的页面,通常是“关于我们”、“联系我们”或者页脚…

公司网站修改_单位网站修改

为了帮助您更好地理解和执行公司或单位网站的修改工作,我将分步骤介绍一般性的网站修改流程和注意事项: 1. 确定修改目标明确网站需要改进的具体方面,如设计更新、功能增强、内容调整等。 收集用户反馈或分析现有数据来确定优先级。2. 规划与设计制定详细的修改计划,包括时…

怎么修改一个公司网站的网页?

要修改一个公司网站的网页,你可以按照以下步骤进行:获取权限:确保你有权限访问和修改网站的内容管理系统(CMS)或源代码。备份现有内容:在进行任何更改之前,先备份当前的网页文件或数据库内容,以防修改过程中出现问题。确定修改需求:明确你需要修改的具体内容,例如文本…

pdf添加水印

1.打开pdf转换器 2.在pdf工具编辑界面找到添加水印 3.点添加文件。将所需文件拖拽到此处 4.点击添加图片水印,找到所需的图片水印上传并调试水印所在位置,导出即可。

免费如何修改公司的网页 自己就能做个网站

要免费修改公司的网页或自己创建一个网站,可以遵循以下步骤:选择合适的工具:选择适合初学者且功能强大的免费网站建设工具,如Wix、WordPress.com或Google Sites。学习基础知识:学习HTML、CSS和JavaScript的基础知识。这些是网页设计的基本语言。 如果使用WordPress或其他C…

uniapp小程序使用文字转语音播报类似支付宝收款播报小程序语音识别和朗读

第一步:登陆微信公众平台,侧边栏的设置-----第三方设置-----插件管理----添加插件(搜索–同声传译)第二步:打开 hbiuider-x,找到当前项目,打开manifest.json,找到源码视图,配置插件代码块:// provider是你查看详情以后的appId "plugins": {"WechatSI": …

忘记php网站后台密码怎么办?

如果你忘记了PHP网站后台的密码,可以通过以下几种方法来重置或找回:检查邮箱:如果在创建账户时设置了密码找回功能,并绑定了邮箱,可以尝试通过邮箱找回密码。数据库直接修改:连接到数据库(如MySQL),找到存储用户信息的表。 查找管理员账号对应的记录。 使用 SQL 语句更…