记一次 .NET 某安全生产信息系统 CPU爆高分析

一次,net,安全,生产,信息系统,cpu,分析 · 浏览次数 : 2568

小编点评

**多线程环境下使用线程不安全集合的死循环形成** 多线程环境下使用线程不安全集合,问题虽然很小白,但还是很多朋友栽在这上面,值得反思哈,借这一次机会进一步解释下死循环形成的内部机理。 **死循环的形成** 死循环形成是因为当线程在使用线程不安全集合时,试图从集合中获取元素时,集合本身并没有 enough 元素来给线程。这会导致线程不断循环地获取元素,最终导致线程退出。 **死循环的内部机理** 1. **获取元素:**当线程在使用线程不安全集合时,试图从集合中获取元素时,线程会从集合中获取元素。但是,当集合本身没有 enough 元素来给线程时,线程就会遇到异常。 2. **异常:**异常会导致线程退出。 3. **循环:**当线程反复获取元素时,线程会不断循环地获取元素,最终导致线程退出。 **解决方法** 1. **设置线程数量:**设置线程数量时,可以指定线程从线程不安全集合中获取的元素数量。 2. **使用线程安全集合:**使用线程安全集合可以确保线程从线程不安全集合中获取的元素数量不会超过集合本身的元素数量。 3. **检查线程安全集合的元素数量:**在获取元素之前,可以检查线程安全集合的元素数量。如果元素数量不足,可以抛出异常。 **注意事项** 1. **线程安全集合的元素数量必须足够:**线程安全集合的元素数量必须足够来给线程获取的元素数量。如果元素数量不足,线程会遇到异常。 2. **线程安全集合的元素数量必须为整数:**线程安全集合的元素数量必须为整数。如果元素数量不是整数,线程会遇到异常。 3. **不要设置线程数量小于线程安全集合的元素数量:**设置线程数量时,如果线程数量小于线程安全集合的元素数量,线程可能会无法从集合中获取元素。

正文

一:背景

1.讲故事

今天是🐏的第四天,头终于不巨疼了,写文章已经没什么问题,赶紧爬起来写。

这个月初有位朋友找到我,说他的程序出现了CPU爆高,让我帮忙看下怎么回事,简单分析了下有两点比较有意思。

  1. 这是一个安全生产的信息管理平台,第一次听说,我的格局小了。

  2. 这是一个经典的 CPU 爆高问题,过往虽有分析,但没有刨根问底,刚好这一篇就来问一下底吧。

话不多说,我们上 WinDbg 说话。

二:WinDbg 分析

1. 真的 CPU 爆高吗?

别人说爆高不算,我们得拿数据说话不是,验证命令就是 !tp


0:085> !tp
CPU utilization: 100%
Worker Thread: Total: 40 Running: 26 Idle: 6 MaxLimit: 32767 MinLimit: 8
Work Request in Queue: 0
--------------------------------------
Number of Timers: 0
--------------------------------------
Completion Port Thread:Total: 1 Free: 1 MaxFree: 16 CurrentLimit: 1 MaxLimit: 1000 MinLimit: 8

从卦中看果然是被打满了,接下来可以用 ~*e !clrstack 观察各个线程都在做什么,稍微一观察就会发现有很多的线程卡在 FindEntry() 方法上,截图如下:

从图中可以看到,有 25 个线程都停在 FindEntry() 之上,如果你的经验比较丰富的话,我相信你马上就知道这是多线程环境下使用了非线程安全集合 Dictionary 造成的死循环,把 CPU 直接打爆。

按以往套路到这里就结束了,今天我们一定要刨到底。

2. 为什么会出现死循环

要知道死循环的成因,那就一定要从 FindEntry 上入手。


private int FindEntry(TKey key)
{
    if (key == null)
    {
        ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key);
    }
    if (buckets != null)
    {
        int num = comparer.GetHashCode(key) & 0x7FFFFFFF;
        for (int num2 = buckets[num % buckets.Length]; num2 >= 0; num2 = entries[num2].next)
        {
            if (entries[num2].hashCode == num && comparer.Equals(entries[num2].key, key))
            {
                return num2;
            }
        }
    }
    return -1;
}

仔细观察上面的代码,如果真有死循环肯定是在 for 中出不来,如果是真的出在 for 上,那问题自然在 next 指针上。

关于 Dictionary 的内部布局和解析 可以参见我的 高级调试训练营,这里我们就不细说了。

那是不是出在 next 指针上呢? 我们来剖析下方法上下文。

3. 观察 next 指针布局

为了方便观察,先切到 85 号线程。


0:085> ~85s
mscorlib_ni!System.Collections.Generic.Dictionary<string,F2.xxx.ORM.SqlEntity>.FindEntry+0x8f:
00007ff8`5f128ccf 488b4e10        mov     rcx,qword ptr [rsi+10h] ds:0000017f`39c07d00=0000017eb9ee00c0
0:085> !clrstack
OS Thread Id: 0x4124 (85)
        Child SP               IP Call Site
0000007354ebcc70 00007ff85f128ccf System.Collections.Generic.Dictionary`2[[System.__Canon, mscorlib],[System.__Canon, mscorlib]].FindEntry(System.__Canon) [f:\dd\ndp\clr\src\BCL\system\collections\generic\dictionary.cs @ 305]

接下来把 Dictionary 中的 Entry[] 中的 next 给展示出来,可以用 !mdso 命令。


0:085> !mdso
Thread 85:
Location          Object            Type
------------------------------------------------------------
RCX:              0000017eb9ee00c0  System.Collections.Generic.Dictionary`2+Entry[[System.String, mscorlib],[xx]][]
RSI:              0000017f39c07cf0  System.Collections.Generic.Dictionary`2[[System.String, mscorlib],[xxx.xxx]]

0:085> !mdt -e:2 0000017eb9ee00c0
0000017eb9ee00c0 (System.Collections.Generic.Dictionary`2+Entry[[System.String, mscorlib],[xxx.xxx]][], Elements: 3, ElementMT=00007ff816cedc18)
[0] (System.Collections.Generic.Dictionary`2+Entry[[System.String, mscorlib],[F2.xxx]]) VALTYPE (MT=00007ff816cedc18, ADDR=0000017eb9ee00d0)
    hashCode:0x0 (System.Int32)
    next:0x0 (System.Int32)
    key:NULL (System.__Canon)
    value:NULL (System.__Canon)
[1] (System.Collections.Generic.Dictionary`2+Entry[[System.String, mscorlib],[F2.xxx]]) VALTYPE (MT=00007ff816cedc18, ADDR=0000017eb9ee00e8)
    hashCode:0x5aba4760 (System.Int32)
    next:0xffffffff (System.Int32)
    key:0000017f39c0ab50 (System.String) Length=20, String="xxxMessage_Select"
    value:0000017f39c0b5d0 (xxx.xxx.ORM.SqlEntity)
[2] (System.Collections.Generic.Dictionary`2+Entry[[System.String, mscorlib],[F2.xxx]]) VALTYPE (MT=00007ff816cedc18, ADDR=0000017eb9ee0100)
    hashCode:0x65b6e27b (System.Int32)
    next:0x1 (System.Int32)
    key:0000017f39c09d58 (System.String) Length=20, String="xxxMessage_Insert"
    value:0000017f39c0ba50 (xxx.xxx.ORM.SqlEntity)

从卦中看也蛮奇葩的,只有三个元素的 Dictionary 还能死循环。。。如果你仔细观察会发现 [0] 项是一种有损状态,value 没值不说, next:0x0 可是有大问题的,它会永远指向自己,因为 next 是指向 hash 挂链中的下一个节点的数组下标,画个图大概是这样。

接下来我们验证下是不是入口参数不幸进入了 [0] 号坑,然后在这个坑中永远指向自己呢?要想寻找答案,只需要在 FindEntry 的汇编代码中找到 int num = comparer.GetHashCode(key) & 0x7FFFFFFF; 中的 num 值,看它是不是 0 即可。


0:085> !U /d 00007ff85f128ccf
preJIT generated code
System.Collections.Generic.Dictionary`2[[System.__Canon, mscorlib],[System.__Canon, mscorlib]].FindEntry(System.__Canon)
Begin 00007ff85f128c40, size 130. Cold region begin 00007ff85ff07ff0, size 11
...
f:\dd\ndp\clr\src\BCL\system\collections\generic\dictionary.cs @ 303:
00007ff8`5f128c6f 488b5e18        mov     rbx,qword ptr [rsi+18h]
00007ff8`5f128c73 488b0e          mov     rcx,qword ptr [rsi]
00007ff8`5f128c76 488b5130        mov     rdx,qword ptr [rcx+30h]
00007ff8`5f128c7a 488b2a          mov     rbp,qword ptr [rdx]
00007ff8`5f128c7d 4c8b5d18        mov     r11,qword ptr [rbp+18h]
00007ff8`5f128c81 4d85db          test    r11,r11
00007ff8`5f128c84 750f            jne     mscorlib_ni!System.Collections.Generic.Dictionary<string,xxx.SqlEntity>.FindEntry+0x55 (00007ff8`5f128c95)
00007ff8`5f128c86 488d154d2f1800  lea     rdx,[mscorlib_ni+0x68bbda (00007ff8`5f2abbda)]
00007ff8`5f128c8d e8ce44f3ff      call    mscorlib_ni+0x43d160 (00007ff8`5f05d160) (mscorlib_ni)
00007ff8`5f128c92 4c8bd8          mov     r11,rax
00007ff8`5f128c95 488bcb          mov     rcx,rbx
00007ff8`5f128c98 488bd7          mov     rdx,rdi
00007ff8`5f128c9b 3909            cmp     dword ptr [rcx],ecx
00007ff8`5f128c9d 41ff13          call    qword ptr [r11]
00007ff8`5f128ca0 8bd8            mov     ebx,eax
00007ff8`5f128ca2 81e3ffffff7f    and     ebx,7FFFFFFFh
...

0:085> ? ebx
Evaluate expression: 957083499 = 00000000`390bef6b

0:085> ? 0n957083499 % 0n3
Evaluate expression: 0 = 00000000`00000000

从汇编代码中分析得出,num 是放在 ebx 寄存器上,此时 num=957083499,再 %3 之后就是 0 号坑,大家再结合源代码,你会发现这里永远都不会退出,永远都是指向自己,自然就是死循环了。

3. .NET6 下的补充

前段时间在整理课件时发现在 .NET6 中不再傻傻的死循环,而是在尝试 entries.Length 次之后还得不到结束的话,强制抛出异常,代码如下:


internal ref TValue FindValue(TKey key)
{
    uint hashCode2 = (uint)comparer.GetHashCode(key);
    int bucket2 = GetBucket(hashCode2);
    Entry[] entries2 = _entries;
    uint num2 = 0u;
    bucket2--;
    while ((uint)bucket2 < (uint)entries2.Length)
    {
        reference = ref entries2[bucket2];
        if (reference.hashCode != hashCode2 || !comparer.Equals(reference.key, key))
        {
            bucket2 = reference.next;
            num2++;
            if (num2 <= (uint)entries2.Length)
            {
                continue;
            }
            goto IL_0171;
        }
        goto IL_0176;
    }

    return ref Unsafe.NullRef<TValue>();
IL_0176:
    return ref reference.value;
IL_0171:
    ThrowHelper.ThrowInvalidOperationException_ConcurrentOperationsNotSupported();
    goto IL_0176;
}

可能是 .NET团队 被这样的问题咨询烦了,干脆抛一个异常得了。。。

三: 总结

多线程环境下使用线程不安全集合,问题虽然很小白,但还是有很多朋友栽在这上面,值得反思哈,借这一次机会进一步解释下死循环形成的内部机理。

图片名称

与记一次 .NET 某安全生产信息系统 CPU爆高分析相似的内容:

记一次 .NET 某安全生产信息系统 CPU爆高分析

一:背景 1.讲故事 今天是🐏的第四天,头终于不巨疼了,写文章已经没什么问题,赶紧爬起来写。 这个月初有位朋友找到我,说他的程序出现了CPU爆高,让我帮忙看下怎么回事,简单分析了下有两点比较有意思。 这是一个安全生产的信息管理平台,第一次听说,我的格局小了。 这是一个经典的 CPU 爆高问题,过往

记一次 .NET 某拍摄监控软件 卡死分析

一:背景 1. 讲故事 今天本来想写一篇 非托管泄露 的生产事故分析,但想着昨天就上了一篇非托管文章,连着写也没什么意思,换个口味吧,刚好前些天有位朋友也找到我,说他们的拍摄监控软件卡死了,让我帮忙分析下为什么会卡死,听到这种软件,让我不禁想起了前些天 在程序员桌子上安装监控 的新闻,参考如下: 我

记一次 .NET某网络边缘计算系统 卡死分析

一:背景 1. 讲故事 早就听说过有什么 网络边缘计算,这次还真给遇到了,有点意思,问了下 chatgpt 这是干嘛的 ? 网络边缘计算是一种计算模型,它将计算能力和数据存储位置从传统的集中式数据中心向网络边缘的用户设备、传感器和其他物联网设备移动。这种模型的目的是在接近数据生成源头的地方提供更快速

记一次 .NET某机械臂上位系统 卡死分析

一:背景 1. 讲故事 前些天有位朋友找到我,说他们的程序会偶发性的卡死一段时间,然后又好了,让我帮忙看下怎么回事?窗体类的程序解决起来相对来说比较简单,让朋友用procdump自动抓一个卡死时的dump,拿到dump之后,上 windbg 说话。 二:WinDbg 分析 1. 主线程在做什么 要想

记一次 .NET某工厂报警监控设置 崩溃分析

一:背景 1. 讲故事 前些天有位朋友在微信上丢了一个崩溃的dump给我,让我帮忙看下为什么出现了崩溃,在 Windows 的事件查看器上显示的是经典的 访问违例 ,即 c0000005 错误码,不管怎么说有dump就可以上windbg开干了。 二:WinDbg 分析 1. 程序为谁崩溃了 在 Wi

记一次 .NET某游戏币自助机后端 内存暴涨分析

一:背景 1. 讲故事 前些天有位朋友找到我,说他们的程序内存会偶发性暴涨,自己分析了下是非托管内存问题,让我帮忙看下怎么回事?哈哈,看到这个dump我还是非常有兴趣的,居然还有这种游戏币自助机类型的程序,下次去大玩家看看他们出币的机器后端是不是C#写的?由于dump是linux上的程序,刚好win

记一次 .NET某工控视觉自动化系统 卡死分析

一:背景 1. 讲故事 今天分享的dump是训练营里一位学员的,从一个啥也不会到现在分析的有模有样,真的是看他成长起来的,调试技术学会了就是真真实实自己的,话不多说,上windbg说话。 二:WinDbg 分析 1. 为什么会卡死 这位学员是从事工控大类下的视觉自动化,也是目前.NET的主战场,这个

记一次 .NET某质量检测中心系统 崩溃分析

一:背景 1. 讲故事 这些天有点意思,遇到的几个程序故障都是和Windows操作系统或者第三方组件有关系,真的有点无语,今天就带给大家一例 IIS 相关的与大家分享,这是一家国企的.NET程序,出现了崩溃急需分析。 二:WinDbg 分析 1. 为什么会崩溃 崩溃原因相对还是好找的,双击dump文

记一次 .NET某工业设计软件 崩溃分析

一:背景 1. 讲故事 前些天有位朋友找到我,说他的软件在客户那边不知道什么原因崩掉了,从windows事件日志看崩溃在 clr 里,让我能否帮忙定位下,dump 也抓到了,既然dump有了,接下来就上 windbg 分析吧。 二:WinDbg 分析 1. 为什么崩溃在 clr 一般来说崩溃在clr

记一次 .NET某工控WPF程序被人恶搞的 卡死分析

一:背景 1. 讲故事 这一期程序故障除了做原理分析,还顺带吐槽一下,熟悉我的朋友都知道我分析dump是免费的,但免费不代表可以滥用我的宝贵时间,我不知道有些人故意恶搞卡死是想干嘛,不得而知,希望后面类似的事情越来越少吧!废话不多说,我们来看看是如何被恶搞的。 二:WinDbg 分析 1. 程序是如