1. 先搞懂一件事:线程和进程在Linux里到底差在哪
很多人刚接触 Linux 性能排查时,都会有个疑问:进程和线程不是一回事吗?为什么非要单独强调“查看线程”?这个问题的答案,恰恰是理解整个排查思路的起点。
在 Linux 内核的实现里,线程和进程本质上用的是同一个数据结构和调度单位,也就是task_struct。内核并不区分“这是进程、那是线程”,它只看“这是一个 task,可以被调度”。线程的创建靠的是clone系统调用,传入不同的标志位来决定新 task 和父 task 之间共享什么、不共享什么。共享地址空间、共享文件描述符、共享信号处理的,我们管它叫线程;反之,完全独立的就是进程。所以严格来说,Linux 采用的是“线程即进程”的模型,这也是为什么ps -ef看不到线程、必须用专门参数或工具才能把线程从进程的壳子里剥出来的根本原因。
那为什么要单独看线程?因为性能瓶颈往往不是整个进程都忙,而是进程里的某几个线程在拼命烧 CPU。如果只盯着进程级别的 CPU 占用率,你只能看到一个整体数字,比如 PID 1234 占了 200% CPU,但你不知道这 200% 到底是哪个线程贡献的。尤其在 Java、Node.js、数据库这类多线程应用里,定位到具体线程,基本就等于定位到了问题代码所在的执行流。所以,查看线程不是“多看一层信息”,而是“找到元凶的唯一途径”。
这篇文章我会从最常用的ps讲起,再到动态监控的top、htop,接着聊/proc文件系统这种底层玩法,最后用一个真实场景演示“线程 CPU 飙高怎么一步步抓到具体线程”。每一段都会附上操作命令和输出解读,你可以直接复制到自己的机器上试。
提示:下面所有的命令和输出,基于 CentOS 7+ / Ubuntu 20.04+,内核版本 3.10 以上。如果你用的是 BusyBox 这类精简环境,部分参数可能不支持,我会在对应位置单独说明。
2. 最直接的查看命令:ps 的各种姿势
2.1 ps -eLf:一条命令看全所有线程
ps是 Linux 里最基础的进程查看命令,但它默认只看进程,看不到线程。要让它显示线程,需要加-L参数。完整命令是:
ps -eLf这条命令的含义是:-e显示所有进程,-L显示每个进程下的所有线程,-f显示完整格式。执行后,输出大概是这样的:
UID PID PPID LWP C NLWP STIME TTY TIME CMD root 1 0 1 0 1 09:30 ? 00:00:03 /sbin/init root 123 1 123 0 1 09:30 ? 00:00:00 /usr/sbin/cron mysql 1456 1 1456 0 4 09:31 ? 00:00:01 /usr/sbin/mysqld mysql 1456 1 1457 0 4 09:31 ? 00:00:00 /usr/sbin/mysqld mysql 1456 1 1458 0 4 09:31 ? 00:00:00 /usr/sbin/mysqld mysql 1456 1 1459 0 4 09:31 ? 00:00:00 /usr/sbin/mysqld注意看几个关键列:
PID:进程 ID,所有线程的 PID 相同,表示它们属于同一个进程。LWP:Light Weight Process,轻量级进程,这是线程自己的 ID。我们平时说“线程 ID”,在 Linux 里指的就是 LWP。NLWP:Number of Light Weight Processes,这个进程下线程的总数量。CMD:线程对应的命令名。大部分线程名和进程名一样,但 JVM 之类的高级运行时会给线程起专属名字,比如“GC Thread”“C2 CompilerThread0”。
如果我只想查看某一个进程下的线程,可以用-T配合-p指定 PID:
ps -T -p 1456这条命令会列出 PID 为 1456 的进程下的所有线程,输出里会多一列SPID(Thread ID),其实就是 LWP 的别名。这种方式比ps -eLf全量输出更聚焦,适合已经明确知道目标进程的情况。
2.2 用 ps 按线程 CPU 排序:快速找出热点线程
ps不仅能看,还能排序。假设你想找出系统里哪个线程最消耗 CPU,可以这样:
ps -eLf --sort=-%cpu | head -20--sort=-%cpu表示按 CPU 占用率从高到低排序,head -20取前二十行。输出里你会看到排在前面的是高 CPU 的线程而不是进程,这就避免了“进程 200%、内部一片混乱”的尴尬。
有时候进程名都是一样的,比如 Java 进程,所有线程的 CMD 都叫java,光看名字根本分不清谁是谁。这时候可以加-o自定义输出列,把 LWP、PID、CPU 百分比、内存、线程名一起打出来:
ps -eLo pid,lwp,nlwp,pcpu,pmem,comm,args --sort=-pcpu | head -30pcpu是 CPU 占用率(百分比),pmem是内存占用率,args能显示这个线程启动时的完整命令行,对定位 Java 启动参数、Python 脚本路径很有用。
2.3 ps 查看线程的底层逻辑:为什么 -ef 看不到线程
理解ps为什么需要-L或-T才能显示线程,得从/proc说起。ps的所有信息都来自/proc文件系统,默认情况下ps -ef遍历的是/proc/[pid]/stat,这个文件存的是进程级别的汇总信息。而线程的信息在/proc/[pid]/task/目录下,每个线程对应一个子目录,目录名就是 LWP 的 ID。
ps -eLf做的事情,本质上是额外遍历了/proc/[pid]/task/下的每个子目录,把每个线程的统计信息读出来并格式化展示。所以你要是想特别“底层”地数线程数,可以直接用ls数 task 目录的数量:
ls /proc/1456/task | wc -l如果这个进程有 4 个线程,task 目录下就有 4 个子目录,wc -l就能得到一个直观的数字。这招在很多没有ps -L的精简嵌入式环境里特别好用。
实操心得:我排查问题时的习惯是——先用
ps -eLf --sort=-%cpu | head -20看全局热点,锁定了嫌疑 PID 之后,再用ps -T -p <PID>细看这个进程内部的线程情况。两步走,既不会被海量线程刷屏,又不会漏掉真正消耗资源的进程。
3. top 和 htop:动态监控线程的实战用法
3.1 top -H:让 top 直接显示线程
ps适合“截取某一时刻的快照”,但如果是 CPU 占用率忽高忽低、需要持续观察的情况,top是更好的选择。
直接在 top 运行的时候按H键,就可以在“进程视图”和“线程视图”之间切换。不过我更推荐启动时直接指定参数:
top -H加了-H之后,top 输出的每一行就不再是进程,而是线程。你会注意到同一个进程的多个线程会连续出现多行,PID 列显示的是 LWP 线程 ID,而不是进程 ID。
如果只想看某个特定进程的线程,可以配合-p:
top -H -p 1456这样 top 就只盯着 PID 1456 这个进程,把它下面的所有线程单独展示出来,CPU 占用率最高的线程会排在前面。这个命令在多线程 Java 应用出现 CPU 飙高问题时几乎是必用的。
top 线程视图里还需要关注几个默认不显示、但很有用的列:
%CPU:线程的 CPU 占用率。这里要注意,top 默认是“相对于单个核”的百分比。比如 4 核机器上,一个线程最多能到 100%,而整个进程理论上能到 400%。%MEM:线程的内存占用,这个数字在线程级别其实意义不大,因为同一进程的线程共享内存,数值基本一致。TIME+:线程累计消耗的 CPU 时间,这个指标特别有用。如果一个线程的 CPU 百分比不高,但累计时间一直在涨,说明它是持续活跃的,而不是偶尔闪现。
如果你需要按 CPU 排序,在 top 界面里按P键(大写 P),线程就会按 CPU 占用率从高到低排列。按M键可以按内存排序,按T键按累计 CPU 时间排序。
3.2 top 内部常用的交互键:
H:切换进程视图 / 线程视图P:按 CPU 占用率排序M:按内存占用排序T:按累计 CPU 时间排序c:显示完整命令行,而不是只显示进程/线程名1:查看每个 CPU 核心的使用率,有助于判断是单线程瓶颈还是多核都满
切换线程视图后,第一列的 PID 对应的其实是 LWP,这个细节初学容易踩坑。假设你在 top 里看到一个线程 PID 是 28931,然后你想用kill -9 28931去干掉它,这基本没什么用——因为线程不能单独 kill(除非它自己响应信号退出,但这对主进程影响很大)。正确的做法是确认这个线程属于哪个进程,再去排查进程本身。
3.3 htop:更友好的线程树和搜索
如果你觉得top的界面太古板、交互逻辑太绕,htop是更好的替代品。htop 默认是彩色界面,操作更直观,而且它有一个杀手锏——树形视图。
在 htop 界面里按F5,会切换到树形模式,同一个进程的所有线程会以缩进的方式挂在进程下面,一眼就能看清楚进程和线程的从属关系。配合F4按名称搜索、F6按列排序,定位线程非常方便。
htop 里默认只显示进程,想看线程,需要进入设置(按F2),在 Display options 里勾选 “Show custom thread names” 和 “Hide kernel threads” 之类选项。不同的 htop 版本设置项略有不同,核心思路是:让线程显示为独立行,并展示出 JVM 或应用自定义的线程名。
特别推荐 htop 的线程颜色区分。默认配置下,进程行和线程行的颜色强度不同,线程行颜色偏淡。在大量线程的情况下,这种视觉区分能让你的眼睛瞬间聚焦到进程骨架,而不是迷失在线程的海洋里。
3.4 top/htop 的局限:线程状态和阻塞场景
top 和 htop 对 CPU 占用率这一指标很敏感,但如果是线程卡在 IO 等待、锁等待、网络阻塞,CPU 占用率并不高,这时候 top 的线程视图未必能一眼暴露问题。我的经验是,如果 CPU 不高但系统明显卡顿,需要辅以其它工具查看线程状态。top 里按H切换线程视图后,观察S列(进程状态),R代表运行,S代表可中断睡眠,D代表不可中断的 IO 等待,T代表暂停。大量线程处于D状态往往是磁盘或网络 IO 出了问题;大量S状态线程则可能是线程池等待任务、锁等待或者事件驱动模型的正常现象,需要结合应用日志判断。
4. /proc 文件系统:不看工具,也能徒手查线程
4.1 /proc/[pid]/status:数线程最快的方式
/proc是 Linux 虚拟文件系统,内核把运行时信息以文件的形式暴露出来。查看线程,最直接的文件是/proc/[pid]/status,里面有一个字段叫Threads,直接告诉你这个进程当前有多少线程:
cat /proc/1456/status | grep Threads输出:
Threads: 4这个数字和ps -eLf里 NLWP 列的值是一致的。用这个文件的好处是,即使ps命令被裁剪过、或者你处于一个极简容器里,只要能访问/proc,就能拿到线程数量。
4.2 /proc/[pid]/task:每个线程一个目录
更细致的方法是进入/proc/[pid]/task/目录:
ls -l /proc/1456/task你会看到类似这样的输出:
total 0 dr-xr-xr-x 9 mysql mysql 0 Jul 15 09:31 1456 dr-xr-xr-x 9 mysql mysql 0 Jul 15 09:31 1457 dr-xr-xr-x 9 mysql mysql 0 Jul 15 09:31 1458 dr-xr-xr-x 9 mysql mysql 0 Jul 15 09:31 1459每个目录名是线程自己的 PID(LWP),进入任意一个目录,你会看到一堆和进程级/proc/[pid]/一样的文件,比如stat、status、wchan、stack等。这就印证了我前面说的——Linux 线程本质上是独立的 task,只是共享了地址空间和资源。
想单独看某个线程的状态,可以这样:
cat /proc/1456/task/1458/status里面会有State、VmRSS、voluntary_ctxt_switches(主动上下文切换次数)、nonvoluntary_ctxt_switches(被动上下文切换次数)等字段。其中上下文切换次数对排查频繁线程切换、锁竞争问题很有价值。如果某个线程的非自愿切换次数特别高,通常意味着它在频繁被抢占或者锁竞争激烈。
4.3 用 ls + wc 快速统计线程数
在脚本或者监控场景中,你可能只需要一个线程数量数字,不需要完整输出。一行命令就能搞定:
ls /proc/1456/task | wc -l如果想要统计系统里所有进程的线程数总和,可以用一个循环:
find /proc/[0-9]*/task -maxdepth 0 2>/dev/null | wc -l或者更精确地遍历每个进程:
total=0 for pid in /proc/[0-9]*; do n=$(ls ${pid}/task 2>/dev/null | wc -l) total=$((total + n)) done echo $total这段脚本会统计系统内所有线程的总数。当系统大量创建线程时,总数会飙升,这通常是线程泄漏的早期信号。
实操心得:我在排查一个“进程明明活着,但响应越来越慢”的问题时,就是用
ls /proc/<pid>/task | wc -l发现线程数从正常的 50 暴增到了 3000 多。线程池疯狂创建新线程,根本没复用。这种情况下用ps -eLf看具体线程列表意义不大(几千行),更多的是靠进程内线程数量趋势来判断是否泄漏。
5. 线程 CPU 占用过高:从 top 一路查到代码
5.1 理论工具链:top + 十六进制转换
我见过很多开发者,手里有工具,但排查询题没有章法。其实“线程 CPU 高”这件事,排查链路非常固定,我总结成了四个步骤。
第一步,用top -H找到占用 CPU 最高的线程 PID。假设我们找到一个 Java 进程的线程 PID 是28931。
第二步,拿到线程 PID 后,它对应到应用层工具(尤其是 Java 的jstack、JVM 的线程转储)时,线程 ID 通常是用十六进制表示的。所以需要转换:
printf "%x\n" 28931输出可能是7103。这里的十六进制值是 JVM 内部线程的 nid(native thread ID)。
第三步,用线程转储工具抓取线程栈,比如 Java 应用:
jstack <java_pid> | grep -A 30 "nid=0x7103"这样就能看到这个线程正在执行什么代码。
第四步,如果是底层 C/C++ 程序,可以用gdb附加进程,或者用perf top -t <线程PID>直接看线程的热点函数。
5.2 实战场景:Java 进程 CPU 飙升
举个例子,某次线上反馈服务响应变慢,我登录服务器先跑:
top -H -p <java_pid>按P排序后,看到 PID 29231 的线程 CPU 占用率稳定在 99%。我随手把 PID 转成十六进制:
printf "%x\n" 29231得到72df。接着执行:
jstack <java_pid> | grep -A 30 "nid=0x72df"很快定位到一段ConcurrentHashMap的put操作,大量线程在竞争同一个锁。通过线程栈再结合业务日志,发现是缓存热点 key 过期后大量请求同时回源写入,导致锁竞争白热化。修复方式是给热点 key 加锁随机失效时间,问题解决。
整个排查过程不到十分钟,其中最关键的一步就是top -H拿到高 CPU 的线程 PID,以及十六进制转换。这个流程放之四海而皆准,不管你是 Java、Go 还是 C++,核心思路都一样——系统层的线程 ID 要和应用层的线程栈对上号。
5.3 pidstat -t:按线程维度输出 CPU 和上下文切换
如果你不想用 top 交互式界面,希望把线程级别的 CPU 使用情况记录到日志里或定时采样,pidstat是更好的选择。它来自sysstat工具包,没有的话可以用包管理安装:
# Ubuntu / Debian apt install sysstat # CentOS / RHEL yum install sysstat查看某个进程下所有线程的 CPU 使用率:
pidstat -t -p <PID> 1 5-t表示显示线程级别的统计,1 5表示每秒输出一次,共 5 次。输出里会多一列TID,这就是线程 ID。更重要的是,pidstat 还能输出上下文切换次数:
pidstat -wt -p <PID> 1 3-w显示上下文切换统计,cswch/s表示每秒自愿上下文切换次数,nvcswch/s表示每秒非自愿上下文切换次数。如果某个线程的 nvcswch/s 特别高,说明它被内核抢占得很频繁,有可能在自旋等待或者忙等。
5.4 perf 和 bcc/bpftrace:更底层的线程热点分析
如果是 C/C++ 写的服务,锁竞争往往不是靠线程栈能快速发现的,你需要更底层的性能分析工具。perf和bcc/bpftrace是这类场景的利器。
perf按线程采集 CPU 采样:
perf top -t <线程PID>perf top -t会以实时刷新的方式展示该线程内部各个函数的 CPU 占用占比,直接能看到热点函数名。也可以做离线采样:
perf record -t <线程PID> -g -- sleep 30 perf reportbcc/bpftrace适合写动态探针,比如统计某进程下所有线程的系统调用次数、锁等待时间。不过这类工具的复杂度较高,不是日常排查的第一选择。我的建议是:先通过 top + jstack/gdb 解决 80% 的问题,剩下 20% 的疑难杂症再上动态追踪工具。
6. 常见问题与排查技巧实录
6.1 为什么 ps -ef 只看到进程,根本看不到线程
这是ps的默认行为决定的。ps -ef遍历的是进程级的信息,要想看线程必须用-L或-T参数。类似地,pgrep默认也只匹配进程,如果你想按线程名查找,需要额外处理。比如 Java 进程的 GC 线程名可能叫GC Thread 0,但pgrep "GC Thread"是找不到的,因为 GC 线程不是一个独立的进程。
6.2 线程名字都一样,怎么区分谁是谁
很多运行时线程名都基于进程名。比如所有 Java 线程的 COMMAND 列都叫java,Go 程序的线程也都叫同一个二进制名。这时候需要用-o自定义列,把args加进去,或者用应用自己的线程转储工具(jstack、dotnet-dump、gdb 的info threads)。在 htop 里开启 “Show custom thread names”,JVM 会以"GC Thread#0"这种自定义名字展示线程,这对辨析线程功能帮助极大。
6.3 线程数过多,如何快速判断是否异常
Linux 线程数量受两个主要限制:一是pthread_create时栈空间大小(默认 8MB,但实际是虚拟内存,不会真的占那么多物理内存),二是/proc/sys/kernel/threads-max内核参数。一般业务系统线程数上千是正常现象,但如果一个进程的线程数持续线性增长,最终触及threads-max或进程的 max map count,应用就会崩溃。
判断线程数是否异常,不只是看绝对值,还要看趋势:
# 每 10 秒统计一次指定进程的线程数 while true; do echo "$(date +%H:%M:%S) $(ls /proc/<PID>/task | wc -l)" sleep 10 done如果线程数脚本跑出来是单调递增且不回落的,十有八九是线程池没做回收或线程创建没有上限。
6.4 有一个线程假死在锁上,怎么定位
线程状态卡在S(可中断睡眠)不一定有问题,但如果这个线程长期占用某把锁,其它线程全部排队,你会看到大量线程堆积在同一个锁方法上。这时除了看线程栈,还要关注wchan(waiting channel),也就是线程在内核里等待什么:
cat /proc/<PID>/task/<TID>/wchan输出如果是mutex_lock、futex_wait这类内核函数名,基本可以断定是锁竞争。配合jstack看到的业务调用栈,就能找到持锁线程。Java 端可以用jstack抓线程转储后搜索 “waiting to lock” 或 “locked”。
提示:
wchan在部分内核版本中需要 root 权限才能完整显示,普通用户可能看到0或一堆没符号的地址,这是正常的,不代表没有等待。
6.5 容器环境里查看线程的两个坑
第一个坑是 PID 命名空间。在容器里,你看到的 PID 是容器命名空间内的 PID,和宿主机上的 PID 可能完全对不上。如果你用宿主机工具去查容器内进程,需要先通过docker inspect或cat /proc/<容器PID>/status的 NSpid 字段拿到宿主机视角的真实 PID。
第二个坑是部分精简容器镜像里没有top、ps这类工具。这种情况下不要慌,直接用/proc文件系统就行:
cat /proc/self/status | grep Threads依赖/proc的方式不依赖任何额外工具,只要内核是在正常状态,这个文件几乎一定存在。
6.6 几个我常用的命令速查
| 场景 | 命令 |
|---|---|
| 查看所有线程,按 CPU 排序 | ps -eLf --sort=-%cpu |
| 查看指定进程的全部线程 | ps -T -p <PID> |
| 动态看线程 CPU | top -H -p <PID> |
| 数线程数量 | ls /proc/<PID>/task | wc -l |
| 看线程状态字段 | cat /proc/<PID>/task/<TID>/status |
| 查看线程等待内核对象 | cat /proc/<PID>/task/<TID>/wchan |
| Java 线程栈定位 | jstack <PID> | grep -A 30 "nid=0x..." |
| 线程级别的 CPU/上下文切换采样 | pidstat -t -p <PID> 1 5 |
7. 我的日常排查顺序:一台陌生机器上怎么快速摸清线程情况
没系统学过 Linux 线程查看的人,最容易犯的错就是一上来直接敲top,然后被海量的行刷懵。我自己的固定顺序是四步定位法,在这里分享给你。
第一步,看整体。登录机器后先跑top看整体负载,有几个 CPU 满了、内存还剩多少、负载均值多高。如果 CPU 核心数本身不多,甚至不用看线程,直接就知道是资源不够。
第二步,锁嫌疑进程。用ps -eLf --sort=-%cpu | head -20找到 CPU 占用率最高的前几个线程,记录下它们的 PID 和所属进程 PID。这里留意一下:是单个进程内部多个线程占了高 CPU,还是多个进程各占一核?两种情况的分析方向不一样。前者是应用内部并发热点,后者更像是配置不当导致的进程数过多。
第三步,深挖点。针对嫌疑最大的那个进程,用top -H -p <PID>观察它内部线程的动态变化,同时准备好线程栈工具(jstack、gdb、py-spy 等)。如果 CPU 占用率稳定,直接抓线程栈;如果 CPU 是周期性波动,就多抓几次,最好隔几秒连续抓三四份,对比线程栈变化。
第四步,联动分析。把线程 PID 转成十六进制,在抓取的线程栈里搜索 nid 或 TID,确认热点线程当前执行的代码路径。再到/proc/<PID>/task/<TID>/status看状态、/proc/<PID>/task/<TID>/wchan看内核等待对象。如果线程状态是 R(运行),且 wchan 信息不多,用perf top -t <TID>看热点内核函数。
这套顺序对多线程 Java 服务、Go 微服务、Nginx 工作进程、MySQL 线程池几乎通吃。核心思想就一句话:进程是壳,线程是魂,CPU 时间都消耗在线程上。
另外想强调一个容易忽略的细节:top -H里的 PID 列不是进程 ID,而是 LWP 线程 ID。我见过太多人拿着top -H显示的数字去kill -9,结果杀掉的不是线程(因为线程不能被单独 kill),而可能因为-9信号作用到线程组导致整个进程被终止——这在生产环境是非常危险的。保留这个认知,会帮你避免一个大事故。
还有一点心得:排查线程问题时,系统的 CPU 核数决定了“百分比”的参考价值。在 32 核机器上,一个线程跑满 100% 其实只占了单核,对整个系统影响不大;但如果是一个进程内 32 个线程同时跑满 100%,那问题就严重了。判断问题时请始终把核数放在心里,线程的 CPU 占用率是相对于单核的,不是相对于整机的。