大家好,我是杂烩君。
之前有个小伙伴找我帮忙看一个问题:板子跑着跑着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_recv、can_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/ :最原始的方式,精简系统下的兜底方案
实际使用中根据场景灵活选就行,不用拘泥于某一种。
以上就是本次的分享,如果觉得有帮助的话,欢迎点赞、在看、转发三连支持!
331