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

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

时间:2024-10-14 10:32:43浏览次数:1  
标签:C# graph AddRoot AOT 线程 ReadyToRunHeader always CPU

一:背景

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的内容反向搜索。
图片名称

标签:C#,graph,AddRoot,AOT,线程,ReadyToRunHeader,always,CPU
From: https://www.cnblogs.com/huangxincheng/p/18463560

相关文章

  • k8s 1.28 安装配置 knative-serving v1.15.2 + cert-manager v1.16.1
    安装配置knative-serving配置基础组件#考虑image可能存在拉取问题,可以使用https://github.com/DaoCloud/public-image-mirror方法替换kubectlapply-fhttps://github.com/knative/serving/releases/download/knative-v1.15.2/serving-crds.yamlkubectlapply-fhttps:......
  • 10.12 代码源 2024 CSP-S 模拟赛 Day 14
    省流:\(100+0+0+8=108\)简称:唐诗T1T2T2很有思路,几分钟就推出来一个\(a_i\)不全为奇数的柿子,然后发现大样例是全为奇数的()然后就一直在推式子,然后快推完了比赛结束了……然后赛后发现全为奇数的用暴力搞……T3一眼DP但是想写T2,甚至连暴力都没码……正解是状压(一位大......
  • MySQL 的 JDBC 编程
    MySQL的JDBC编程使用java代码操作数据库jdbc统一了各种数据库的api准备阶段首先要准备编辑语言的安装:javajdk1.8数据库数据库驱动包:在中央仓库下载搜索MySQL第一个就是MySQLConnector/j下载完成后在IDEA中导入这个包打开IDEA新建一个java项目新建......
  • Navicat Premium 17.1.3版本发布,原生支持华为云数据仓库GaussDB(DWS)
     近日,Navicat发布了一项重要更新,原生支持华为云数据仓库GaussDB(DWS)。这是Navicat支持的国内首个数据仓库产品,将为广大GaussDB(DWS)用户带来更多的便利。Navicat无缝对接GaussDB(DWS),让数据仓库的构建、查询、优化及维护变得更加直观和易于管理,助力企业轻松驾驭大数据时代的......
  • 慧通教育C++测试题 103667--103673(5题)
    103667.求最大值难度:1登录//103667.求最大值难度:1#include<bits/stdc++.h>usingnamespacestd;intans=0,n,m,q,a[105][105];intmain(){ cin>>n>>m>>q; for(inti=1;i<=n;i++){ for(intj=1;j<=m;j++){ cin>>a[i][j]; } } int......
  • jar包内替换依赖jar后无法启动,错误日志:It has been compressed and nested jar files
    jar包内替换依赖jar后无法启动,错误日志:Ithasbeencompressedandnestedjarfilesmustbestoredwithoutcompression.ruoyi、springboot、java、jar、libs、压缩背景某服务jar包足足90MB有余,远程传输太慢,目前在改动的是其中的某子jar(项目内部依赖,另一个jar)。之前......
  • jar包内替换依赖jar后无法启动,错误日志:It has been compressed and nested jar files
    jar包内替换依赖jar后无法启动,错误日志:Ithasbeencompressedandnestedjarfilesmustbestoredwithoutcompression.ruoyi、springboot、java、jar、libs、压缩背景某服务jar包足足90MB有余,远程传输太慢,目前在改动的是其中的某子jar(项目内部依赖,另一个jar)。之前......
  • C语言分支与循环的学习(小知识)
    学习目录 1.if 表达式成立(为真),则语句执行;表达式不成立(为假),这语句不执行。    注解:C语言中,非零表示真,0表示假。如果一个表达式的结果不是0,这语句执行。反之,则语句执行。实例:输入一个整数,判断是否为奇数该程序的执行逻辑是包含头文件#include<stdio.h>,输入主函数,......
  • MySql数据库---存储过程(带in、out、inout参数,),变量定义,if,case判断,循环结构,游标,handler
    思维导图 存储过程概念MySQL5.0版本开始支持存储过程。简单的说,存储过程就是一组SQL语句集,功能强大,可以实现一些比较复杂的逻辑功能,类似于JAVA语言中的方法,类似Python中的函数;存储过就是数据库SQL语言层面的代码封装与重用入门案例语法:delimiter自定义结束......
  • discuz网站搬家 数据库连接错误
    Discuz!网站搬家后遇到数据库连接错误通常是因为搬家过程中某些配置没有正确更新或迁移过程中出现问题。以下是一些可能的原因及解决步骤:检查数据库配置文件:打开 config/config_global.php 文件,确认数据库的用户名、密码、主机地址和端口是否与新环境中的数据库一致。如......