先说一个我经常碰到的场景:告警群里突然刷出一条消息,某台4核机器上的Java进程CPU吃到了接近400%。我登录上去,第一反应是执行ps -ef | grep java,结果只看到孤零零的一行Java进程,没有线程,也没有实时CPU列。进程明明忙得不行,但我连"里面哪条线程在忙"都看不出来。Linux如何查看线程,这个问题在书本上是个概念题,在线上它就是一道生死题。
这篇内容是我结合多次实际排查经历整理出来的。我会把最常用的几条查看线程命令、它们在输出上的关键区别、内核里线程到底是怎么表示的原理,以及一套从CPU飙高到锁定"罪魁线程"的完整排查链路全部讲清楚。无论你是刚接触Linux的运维,还是写后端写了几年的开发,只要平时需要跟进程打交道,这篇文章都值得认真看一遍。
1. 为什么ps -ef里看不到线程:先补上进程与线程的分界认知
1.1 一次CPU告警引发的困惑
先说回开头那个场景。同事执行ps -ef | grep java之后,看到的是类似这样的输出:
root 2300 2299 0 14:00 ? 00:02:11 java -Xmx2g -jar order-service.jar只有一条记录,PID是2300。ps -ef是静态快照,它根本不展示实时CPU占用率,更不会把进程内部的线程拆成多行显示。于是我们从"知道进程有问题"到"看出是哪个线程有问题",中间断了一截。很多人到这里就卡住了,只能去重启或者让开发加日志,然后反复试。
这个困惑的本质,不是命令不够多,而是我们默认了"进程就是可以被观察的最小单位"。在Linux上,这个默认是错误的。
1.2 线程不是"进程的子进程"
Linux内核对待进程和线程的态度,和Windows差异很大。在Linux里,线程实际上也对应一个任务(task),内核同样用一个task_struct结构体来管理它。线程是通过clone()系统调用创建的,只不过创建时带了CLONE_THREAD、CLONE_VM、CLONE_FILES这些标志位,表示要和创建它的那个任务共享地址空间、文件描述符表、信号处理函数等资源。
因为共享了这么多东西,我们才把这些内核任务叫做线程。而英文资料里更准确的说法是轻量级进程,LWP(Light Weight Process)。所以"线程"在Linux内核眼里并不是"进程的儿子",而是"和你平级、但共享家产的另一张身份证"。主线程的身份证号恰好和进程号一样,其他线程的身份证号则各不相同。
用生活类比来说:进程是一个办公室,线程是办公室里各干各活的员工。ps -ef就像前台那张访客登记表,只记录哪个办公室有人来过,不会把办公室里每个人每天做了什么细节都登记上去。想看具体员工名单,你得换一份更细的表格,这就是后面几节要讲的命令。
1.3 会查线程,是解决性能问题的分水岭
进程层面的信息太粗了。一个应用开了50条线程,其中只有1条在死循环,单核CPU就能被吃到100%。从进程层面看,你只能得出"这个进程很忙"的结论,完全看不出是50条线程都在忙,还是1条线程在独奏。如果不具备查看线程的能力,后续所有排查都只能靠猜。
尤其像JVM这种多线程大户,GC线程、线程池线程、定时器线程、网络IO线程混在一起,动辄几十上百条。没有查看线程的能力,CPU飙高时你只能盯着进程号发呆。会从进程走进线程,是我认为Linux性能排查的第一个分水岭,过了这个坎,后面很多手段才能真正落地。
2. 四条命令,四种看法:top -H、ps -eLf、htop与pidstat -t实测对比
2.1 top -H:最快看到进程内部的线程
如果你已经定位到有问题的进程PID,最快的下一步就是:
top -H -p 2300这里的-H让top进入线程模式。进入之后,原来显示进程PID的那一列,此刻显示的是每个线程的TID(Thread ID),每个线程单独占一行,包括主线程。默认按CPU%从高到低排序,所以最耗CPU的线程通常出现在最顶部。
操作细节上要注意:进入top后再按一下H键,可以在"进程视图"和"线程视图"之间切换,这个交互快捷键在有些终端环境下容易误触,脚本或远程排查时更推荐直接带-H参数。如果想看某个进程内部的总线程数,可以按f键进入字段管理,把NLWP那一列选出来显示,它表示该进程当前拥有的线程数量。
需要注意的是,top默认情况下单线程的CPU%在IRIX模式下可能显示超过100%,按I键可以切换成Solaris模式,单线程最大显示100%。实际排查时重点看哪些TID显著偏高,相对值比绝对值更重要。
2.2 ps -eLf:把系统里所有线程摊开
ps -eLf是另一个高频命令:
ps -eLf | grep 2300它把系统里所有进程的所有线程都铺开展示。输出里最关键的是三列:
PID:进程ID,也就是主线程所在的那个进程号。LWP:线程ID,轻量级进程ID,这才是线程的真名。NLWP:这个进程一共拥有的线程数量。
还有一个容易被忽略的C列,它是内核调度器维护的一个粗略CPU利用率,不是top那种实时刷新值,参考意义有限。想看实时CPU,还是用top或者接下来要说的pidstat更靠谱。
如果只想看某一个进程,更精简的写法是:
ps -L -p 2300 -o pid,tid,pcpu,comm这里%CPU也是进程启动以来的平均CPU占用,不是瞬时值。ps -T -p 2300的效果和ps -L -p 2300类似,都属于"指定进程线程列表"的专用形态。用ps -eLf的好处是脚本友好,输出可以直接用管道处理,比如快速过滤某个进程的所有线程。
2.3 htop:带树的线程视图
htop是top的增强版,默认不一定安装,需要根据发行版自行安装。它最大的优点是可视化,按F5进入树形视图后,同一个进程下面的线程会以子节点的形式展开,层次关系一眼就能看清。
进入htop后,默认视图其实会混着显示进程和线程,可以在设置里把线程展示调整为更舒服的模式。F4支持按关键字过滤,F6可以选择排序字段。当线程数量特别多的时候,htop的树形展示比top的一屏列表直观得多,尤其在给同事或领导解释问题现场时,一张htop截图比一段top文字输出更有说服力。
不过htop这样的交互式工具不太适合自动化脚本,更多是给人看的。
2.4 pidstat -t:适合持续采样的线程统计
如果你需要长时间观察线程的CPU波动,pidstat是更专业的选择。它来自sysstat包,需要先安装。用法:
pidstat -t -p 2300 1输出中会多出TGID和TID两列。TGID是线程组ID,其实就是进程PID;TID是线程ID。后面的%CPU是采样周期内该线程的实时CPU占用,比ps的累计平均值更有参考价值。把间隔和次数加进命令,就可以边采集边记录:
pidstat -t -p 2300 5 10 > /tmp/thread_cpu.log这会在50秒内采样10次,每次间隔5秒,落盘后可以慢慢分析。
2.5 四个命令到底怎么选
| 命令 | 适用场景 | 输出粒度 | 优势 | 注意点 |
|---|---|---|---|---|
top -H -p PID | 线上快速定位某进程内CPU最高的线程 | 单进程内线程 | 实时刷新、自带排序 | 交互界面,脚本化不便 |
ps -eLf | 全量快照、脚本统计、排查线程总数 | 系统全部线程 | 静态完整,方便管道处理 | 不刷新,CPU列非实时 |
htop | 可视化查看线程树、向人解释现场 | 系统全部进程/线程 | 树形结构直观 | 需要额外安装 |
pidstat -t | 持续采样线程CPU、生成趋势数据 | 指定进程内线程 | 可脚本化,适合留证据 | 需要sysstat包 |
我的选择逻辑很直接:临时看现场用top -H -p;留证据、做统计用ps -eLf;需要在窗口里反复观察和解释用htop;要采集线程CPU趋势数据就用pidstat -t。没有什么"最强命令",只有"匹配场景的命令"。
3. 内核视角的线程档案:TID、tgid,还有/proc/PID/task这个宝藏目录
3.1 每个线程都有一张独立的"身份证"
在Linux内核里,每个任务(进程或线程)的task_struct中都有两个容易混淆的字段:pid和tgid。
pid字段:当前任务自己的ID。对线程来说,这就是线程ID,也就是TID。tgid字段:线程组的ID,也就是主线程的进程PID。
对于主线程,pid和tgid是相等的,所以你会看到主线程的TID恰好等于进程PID。用户态调用getpid()返回的是tgid,而gettid()返回的才是线程真正的pid字段。这就是为什么很多命令默认展示进程时,你能看到的"那一行"其实是主线程;非主线程因为身份证号和进程号不同,默认就不展示。
下面的例子能帮你建立直觉:进程PID=2300,内部有4条线程,它们的TID可能是2300、2301、2302、2303。主线程TID正好是2300,其余三条线程的TID都是独立编号。理解了这一层,再看ps -eLf里PID列和LWP列的关系,就不会觉得奇怪了。
3.2 /proc/PID/task:文件系统里的线程目录
Linux把大量内核状态暴露在 /proc 伪文件系统里。每个进程的线程信息,就躺在/proc/PID/task/目录下:
ls /proc/2300/task你会看到像2300 2301 2302 2303这样的数字目录,每一个都代表一条线程。进入某个线程目录,你能看到它的完整状态:
cat /proc/2300/task/2301/comm cat /proc/2300/task/2301/statuscomm文件里是这个线程的名字,status里则有这个线程的运行状态、内存信息等。想知道进程总共有多少线程,更快的命令是:
grep Threads /proc/2300/status输出类似Threads: 4,一目了然。
3.3 一条脚本,把线程ID和线程名对应起来
/proc这种方式在写脚本时特别好用,因为它不需要额外fork一个ps进程去解析文本。下面这条命令可以列出某个进程的所有线程ID和线程名:
for tid in /proc/2300/task/*; do echo "$(basename $tid) $(cat $tid/comm)" done如果进程线程非常多,想按CPU时间排个序,可以借助线程的stat文件。注意:/proc/PID/task/TID/stat的第14个字段是utime,即用户态累计CPU时间,可用作粗略排序依据:
for tid in /proc/2300/task/*; do printf "%s %s %s\n" "$(basename $tid)" "$(cat $tid/comm)" "$(awk '{print $14}' $tid/stat 2>/dev/null)" done | sort -k3 -nr | head -20这里有一个小坑:如果线程名里含有空格,awk取字段时会发生偏移。我的建议是把线程名都设置成不含空格的单词,这本身也是好的命名习惯。
提示:
/proc/PID/task下每个数字目录就是一条线程,目录项数量减去特殊情况下的开销,基本就等于进程线程总数。如果你发现实际线程数明显大于你配置的线程池数量,那多半是有线程创建后没有被回收,这就是线程泄漏的前兆。
4. CPU飙高400%之后:从PID到TID再到jstack的完整排查流程
4.1 第一步:top先锁定进程PID
假设现场是4核机器,执行top后看到Java进程PID 2300,CPU占用约375%。此时先不要急着杀进程,第一步是把PID记录下来。接下来所有操作都围绕这个PID展开。
4.2 第二步:top -H -p 定位线程TID
执行:
top -H -p 2300输出里原本的PID列变成了TID列。我盯着看了一会儿,发现TID 2347的CPU一直稳定在100%左右,而其他线程基本都在个位数徘徊,TIME+列也在持续增长。到这个程度,嫌疑线程基本就锁定在2347了。
这里说一个经验:当某个线程的CPU长期稳定在接近100%,其他线程都相对安静时,优先怀疑这条线程是死循环或者密集计算;如果所有线程CPU都不高但进程总CPU高,那有可能是线程频繁创建销毁,或者有大量短生命周期任务在排队。
4.3 第三步:把TID转成十六进制
很多人在这一步卡住:明明在top里看到了TID是2347,但Java线程转储文件jstack里全是nid=0x92b这种十六进制值,二者对不上。其实只需要一次进制转换:
printf "%x\n" 2347输出是92b。匹配的时候要在前面补上0x,也就是jstack里的nid=0x92b。这个步骤为什么必须做?因为JVM的线程转储规范就是用十六进制表示线程ID的,你不转换就永远无法在jstack里精准定位。
4.4 第四步:jstack转储,按nid匹配线程栈
执行线程转储并把结果保存下来:
jstack 2300 > /tmp/tstack.log然后用grep抓取目标线程的栈段:
grep -A 20 "nid=0x92b" /tmp/tstack.log这段输出会告诉你这个线程的线程名,以及它此刻的Java调用栈。例如栈里如果反复出现业务代码里的某个类名和方法名,那基本就是元凶所在;如果栈停在类似网络IO的native方法上,通常只是正常的等待线程,CPU占用不太可能长期100%。对照栈帧回到代码里,定位效率会非常高。
4.5 Java之外的场景怎么办
jstack是Java专属工具,C/C++、Go或者其他语言怎么办?同样思路,只是工具不同:
pstack 2300:打印进程所有线程的调用栈,比gdb attach更轻量。gdb -p 2300:进入后执行thread apply all bt打印所有线程栈。注意gdb attach会短暂暂停进程,线上环境要谨慎。perf record -t 2347 -g -- sleep 5:按线程ID采样,随后perf report查看热点函数调用链。strace -p 2347:跟踪指定线程的系统调用,但会让该线程明显变慢,适合在低峰期或测试环境使用。
不管什么语言,方法论是一致的:先通过top找到进程PID,再用某种线程视图找到异常的TID,最后用该语言生态对应的工具,把线程栈、热点或者系统调用捞出来,建立"TID → 线程上下文 → 代码逻辑"的映射。
5. 线程名、线程数与持续采样:容易踩的坑和拿来即用的脚本
5.1 线程名到底怎么查
很多人在top -H里看到的COMMAND列清一色是进程名,比如全都是java,这是不是bug?不是。很多线程根本没有设置自己的线程名,内核就会拿进程的可执行文件名或者进程名作为它的comm。想看真实的线程名,两个办法:
cat /proc/2300/task/2347/comm在应用代码里,也强烈建议主动设置线程名。C语言可以用pthread_setname_np(),注意Linux下线程名最多15个字节;Java在较新版本里可以通过Thread.ofPlatform().name("worker-1").start(...)这种方式来命名。线程名一旦设置好,配合top -H或pidstat -t,一眼就能看出是哪个业务线程在消耗CPU,排查成本会直线下降。
5.2 统计线程数时容易踩的两个坑
统计系统线程总数时,如果直接写:
ps -eLf | wc -l结果会多算1,因为第一行是表头。想统计单个进程的线程数,更稳的方式是:
ps -L -p 2300 --no-headers | wc -l或者直接看ps -L -p 2300 -o nlwp的NLWP列。还有一个细节:/proc/PID/task目录里的数字子目录就是一条条线程,用ls /proc/2300/task | wc -l统计也是可靠的。
线程泄漏的排查思路也可以从这里切入:定时采集进程的NLWP,如果这个数字随时间单调上涨且不回落,大概率是线程创建后没有正确关闭,最终会打到操作系统的线程数上限,导致新线程创建失败。想快速找到系统里"线程大户":
ps -eLo pid,lwp,nlwp,comm --sort=-nlwp | head -20这条命令按NLWP降序排列,线程数最多的进程会排在最前面,是排查线程泄漏时常用的第一筛。
5.3 持续采样的正确姿势
单次top -H只能看到当前瞬间,如果要评估一个线程的CPU趋势,要持续采集:
pidstat -t -p 2300 5 10 > /tmp/thread_cpu_$(date +%Y%m%d).log采样结束后,可以用awk按TID维度汇总,看看哪些线程反复出现在CPU消耗名单里。需要注意的是,不同版本pidstat的输出列位置有差异,写解析脚本前先跑一条看准列号再动手。这种来自线上真实数据的日志,比事后的"我感觉"靠谱得多。
5.4 一个小脚本,替代反复截top
除了前面按utime排序的脚本,我还会用下面这条命令,把某个进程所有线程的当前状态存成快照:
for tid in /proc/2300/task/*; do echo "TID:$(basename $tid) NAME:$(cat $tid/comm 2>/dev/null) STATE:$(awk '{print $3}' $tid/stat 2>/dev/null)" done这里的STATE是内核里的线程状态(R表示运行,S表示睡眠,D表示不可中断睡眠等)。把它存到文件里,隔几秒再存一次,对比状态变化,比反复敲top然后人肉记忆要可靠得多。
最后再说一点我个人的体会
排查线程问题,我的顺序永远是先想清楚"我要看什么":是看线程的CPU占用,还是看线程在等什么锁,还是看线程数有没有泄漏,还是看线程名是什么?目的不同,选用的工具完全不同。top -H -p解决"谁在抢CPU",ps -eLf解决"全量快照和线程数",/proc/PID/task解决"脚本化细看每个线程",jstack和perf解决"这个线程到底在干什么"。把这些串起来,就形成了一条从进程到代码层面的完整链路。下次再遇到CPU飙高,别再盯着进程层面发愁了,线程就在那里,等着你去查。