扫码加入

  • 正文
  • 相关推荐
申请入驻 产业图谱

嵌入式 Linux | 进程内线程资源占用排查方法!

03/03 10:26
331
加入交流群
扫码加入
获取工程师必备礼包
参与热点资讯讨论

大家好,我是杂烩君。

之前有个小伙伴找我帮忙看一个问题:板子跑着跑着CPU就飙上去了,用top一看,确实是他的进程占了大头。但问题是他那个进程里面跑了十几个线程,光知道是这个进程没用,得定位到具体是哪个线程在作妖才行。

这其实是嵌入式Linux开发中很常见的场景。今天就借这个话题,聊一聊怎么查看某个进程中各个线程的资源占用情况。

先准备一个多线程示例程序

为了演示方便,我们写一个简单的多线程程序。这里用了表驱动的方式来管理线程,实际项目中也推荐这么做,线程多了以后管理起来比较清爽。

multi_thread.c:

#define _GNU_SOURCE
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

#define APP_THREAD_NAME_MAX_LEN     16

typedefenum _app_thread_index
{
    APP_THREAD_INDEX_TEST0,
    APP_THREAD_INDEX_TEST1,
    APP_THREAD_INDEX_TEST2,
    APP_THREAD_INDEX_TEST3,
    APP_THREAD_INDEX_TEST4,
    APP_THREAD_INDEX_TEST5,
    APP_THREAD_INDEX_MAX
}app_thread_index_e;

typedefstruct _app_thread
{
    pthread_t thread_handle;
    char name[APP_THREAD_NAME_MAX_LEN];
}app_thread_s;

app_thread_s s_app_thread_table[APP_THREAD_INDEX_MAX] =
{
    {0, "test0_thread"},
    {0, "test1_thread"},
    {0, "test2_thread"},
    {0, "test3_thread"},
    {0, "test4_thread"},
    {0, "test5_thread"}
};

static void *common_thread_entry(void *param)
{
    app_thread_s *self = (app_thread_s *)param;
    printf("%s running...n", self->name);

    while (1)
    {
        usleep(2 * 1000);
    }
    
    returnNULL;
}

static int create_all_app_thread(void)
{
    int ret = 0;

    for (int i = 0; i < APP_THREAD_INDEX_MAX; i++)
    {
        ret = pthread_create(&s_app_thread_table[i].thread_handle, NULL, 
                             common_thread_entry, &s_app_thread_table[i]);
        if (0 != ret)
        {
            printf("%s create error!n", s_app_thread_table[i].name);
            return ret;
        }

        printf("%s create success!n", s_app_thread_table[i].name);
        pthread_setname_np(s_app_thread_table[i].thread_handle, 
                           s_app_thread_table[i].name);
        pthread_detach(s_app_thread_table[i].thread_handle);
    }

    return ret;
}

int main(int argc, char **argv)
{ 
    create_all_app_thread();
    
    while (1)
    {
        usleep(2 * 1000);
    }

    return0;
}

编译运行:

gcc multi_thread.c -o multi_thread -lpthread
./multi_thread &

程序跑起来之后,就可以用下面几种方法来监控各线程的情况了。

方法一:top -H(最常用)

top大家肯定不陌生,加上 -H 参数就能把线程展开来看。再配合 -p 指定进程号,就能只看我们关心的那个进程:

top -H -p `pidof multi_thread`

 

注意命令中 pidof 两边用的是反引号 ` ,不是单引号。反引号在键盘左上角,ESC键下面那个。

它的作用是把 pidof multi_thread 的执行结果(也就是PID)传给 top。

运行效果:

简单说下输出中几个关键列的含义:

PID:这里显示的其实是线程ID(LWP),不是进程ID

%CPU:该线程的CPU占用率,排查CPU高占用就盯着这列

TIME:线程累计使用的CPU时间,谁的时间长谁就是大户

COMMAND:线程名称,这个是通过 pthread_setname_np 设的这也是实际调试中我用得最多的方式,简单直接。

方法二:ps -T 快照查看

top是实时刷新的,如果你只想看某一时刻的线程状态,或者想在脚本里用,ps 会更方便:

ps -T -p `pidof multi_thread`

-T 参数会显示进程下的所有线程。输出中 SPID 列就是各线程的LWP(轻量级进程号)。

想看更详细的信息,可以自定义输出列:

ps -T -p `pidof multi_thread` -o spid,comm,%cpu,time

这样一条命令就能看到线程号、线程名、CPU占用和累计时间。

方法三:pidstat 按线程采样

pidstat 是 sysstat 工具包里的命令,做性能分析的时候使用。它可以按固定间隔采样线程的CPU使用率:

pidstat -t -p `pidof multi_thread` 1

-t 表示显示线程级信息,最后的 1 是每隔1秒采样一次。

pidstat的好处在于它的输出是带时间戳的,而且格式比较规整,方便你做一段时间的采样后分析趋势。比如你怀疑某个线程有间歇性的CPU毛刺,用pidstat挂着采几分钟就很容易发现。

方法四:直接读 /proc 文件系统

上面这些命令归根到底都是在读 /proc 文件系统的内容,我们也可以直接去翻。一个进程的所有线程信息都在 /proc/<pid>/task/ 目录下,每个线程一个子目录:

ls /proc/`pidof multi_thread`/task/

进到某个线程目录里,常用的几个文件:

# 查看线程名
cat /proc/<pid>/task/<tid>/comm

# 查看线程状态(包含运行状态、优先级等)
cat /proc/<pid>/task/<tid>/status

# 查看线程的CPU时间统计(utime、stime字段)
cat /proc/<pid>/task/<tid>/stat

这种方式看着原始,但在一些资源受限的嵌入式环境里(比如连top都裁掉了的精简系统),直接读proc可能是唯一的选择。写个shell脚本定时采集这些信息,也能做简易的监控。

关于线程命名,多说几句

前面的代码里用了 pthread_setname_np 来给线程设名字,这一步在实际项目中非常建议加上。如果不设置,top -H看到的所有线程名都会跟进程名一样,根本分不清谁是谁:

全是 multi_thread,出了问题想定位就两眼一抹黑。

使用 pthread_setname_np 时有几点要注意:

1)线程名最长15个字符

Linux内核限制线程名(comm字段)最多16字节,算上末尾的  就是15个可用字符。超过这个长度 pthread_setname_np 会返回 ERANGE 错误。所以命名要尽量简短,像 mqtt_recvcan_parse 这样就挺好。我在前面的代码中把 APP_THREAD_NAME_MAX_LEN 定义为16也是出于这个考虑。

2)pthread_setname_np vs prctl

设线程名还有另一个方法——prctl(PR_SET_NAME, "thread_name")。区别在于 prctl 只能设置调用者自己的线程名,而 pthread_setname_np 可以给任意线程设名字(只要你有它的thread handle)。在创建线程的时候从外部统一命名,用 pthread_setname_np 更方便。

3)养成命名的好习惯

不少项目代码里线程创建完就不管了,名字都是默认的。等到出问题要排查的时候就麻烦了。建议在项目初期就把线程命名规范定下来,后面受益无穷。

写在最后

简单总结一下今天介绍的几种方法:

    top -H:实时查看,最直观,日常调试首选ps -T:快照式查看,脚本中使用方便pidstat -t:定时采样,适合分析CPU趋势和毛刺问题/proc/<pid>/task/ :最原始的方式,精简系统下的兜底方案

实际使用中根据场景灵活选就行,不用拘泥于某一种。

以上就是本次的分享,如果觉得有帮助的话,欢迎点赞、在看、转发三连支持!

相关推荐

登录即可解锁
  • 海量技术文章
  • 设计资源下载
  • 产业链客户资源
  • 写文章/发需求
立即登录

本公众号专注于嵌入式技术,包括但不限于C/C++、嵌入式、物联网、Linux等编程学习笔记,同时,公众号内包含大量的学习资源。欢迎关注,一同交流学习,共同进步!