[原]调试实战——程序CPU占用率飙升,你知道如何快速定位吗?
前言
如果我们自己的程序的CPU Usage
(CPU占用率
)飙升,并且居高不下,很有可能陷入了死循环。你知道怎么快速定位并解决吗?今天跟大家分享几种定位方法,希望对你有所帮助。
如何判断是否有死循环?
-
通过电脑风扇的声音猜测。
如果风扇一直响个不停,说明电脑很热。高
CPU占用率
会导致CPU
发热量增大,从而导致风扇狂响。如果听到风扇响个不停,可以打开任务管理器看看CPU占用率
是不是很高。如果发现是我们的进程导致的高CPU占用率
,那么可以进一步查看是不是有死循环。 -
通过
CPU占用率
来判断。对于多核
CPU
(尤其是性能强劲的CPU
),一个核心的满负荷运转,并不会立刻导致CPU
发热量明显增大,风扇可能不会有明显响动。这时根据风扇声音不能轻易判断出是否有死循环,但是我们可以通过CPU占用率
来判断。如果
CPU
是单核的,那么当CPU
处于满负荷运转状态,CPU占用率
会接近100%
。如果CPU
是4
核的,并且这4
个核心都处于满负荷运转状态,那么CPU占用率
会接近100%
,如果只有一个核心是满负荷运转状态,那么CPU
占用率会在25%
(100 / 4 = 25
)左右。如果我们发现某个进程的CPU占用率
居高不下,有可能是死循环了。{% note info %}
注意: 很多死循环都是busy
类型的,如果是idle
类型的死循环,上面的方法不适用。
{% endnote %}
下面介绍几个我经常使用的工具,可以比较便捷的排查此类的问题。
1. process explorer
在前面的文章里跟大家介绍过,使用process explorer
可以查看线程的调用栈
及CPU占用率
。如果程序里的某个功能迟迟不能完成,我的第一反应是,按Ctrl + Shift + Esc
打开任务管理器(我已经使用process explorer
替换了系统自带的任务管理器,所以启动的是process explorer
。如何使用process explorer
替换系统自带的任务管理器,请参考文章排错实战——使用process explorer替换任务管理器)。
启动process explorer
后,双击我们关心的进程,切换到Thread
页,在这里我们可以看到当前进程中的所有线程。双击某个线程就可以查看调用栈,在弹出的调用栈界面,点击左下角的Refresh
按钮可以刷新。
如果每次刷新都能看到某个函数,很有可能是在这个函数中出现了死循环。对照源码,也许能直接能看出原因。
{% note info %}
注意: 需要正确加载调试符号才可以看到对应的函数名。
{% endnote %}
2. windbg
如果不能使用process explorer
定位到具体的原因,可以使用windbg
附加到进程中进行更深入的调查。我们需要找出哪个线程运行的时间最长,因为一般死循环的线程占用的CPU
时间会比较长。应该怎么找呢?🤔
-
使用
.ttime
命令.ttime
可以查看当前线程的运行时间(用户态运行时间和内核态运行时间)。但是.ttime
有个不足之处——没有输出相关的线程标识。我们需要根据其它信息来获取当前线程的标识。
如果想查看所有线程的运行时间怎么办呢?当然可以手动切换到另外一个线程,然后执行
.ttime
。如果线程数量很多的话,这可是个体力活。不要怕,我们可以通过命令~*e .ttime
来获取每个线程的运行时间。因为.ttime
输出结果中没有线程标识,我们需要执行命令~*e ? $tid;.ttime
把对应的线程ID
一起输出。
简单向大家解释下这条命令:
~*e
会遍历所有线程并执行后面跟着的命令。其实,~*
就可以遍历所有线程,比如我们在前面的文章里用到的~* kvn
命令来查看所有线程的调用栈。但是对于某些命令,如果不加e
,windbg
可能不能正确解析,会报错。? $tid
评估表达式$tid
的值,?
在windbg
中表示Evaluate
的意思,会评估后面表达式的值。$tid
是伪变量,代表了当前线程的线程ID
。;
分号是命令分割符。.ttime
查看当前线程的运行时间。
整条命令的效果是:遍历每个线程,输出其对应的
线程ID
和运行时间。 -
如果觉得上面的命令太长了,还可以使用更简单的命令
!runaway
查看线程运行时间。
下面是我用!runaway
命令排查高CPU占用率
的屏幕录像。
3. visual studio
如果是正在开发的程序在运行过程中出现了死循环,我会考虑用vs
来附加到进程(如果进程是通过Ctrl + F5
启动的话,并没有被调试)。然后通过Parallel Stacks
查看所有线程,并用肉眼查找可能出问题的线程。因为我不知道vs
中是否有类似!runaway
的命令。如果哪位小伙伴有更好的办法,请一定要留言告诉我!
{% note info %}
小提示:按CTRL + ALT + P
可以快速打开附加进程界面。
{% endnote %}
小结
以上三种工具,我会先使用process explorer
大体定位下问题,因为可以非常方便的通过Ctrl + Shift + Esc
启动。如果用process explorer
解决不了,我会根据情况使用windbg
或者vs
。如果vs
正开着(通常是正在写代码的时候),就顺手用vs
附加到对应的进程上。如果vs
没开着,当然会使用windbg
进行排查了。😎
实战代码
如果你想动手实战,复制下面的代码到工程里就可以实战了。
简单介绍下代码:
-
示例代码中启动了
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
可以查看所有线程的运行时间。- '~Ns' 切换到第
N
号线程。 ~~[TID]s
切换到TID
对应的线程。
参考资料
- 《格蠹汇编》
- 《Windows Sysinternals 实战指南》