26

调试实战——程序CPU占用率飙升,你知道如何快速定位吗?

 4 years ago
source link: http://mp.weixin.qq.com/s?__biz=MzAwNTMxMzg1MA%3D%3D&%3Bmid=2654077165&%3Bidx=3&%3Bsn=6f7474e461f6ee934d53b2a79f92459d
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

前言

如果我们自己的程序的 CPU UsageCPU占用率 )飙升,并且居高不下,很有可能陷入了死循环。你知道怎么快速定位并解决吗?今天跟大家分享几种定位方法,希望对你有所帮助。

如何判断是否有死循环?

  • 通过电脑风扇的声音猜测。

    如果风扇一直响个不停,说明电脑很热。高 CPU占用率 会导致 CPU 发热量增大,从而导致风扇狂响。如果听到风扇响个不停,可以打开任务管理器看看 CPU占用率 是不是很高。如果发现是我们的进程导致的高 CPU占用率 ,那么可以进一步查看是不是有死循环。

  • 通过 CPU占用率 来判断。

    对于多核 CPU (尤其是性能强劲的 CPU ),一个核心的满负荷运转,并不会立刻导致 CPU 发热量明显增大,风扇可能不会有明显响动。这时根据风扇声音不能轻易判断出是否有死循环,但是我们可以通过 CPU占用率 来判断。

    如果 CPU 是单核的,那么当 CPU 处于满负荷运转状态, CPU占用率 会接近 100% 。如果 CPU4 核的,并且这 4 个核心都处于满负荷运转状态,那么 CPU占用率 会接近 100% ,如果只有一个核心是满负荷运转状态,那么 CPU 占用率会在 25%100 / 4 = 25 )左右。如果我们发现某个进程的 CPU占用率 居高不下,有可能是死循环了。

    注意:很多死循环都是 busy 类型的,如果是 idle 类型的死循环,上面的方法不适用。

下面介绍几个我经常使用的工具,可以比较便捷的排查此类问题。

1. process explorer

在前面的文章里跟大家介绍过,使用 process explorer 可以查看线程的 调用栈CPU占用率 。如果程序里的某个功能迟迟不能完成,我的第一反应是,按 Ctrl + Shift + Esc 打开任务管理器(我已经使用 process explorer 替换了系统自带的任务管理器,所以启动的是 process explorer 。如何使用 process explorer 替换系统自带的任务管理器,请参考文章 [原]排错实战——使用process explorer替换任务管理器 )。

启动 process explorer 后,双击我们关心的进程,切换到 Thread 页,在这里我们可以看到当前进程中的所有线程。双击某个线程就可以查看调用栈,在弹出的调用栈界面,点击左下角的 Refresh 按钮可以刷新。

如果每次刷新都能看到某个函数,很有可能是在这个函数中出现了死循环。对照源码,也许能直接能看出原因。

mMZfAjj.gif 使用process explorer

注意:需要正确加载调试符号才可以看到对应的函数名。

2. windbg

如果不能使用 process explorer 定位到具体的原因,可以使用 windbg 附加到进程中 进行更深入的调查 。我们需要找出哪个线程运行的时间最长,因为一般死循环的线程占用的 CPU 时间会比较长。应该怎么找呢?

  • 使用 .ttime 命令

    .ttime 可以查看当前线程的运行时间(用户态运行时间和内核态运行时间)。但是 .ttime 有个不足之处——没有输出相关的线程标识。我们需要根据其它信息来获取当前线程的标识。 3EJBV3b.jpg!web

    如果想查看所有线程的运行时间怎么办呢?当然可以手动切换到另外一个线程,然后执行 .ttime 。如果线程数量很多的话,这可是个体力活。不要怕,我们可以通过命令 ~*e .ttime 来获取每个线程的运行时间。因为 .ttime 输出结果中没有线程标识,我们需要执行命令 ~*e ? $tid;.ttime 把对应的 线程ID 一起输出。

    fAZJry3.jpg!web 获取所有线程ID和运行时间

    简单向大家解释下这条命令:

    • ~*e 会遍历所有线程并执行后面跟着的命令。其实, ~* 就可以遍历所有线程,比如我们在前面的文章里用到的 ~* kvn 命令来查看所有线程的调用栈。但是对于某些命令,如果不加 ewindbg 可能不能正确解析,会报错。
    • ? $tid 评估表达式 $tid 的值, ?windbg 中表示 Evaluate 的意思,会评估后面表达式的值。 $tid 是伪变量,代表了当前线程的 线程ID
    • ; 分号是命令分割符。
    • .ttime 查看当前线程的运行时间。

整条命令的效果是: 遍历每个线程,输出其对应的 线程ID 和运行时间。

  • 如果觉得上面的命令太长了,还可以使用更简单的命令 !runaway 查看线程运行时间。 UFRzqaz.jpg!web

下面是我用 !runaway 命令排查高 CPU占用率 的屏幕录像。

3. visual studio

如果是正在开发的程序在运行过程中出现了死循环,我会考虑用 vs 来附加到进程(如果进程是通过 Ctrl + F5 启动的话,并没有被调试)。然后通过 Parallel Stacks 查看所有线程,并用肉眼查找可能出问题的线程。因为我不知道 vs 中是否有类似 !runaway 的命令。如果哪位小伙伴有更好的办法,请一定要留言告诉我!

下面是我用 Parallel Stacks 功能排查高 CPU占用率 的屏幕录像。

小提示:按 Ctrl + Alt + p 可以快速打开 附加进程 界面。

以上三种工具,我会先使用 process explorer 大体定位下问题,因为可以非常方便的通过 Ctrl + Shift + Esc 启动。如果用 process explorer 解决不了,我会根据情况使用 windbg 或者 vs 。如果 vs 正开着(通常是正在写代码的时候),就顺手用 vs 附加到对应的进程中。如果 vs 没开着,当然会使用 windbg 进行排查了。 :sunglasses:

实战代码

如果你想动手实战,复制下面的代码到工程里就可以实战了。

简单介绍下代码:

  • 示例代码中启动了 8 个线程,是为了增大排查的难度,只有一个线程的情况太简单了。

  • 函数 FindFirstRepeatElementIndex() 的用途是 找到给定的数据中第一次出现重复的数据的索引

  • 除了我们发现的死循环的问题,还有什么地方可以优化呢?命名,效率,各个方面都可以优化哦,欢迎留言交流。

#include <vector>
#include <future>
#include <iostream>

int FindFirstRepeatElementIndex(bool bExcute)
{
  if (!bExcute)
  {
    return -1;
  }

  int idx = -1;
  std::vector<int> datas = { 1 , 3, 5, 7, 9, 11, 11, 13, 14, 15, 16, 17 };
  for (size_t i = 0; i < datas.size(); ++i)
  {
    for (size_t j = i = 1; j < datas.size(); ++j)
    {
      if (datas[j] == datas[i])
      {
        idx = i;
        break;
      }
    }
  }

  return idx;
}

#define THREAD_COUNT 8
int main()
{
  std::future<int> results[THREAD_COUNT];

  int realExcuteIdx = rand() % THREAD_COUNT;
  for (int idx = 0; idx < THREAD_COUNT; ++idx)
  {
    bool bRealExcute = (realExcuteIdx == idx);
    results[idx] = std::async(FindFirstRepeatElementIndex, bRealExcute);
  }

  for (auto& one_result : results)
  {
    std::cout<< one_result.get() << std::endl;
  }

  return 0;
}

总结

  • 使用 process explorer 的线程相关功能, 在某些情况下, 我们甚至可以不用调试器,对照源码就可以找出问题所在。
  • visual studio 的并行调用栈可以让我们一次性看到所有线程的调用栈,很是方便。不像 Call Stack ,每次只能查看一个线程的调用栈。当然除了看所有线程的调用栈,还有更多用途等待大家挖掘。
  • 一般,如果一个线程的运行时间 大于 其它线程,这个线程很有可能是与死循环相关的线程。

  • windbg !runaway 命令可以查看每个线程运行的时间, 运行时间最长的线程会排在第一位。
  • ~*e ? $tid;.ttime 可以查看所有线程的运行时间。
  • ~ N s 切换到第 N 号线程。
  • ~~[TID]s 切换到 TID 对应的线程。

参考资料

  • 《格蠹汇编》

  • 《Windows Sysinternals 实战指南》

猜你喜欢:

[原]调试实战——使用windbg调试崩溃在ole32!CStdMarshal::DisconnectSrvIPIDs

[原] 调试实战 ——崩溃在ComFriendlyWaitMtaThreadProc

[原] 调试实战—— 调试PInvoke导致的内存破坏

[原] 调试实战—— 调试excel启动时死锁

[原] 调试实战—— 调试TerminateThread导致的死锁

[原] 调试实战—— 调试DLL卸载时的死锁

[原]排错实战——拯救加载调试符号失败的IDA

[原]排错实战——使用process explorer替换任务管理器

[原]排错实战——VS清空最近打开的工程记录

[原] 排错实战—— 解决Tekla通过.tsep安装插件失败的问题

[原]排错实战——你知道拖动窗口时只显示虚框怎么设置吗?

[原]排错实战——通过对比分析sysinternals事件修复程序功能异常

你知道怎么使用DebugView查看调试信息吗?

b2Q7ryq.jpg!web

欢迎留言交流


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK