使用Process Explorer查看线程的函数调用堆栈去排查程序高CPU占用问题

目录

1、问题描述

2、使用Process Explorer排查软件高CPU占用的一般思路

3、使用Process Explorer工具进行分析

3.1、找到CPU占用高的线程

3.2、查看CPU占用高的线程的函数调用堆栈,找到出问题的代码

3.3、libwebsockets库导出接口lws_service的说明

3.4、解决办法

4、使用Process Explorer查看函数调用堆栈时可能需要pdb符号文件

5、最后


VC++常用功能开发汇总(专栏文章列表,欢迎订阅,持续更新...)icon-default.png?t=N7T8https://blog.csdn.net/chenlycly/article/details/124272585C++软件异常排查从入门到精通系列教程(专栏文章列表,欢迎订阅,持续更新...)icon-default.png?t=N7T8https://blog.csdn.net/chenlycly/article/details/125529931C++软件分析工具从入门到精通案例集锦(专栏文章正在更新中...)icon-default.png?t=N7T8https://blog.csdn.net/chenlycly/article/details/131405795C/C++基础与进阶(专栏文章,持续更新中...)icon-default.png?t=N7T8https://blog.csdn.net/chenlycly/category_11931267.html       前几天使用Process Explorer工具在同事的电脑上排查了一个软件高CPU占用问题,今天正好通过这个实例来完整地讲述如何使用Process Explorer去排查高CPU占用问题。

1、问题描述

       前几天同事在使用我们的软件时,系统出现明显的卡顿,查看Windows资源管理器得知,我们的软件进程占用了将近50%的CPU,所以导致系统出现了较明显的卡顿。查看软件的界面,显示正在重连服务器:

        同事使用的是笔记本电脑,据同事反馈,他之前连的是手机热点(通过手机的移动流量联网),后来他将手机上的热点关闭了,所以笔记本就连不上网络了。软件中连接了多个业务服务器,断网后,与服务器的连接就会陆续断开,软件中会自动发起对各服务器的重连,因为笔记本无法连接外网了,所以软件底层一直在不停地重连。这个高CPU占用可能是服务器自动重连触发的这个高CPU问题,在这个问题场景下是必现的,简单操作一下就能复现。

2、使用Process Explorer排查软件高CPU占用的一般思路

       我们一般在遇到软件高CPU占用时会优先使用Process Explorer工具来排查。使用该工具可以查看到进程中的所有线程,每个线程占用的CPU比例:

​然后双击CPU占用最高的那个线程,查看线程的函数调用堆栈,对照着源码,一般就能分析出高CPU占用问题了。

       导致高CPU占用一般是程序一直在不停歇的执行代码导致的,不停歇地执行代码的场景主要有以下几种:

1)程序中出现了死循环,程序一直在执行发生死循环的循环体中的代码。
2)线程函数中的循环体(通常是While循环)中没有添加Sleep,导致循环体代码一直在不停歇的执行。

所以排查程序高CPU占用问题时,主要考虑这两个排查方向。

3、使用Process Explorer工具进行分析

3.1、找到CPU占用高的线程

       在出问题的电脑上,打开Process Explorer,在进程列表中找到目标软件进程,双击该进程条目打开进程的属性页面,点击Threads标签页,就能看到线程列表页面。

       一般进程高CPU占用是由某个线程引发的,该线程会有明显的高CPU占用。可以在线程列表中点击表头中的CPU占用列,按照CPU占用比例排序,可以看到某个线程明显占用了较高的CPU,如下所示:

​双击高CPU占用的那个线程条目,查看该线程的函数调用堆栈,如下:

​       有时,我们需要多次点击左下角的refresh刷新按钮,查看多次函数调用堆栈,以查看到有效的函数调用堆栈。一般情况下,代码发生死循环或者线程函数中while循环出现不间断执行代码,使用Process Explorer多次查看到的函数调用堆栈都是类似的或者一样的。

3.2、查看CPU占用高的线程的函数调用堆栈,找到出问题的代码

       通过显示的函数调用堆栈,wsswrapperlib库在调用开源库libwebsockets中的接口。这个wsswrapperlib库是底层协议栈的模块,据了解当前的客户端软件与平台侧的某个服务器就是通过libwebsockets进行通信的,从当前堆栈上看,一直在connect远端的服务器。这个和界面层显示的正在重连服务器是一致的,从堆栈中看到wsswrapperlib模块中的WorkerThread字样,这说明当前的函数调用堆栈就和这个WorkerThread线程有关。

       之前我也遇到过调用libwebsockets的lws_service不当,导致程序高CPU占用的问题,当时问题也是出在自动重连服务器的场景下。对应的问题可以参见我之前写的文章:
使用Process Explorer和Clumsy工具定位软件高CPU占用问题icon-default.png?t=N7T8https://blog.csdn.net/chenlycly/article/details/130038272        于是找到维护wsswrapperlib模块的协议栈同事,让他直接在代码中搜索lws_service接口,果然找到了一个线程对应的线程函数WorkerThread,在线程函数中的While循环中调用了lws_service接口:

static void* WorkerThread(void* lpParam)
{
    int n = 0:
    CWSSContext* context = (CWSSContext*)1pParam;
    while (n >=0
#ifdef __ANDROID__
        && context->flag()
#endif
    )
    {
        n = lws_service( context->getContext,50 )
        context->po11();
    }

    return NULL;
}

3.3、libwebsockets库导出接口lws_service的说明

libwebsockets开源库中关于lws_service接口的说明如下:

/**
 * lws_service() - Service any pending websocket activity
 * @context:    Websocket context
 * @timeout_ms:    Timeout for poll; 0 means return immediately if nothing needed
 *        service otherwise block and service immediately, returning
 *        after the timeout if nothing needed service.
 *
 *    This function deals with any pending websocket traffic, for three
 *    kinds of event.  It handles these events on both server and client
 *    types of connection the same.
 *
 *    1) Accept new connections to our context's server
 *
 *    2) Call the receive callback for incoming frame data received by
 *        server or client connections.
 *
 *    You need to call this service function periodically to all the above
 *    functions to happen; if your application is single-threaded you can
 *    just call it in your main event loop.
 *
 *    Alternatively you can fork a new process that asynchronously handles
 *    calling this service in a loop.  In that case you are happy if this
 *    call blocks your thread until it needs to take care of something and
 *    would call it with a large nonzero timeout.  Your loop then takes no
 *    CPU while there is nothing happening.
 *
 *    If you are calling it in a single-threaded app, you don't want it to
 *    wait around blocking other things in your loop from happening, so you
 *    would call it with a timeout_ms of 0, so it returns immediately if
 *    nothing is pending, or as soon as it services whatever was pending.
 */
 
LWS_VISIBLE int
lws_service(struct lws_context *context, int timeout_ms)
{
    return lws_plat_service(context, timeout_ms);
}

上述注释的翻译如下:

This function deals with any pending websocket traffic, for three kinds of event.  It handles these events on both server and client types of connection the same.
1) Accept new connections to our context's server
2) Call the receive callback for incoming frame data received by server or client connections.
此函数处理任何需要处理的(悬而未决的)websocket 流量,适用于三种事件。它以相同的方式处理服务器和客户端连接类型上的这些事件。
1) 接受到我们上下文服务器的新连接
2) 调用回调函数,将服务器或客户端连接接收到的数据,回调出去。

You need to call this service function periodically to all the above functions to happen; if your application is single-threaded you can just call it in your main event loop.
你需要周期性地调用这个服务函数来使上述所有函数发生; 如果您的应用程序是单线程的,您可以在主事件循环中调用它。

从上面的注释可以看出,要保证libwebsockets库中能正常的收发数据,必须要调用lws_service接口。

3.4、解决办法

       在服务器连不上时,调用lws_service函数传递的第二个timeout参数起不到Sleep的作用,我们不管lws_service函数内部会做什么,但lws_service函数会快速的返回,会导致While一直在持续的不停歇地执行,会占用大量的CPU时间片,导致线程占用大量的CPU比例。为了防止出现线程代码不停歇的运行,要在此处人为地添加一个Sleep,代码如下:

static void* WorkerThread(void* lpParam)
{
    int n = 0:
    CWSSContext* context = (CWSSContext*)1pParam;
    while (n >=0
#ifdef __ANDROID__
        && context->flag()
#endif
    )
    {
        n = lws_service( context->getContext,50 )
        context->po11();
        
        Sleep(50);  // 人为地去Sleep一下
    }

    return NULL;
}

一般在业务线程中都要人为地添加Sleep的调用,以防止线程不停歇运行导致高CPU占用问题

4、使用Process Explorer查看函数调用堆栈时可能需要pdb符号文件

       本例中涉及到的接口lws_service,是开源库libwebsockets.dll中的导出接口,导出接口对外部是公开可见的,所以函数调用堆栈中可以直接看到lws_service接口的调用。如果涉及到的接口是库内部的非公开的接口,要在函数调用堆栈中看到详细的函数名,则需要对应模块的pdb符号文件。可以将pdb符号文件事先放到二进制文件的同级目录(一般是程序的安装目录)中。Process Explorer在需要加载pdb文件时,会到exe主程序所在的目录中去搜索pdb文件,搜索到后去自动加载。

       当然,也可以将pdb符号文件统一放在某个目录中,然后将目录设置到Process Explorer中。具体操作方法是,点击Process Explorer菜单栏中的Options -> Configure Symbols...,打开如下的配置窗口:

​配置pdb文件的路径即可。

       还有其他工具可能会用到pdb符号文件,可以参见我之前写的文章:

哪些软件分析工具需要使用到pdb符号文件?icon-default.png?t=N7T8https://blog.csdn.net/chenlycly/article/details/131574418

5、最后

       本文通过一个具体的实例讲述了如何使用Process Explorer排查程序高CPU占用问题,有一定的参考或借鉴价值。本例中使用Process Explorer查看进程中某线程的函数调用堆栈,Process Explorer工具还有其他的用处,Process Explorer的用途可以总结为:

1)Process Explorer可以查看程序整个进程占用的总的虚拟内存,而Windows任务管理器中看不到,在排查内存泄漏时可以用上。
2)Process Explorer可以看目标程序加载了哪些库以及这些库的路径,还可以看动态加载的dll库有没有加载起来。
3)Process Explorer可以看启动程序时给程序进程传递了哪些命令行参数,比如chrome浏览器运行时会启动多个进程,每个进程负责处理不同的事务,我们可以通过给进程传递的命令行参数得知这个进程是做什么任务的,比如render渲染进程和GPU加速线程。
4)Process Explorer可以看启动程序各个线程的函数调用堆栈,排查死循环、高CPU占用和多线程死锁问题。

我博客中有多个使用Process Explorer排查问题的案例,可以参见专栏《C++软件分析工具案例集锦》中的相关文章:

C++软件分析工具从入门到精通案例集锦(专栏文章正在更新中...)icon-default.png?t=N7T8https://blog.csdn.net/chenlycly/article/details/131405795

猜你喜欢

转载自blog.csdn.net/chenlycly/article/details/132830803
今日推荐