排查 Linux 线程问题的时候,最怕的不是不会用命令,而是用错了命令看错了对象。很多刚接触 Linux 的同学,一上来就问“怎么查看线程”,但真正到了现场,往往连线程和进程的关系都还没理清楚——拿ps一顿输出,看到的全是进程 PID,压根儿没看到线程的影子,白白浪费半天时间。这篇文章就专门解决这个问题,把 Linux 下查线程的常用手段、适用场景和坑位一次讲透,适合刚入门的新手,也适合排查问题时需要快速回忆命令的老手。
1. 线程与进程:先把这个概念掰清楚
1.1 Linux 线程到底是怎么回事
在 Windows 上,线程和进程是两种非常明确的内核对象,打开任务管理器你既能看进程也能看线程。但 Linux 不一样,它对线程的实现方式很“取巧”——线程其实也是一种进程,只不过它和父进程共享了大部分资源(内存地址空间、文件描述符、信号处理等),内核里管这种共享资源的进程叫“轻量级进程”(LWP,Lightweight Weight Process)。
这个设计带来的直接后果就是:你平时用ps -ef查看系统状态时,默认看到的只是进程,不会单独列出线程。但这并不代表线程不存在,而是 Linux 把线程藏在了进程底下。要看到线程,就必须显式告诉命令“把每个线程也给我展示出来”,这就是-T、-L、-H这些参数的由来。
理解这一点非常重要,因为后面所有查看线程的命令,本质上都是在做同一件事:把进程内部那些 LWP 一个个单独列出来,并标注上线程 ID(TID,Thread ID)。
1.2 查看线程前的心态调整
排查线程问题,其实就是排查一种特殊的进程问题。线程占 CPU、线程卡死、线程数过多、线程栈异常——这些场景看起来各不相同,但底层都得靠一套命令去定位。所以我不建议你死记硬背一堆命令,而是建立一条清晰的排查链路:
先确认进程 PID → 再列出这个进程下的所有线程 → 观察线程状态和 CPU 占用 → 定位异常线程 ID → 分析这个线程的调用栈。
有了这条链路,不管工具怎么换,思路都不会乱。下面我会按照这条链路,把每步对应的命令和实际使用场景拆开讲。
2. 用 ps 命令快速查看线程
2.1 -T 参数:三秒快速上手
如果你只想用最少的参数快速看到某个进程下面的线程,ps -T是最直接的。比如我想看 PID 为 12345 这个进程下的线程:
ps -T -p 12345输出里会多出一个 SPID 列,这个 SPID 就是线程的 ID。除了线程 ID,你还能看到这个线程的 CPU 时间、状态等基本信息。这个命令的好处是简单,适合在没太多时间思考的时候先打个底,看看进程内部到底开了多少线程。
但要注意一点:ps -T的输出默认不会做任何排序,多个线程的排列顺序比较随意,想找最耗 CPU 的线程,你得配合--sort=-pcpu这类参数排序。
2.2 -L 参数:带 LWP 信息的进阶版
ps -L是另一个常用参数,它和-T的区别主要在于输出列名。用ps -L -p 12345查看时,输出里会有两列非常重要的信息:LWP 和 NLWP。LWP 就是前面说的轻量级进程 ID,也就是线程 ID;NLWP 表示这个进程下面总共开了多少个线程。
ps -L -p 12345 # 输出示例 PID LWP TTY TIME CMD 12345 12345 ? 00:00:01 main 12345 12346 ? 00:00:00 worker-1 12345 12347 ? 00:00:00 worker-2这个命令非常适合在检查“线程数是否符合预期”的时候用。比如某个服务的配置里写了最大线程池大小是 50,结果你用ps -L一看 LWP 有几百个,那很明显有问题,要么代码里的线程池没有回收,要么中间件内部偷偷起了额外线程。
如果只想统计线程数量,直接用wc -l管道处理即可:
ps -L -p 12345 | wc -l注意输出里包含标题行,实际线程数要减 1。当然也可以直接读/proc获取精确数值,后面会讲。
2.3 自定义输出字段:把你想看的信息全部列出来
默认输出虽然够用,但排查性能问题时远远不够。我更推荐自定义输出列,这样可以把 PID、TID、CPU 占用率、内存、状态、命令全部放在同一行,一眼就能定位问题:
ps -eLo pid,lwp,stat,pcpu,pmem,time,comm --sort=-pcpu这条命令会把系统里所有线程按 CPU 占用率从高到低排列,瞬间就能看到哪个线程在疯狂刷 CPU。如果你已经锁定了进程 PID,可以加上-p缩小范围:
ps -eLo pid,lwp,stat,pcpu,pmem,time,comm -p 12345 --sort=-pcpu有时候你会看到comm列显示的不是线程名而是进程名,这跟线程是否有独立的名称有关。真正规范的线程会通过pthread_setname_np()设置名字,比如 Java 线程会显示成pool-1-thread-2这种可读性很强的名字。
用ps查看线程还有一个实用场景:判断多线程程序是否真的在工作。有些网络服务主进程看起来一直在运行,但工作线程因为某种原因全部阻塞了,这时候你会看到很多线程状态为S(可中断睡眠)而没有一个R(运行中),这往往意味着请求堆积但处理能力已经停滞。
3. 用 top/htop 实时监控线程
3.1 top -H 与 H 键切换
ps是快照式查看,也就是某一瞬间的状态。但线程问题经常是动态的,比如某个线程每隔一段时间就飙高一次 CPU,这时候就得靠 top 这类动态监控工具。
查看实时线程最常用的命令是:
top -H -p 12345-H参数表示按线程模式显示,-p指定进程 PID。进入 top 界面后,按大写H可以在线程视图和进程视图之间切换。这一点非常实用,因为有时你只是照例看 CPU,突然发现整个进程占用很高,但分不清是哪个线程在捣鬼,就可以在 top 里按H展开看到具体的 LWP。
top 里看到的每行都代表一个线程,PID 列显示的其实是 TID,S列是线程状态,%CPU是该线程消耗的 CPU。如果top -p PID之后发现整个进程 CPU 高,立刻按H展开,通常就能定位到那几个疯跑的线程。
3.2 top 内排序与字段定制
top 进去之后默认按 CPU 占用排序,如果想按内存、时间等排序,按O可以设置排序字段,按f可以增删显示的列。对于线程排查,我一般关注这几个列:PID(线程ID)、%CPU、TIME+、S、COMMAND。
很多时候你会发现,一个进程总 CPU 占用 300%,但有几十个线程,每个只占 2%~5%,这说明负载比较均匀地分散在线程池里,属于正常情况。而如果总 CPU 占用 300%,其中某一个线程占 290%,那基本可以确定代码里存在线程内死循环或者单点热点逻辑。
3.3 htop 的树形视图更直观
除了 top,htop在查看线程方面也很顺手。如果系统里没装也没关系,多数发行版一条命令就能装好。htop 默认就会展示线程,只是线程默认是折叠在进程下面的。按t键可以切换到树形视图,父子关系一目了然;按H键可以切换是否隐藏用户线程。
htop 里还有一个好处是可以直接用鼠标点击选中某个线程,按下F5把这个线程下的调用关系展开成树。虽然这个操作对定位线程 ID 没有决定性帮助,但视觉上比 top 舒服很多,适合长时间挂在终端观察。
4. 用 pidstat 按线程统计 CPU 情况
4.1 基础用法
pidstat 是 sysstat 包里的工具,如果你没装过 sysstat,可能找不到这个命令。它的专业之处在于按时间间隔采样,比如每秒输出一次各线程的 CPU 使用率:
pidstat -t -p 12345 1 5-t表示显示线程级别的统计信息,1 5表示每秒采样一次,共采样 5 次。输出里会有 TID 列和 %CPU 列,比 top 更适合写进脚本做性能记录。
如果不想限制 PID,可以不带-p,让它输出整个系统所有线程的统计:
pidstat -t 1这在对比不同进程线程负载时会非常方便。
4.2 定位某个线程的 CPU 飙高问题
我排查过一个典型问题:某服务进程 CPU 持续飙高,但top加载进界面后因为线程数量太多,排序混乱,很难看清。这时候我直接用pidstat -t -p <PID> 1持续采样,日志里每个线程的 CPU 一目了然,很快就抓到某个 TID 稳定占用 80% 以上。
拿到 TID 之后,下一步就是看这个线程到底在干什么。可以用cat /proc/<PID>/task/<TID>/stack查看内核栈,或者用gdb附加进程打印用户态调用栈。再不行,还可以用perf top -t <TID>做性能采样,直接看到函数级别的热点。链路是这样的:先找到 TID,再深入分析,而不是一上来就瞎猜。
4.3 附加内存与上下文切换信息
pidstat 还能进一步展开:
pidstat -t -r -p 12345 1-r可以查看每线程的内存缺页统计,-w可以查看上下文切换次数。上下文切换次数异常升高,往往意味着线程数过多、锁竞争激烈或者线程在频繁让出 CPU。我用这个字段判断线程池是不是设置得过大,比单纯看线程数更可靠——因为有些线程数很多但排布合理,切换并不频繁;而有些线程数不多,却因为频繁抢锁导致上下文切换暴涨。
5. 用 /proc 文件系统查看线程细节
5.1 /proc/PID/task 目录结构
Linux 的/proc是一个虚拟文件系统,直接映射内核里的进程和线程结构,所以查看线程最底层的办法就是读/proc目录。
一个 PID 对应的所有线程,都挂在/proc/<PID>/task/下面。每有一个线程,这个目录里就有一个以 TID 命名的子目录。比如:
ls /proc/12345/task/输出里那些数字就是线程 ID。想统计线程数量:
ls /proc/12345/task/ | wc -l这个方法返回的就是精确的线程数,不像ps那样还要减标题行。而且/proc不受工具版本影响,无论在脚本里还是容器里都能稳定工作。
5.2 从 /proc 里读线程状态和栈
每个 TID 目录下面还有很多文件,其中最常用的是:
cat /proc/12345/task/12346/status这个命令能看到线程的状态(State)、父子关系、所属进程 PID 等,信息非常全。如果线程卡在某个系统调用里,可以用:
cat /proc/12345/task/12346/wchan cat /proc/12345/task/12346/syscallwchan表示线程正在等待的内核函数名,syscall表示当前正在执行的系统调用号。这俩字段对定位线程卡死特别有用,比如线程大量阻塞在futex_wait,通常意味着它们在等待锁;阻塞在poll或epoll_wait,说明它们在等待网络事件。
/proc还能直接读线程的/proc/<PID>/task/<TID>/stack,但注意这个文件通常需要 root 权限,而且某些内核版本可能限制访问。读到的内核栈信息可以帮助判断线程是否在内核态长时间旋转,比如自旋锁导致的 CPU 飙升。
5.3 进程级 status 里的 Threads 字段
除了看/proc/<PID>/task/目录,另一个快速判断线程数量的方式是直接看进程的 status 文件:
cat /proc/12345/status | grep Threads例如输出:
Threads: 12这个数值代表进程当前的线程数量,跟ls /proc/12345/task | wc -l结果一致。在一段性能排查中,我会每隔几秒采集一次这个数据,看线程数是否在持续增长。如果线程数单调上升、不回落,基本上就是线程泄漏了——大量线程创建后没有退出。这比 CPU 占用更能提前暴露问题,因为线程泄漏初期 CPU 可能并不高,但资源最终会被耗尽。
6. 调试场景下的线程视图:gdb / pstack / jstack
6.1 gdb 附加进程查看线程
当你需要查看某个线程正在执行什么函数,静态视图就不够用了,必须动态附加进程。gdb是排查 C/C++ 多线程程序的利器。
先附加进程:
gdb -p 12345进入 gdb 后:
info threads这会列出进程下的所有线程,包括每个 LWP 的 ID 和当前执行位置。切换线程:
thread 2然后打印调用栈:
bt如果要把所有线程的调用栈全部打出来,可以一次执行:
thread apply all bt输出很长,建议把 gdb 输出重定向到文件里再慢慢分析。gdb 附加进程会让程序暂停一段时间,生产环境使用要谨慎,最好在流量低峰操作,或者使用gcore先导出 core 文件再离线分析。
6.2 pstack 快速打印线程栈
如果不想进 gdb 那么“重”,pstack是一个更轻量的选择。直接对着 PID 执行:
pstack 12345它会打出这个进程所有线程的用户态调用栈,不需要交互。速度比 gdb 快,适合抓现场。缺点是某些发行版默认不装,需要额外安装,而且面对极度复杂的程序时输出可读性不如 gdb。
pstack 在 Java 服务上也能用,但输出的是 C++ 虚拟机层面的栈,对 Java 代码逻辑帮助有限。做 Java 排查的话,还是得靠 jstack。
6.3 Java 服务的 jstack
Java 的多线程模型和系统线程的对应关系比较绕:一个 Java 线程对应一个 JVM 内部的原生线程,原生线程又对应一个 Linux LWP。如果你想看 Java 层面某个线程在干什么,用jstack <PID>会直接打印出 Java 线程名、线程状态、栈帧,以及对应的 nid(native thread ID 的十六进制形式)。
这个 nid 可以用来和系统线程 ID 对上号。比如系统层用top -H看到一个 TID 是 31492,转成十六进制是 0x7B04,到 jstack 输出里搜nid=0x7b04,就能定位到具体的 Java 线程名和业务代码位置。
Java 线程名往往能直接说明问题,比如"http-nio-8080-exec-11"表示这是处理 HTTP 请求的工作线程,如果大量线程卡在"java.lang.Thread.State: WAITING (parking)"且堆内大量对象堆积,那基本可以判断是线程池满、队列拥堵。
6.4 strace 跟踪线程系统调用
还有一种动态场景是跟踪线程的系统调用行为,strace加-f参数会跟随线程:
strace -f -p 12345这样每个线程的系统调用都会输出,带上 TID 前缀。这个工具适合排查线程是否反复打开文件、频繁 socket 连接、或者陷入奇怪的系统调用循环。缺点也是会拖慢目标进程,生产环境慎用。
7. 常见问题与排查技巧实录
7.1 为什么实际线程数比代码里创建的还多
这个坑几乎人人踩过。用ps -L或/proc一数线程,发现线程数远超业务代码里手动创建的线程池大小。原因主要有几个:
- 运行时自带后台线程。JVM 至少有 GC 线程、编译器线程、信号处理线程等;Go 运行时也有自己的后台调度线程。
- 第三方库偷偷起线程。比如某些日志库、监控 SDK、连接池组件都会在内部创建线程,而且名字常常不显眼。
- 线程创建了但没回收。这属于资源泄漏,排查时最危险,因为线程数会越涨越多。
遇到线程数超预期,不要急着怪代码逻辑,先统计一段时间内线程数是否稳定。稳定就意味着大概率是运行时正常开销,持续增长才是问题。
7.2 线程状态怎么解读
查看线程时,STAT列经常出现字母组合,常见的有:
R:正在运行或正在等待运行。S:可中断睡眠,通常等待某个事件,比如 IO 完成或定时器。D:不可中断睡眠,一般是内核态 IO 阻塞,比如读取磁盘。看到大量D状态线程要注意是否磁盘出问题。T:被停止,比如被SIGSTOP暂停。Z:僵尸状态,线程已结束但没被回收。在正常多线程程序里,僵尸线程不常见,一旦出现要重点检查线程退出后的资源回收逻辑。
ps -L输出里如果大量线程是S而业务请求没有响应,大概率线程池在空转或等待锁,这时候要去抓线程栈看看它们到底在哪里 sleep。
7.3 线程暴涨怎么快速定位
线程像雪崩一样暴涨时,首要任务是找出创建线程的源头,而不是逐行读日志。我通常按两步走:
先看整体趋势,确认线程数在涨:
for i in $(seq 1 10); do cat /proc/<PID>/status | grep Threads; sleep 1; done再在短时间内抓几次线程列表,对比新出现的 TID 规律:
ps -eLo pid,lwp,comm -p <PID> | sort -k2 -n | tail -20如果新线程名都带同一前缀,比如worker-thread-、pool-1-thread-,那八成是某个线程池在不断创建新线程执行任务。此时把高并发场景下的任务入口代码翻出来检查,十有八九是任务队列积压、线程池参数设置不当,或者业务逻辑中每个请求都会创建新的临时线程。
还有一种隐蔽情况是每次请求都触发创建新线程,但线程执行完又无法退出,最后堆积成山。这种往往不是线程池问题,而是原始线程创建方式没有配合join或者通过executor管理,导致大量游离线程。
7.4 常见的“查不到线程”情形
排查环境是容器时,/proc可能只暴露容器内可见的信息,某些工具依赖的权限会被限制,导致查看/proc/<PID>/task时提示权限不足。这时优先确认是否能进入目标进程命名空间,以及是否具备SYS_PTRACE权限,否则gdb、strace这类动态附加工具基本用不了。
另外,如果进程 PID 是复用过的,你在查看时小心旧数据残留。每次都先ps -ef | grep 进程名确认 PID 再继续,避免盯着错误目标查半天。
7.5 高效输出技巧:给现场留证据
排查线程问题时,现场信息转瞬即逝,一定要第一时间留存。我自己常用的命令组合:
ps -eLo pid,tid,stat,pcpu,comm --sort=-pcpu > thread_snapshot.txt pidstat -t -p <PID> 1 10 > pidstat.log pstack <PID> > pstack.log 2>&1这些输出可以组合成一份排查记录,方便对比 CPU、线程栈在不同时间的变化。比一次次手抄和截图靠谱得多。
8. 我常用的几条命令速查
整理一份自己常用的速查表,排查时效率会高很多:
| 排查需求 | 推荐命令 |
|---|---|
| 查看进程下所有线程 | ps -T -p <PID> |
| 查看线程 ID 与线程数 | ps -L -p <PID> |
| 按 CPU 排序看系统所有线程 | ps -eLo pid,lwp,stat,pcpu,comm --sort=-pcpu |
| 实时看某进程的线程 CPU | top -H -p <PID> |
| 按线程周期采样 CPU | pidstat -t -p <PID> 1 |
| 精确线程数 | ls /proc/<PID>/task/ | wc -l |
| 查看线程状态和栈信息 | cat /proc/<PID>/task/<TID>/status |
| 快速打印线程栈 | pstack <PID> |
| 动态调试线程 | gdb -p <PID>后执行info threads |
| 查看 Java 线程栈 | jstack <PID> |
老实说,工具再多,最常用的也就前五条。把ps -L、top -H、pidstat -t这三件套练熟,绝大多数线程问题已经能定位到 TID 级别;剩下那些一两百字的异常栈,才值得动用gdb和pstack深入。我自己的习惯是先在/proc目录确认线程数量和状态的轮廓,再决定要不要上重型工具,因为这一步成本最低,信息也最直接。排查线程问题跟排查进程问题没有本质差别,关键是别把线程当作“另一种神秘实体”,它就是一组共享资源的进程,找到它,读懂它的状态,答案自然就出来了。