☰
GPU进程管理与Linux运维排查实战:从nvidia-smi到strace
2026/10/21 9:20:09 网站建设 项目流程

2. 训练场景:GPU进程管理是一场硬仗

3.1 nvidia-smi 只是第一步,关键是和 ps 组合使用

我试过很多次,训练跑挂了,第一反应就是敲nvidia-smi,然后看到什么?GPU 利用率 0%,显存占用一大半,完全没有进程号,也没有进程名。这时候如果直接根据 PID 去 kill,往往发现 PID 不存在,或者 kill 完了显存还不释放。

这里我要分享第一个"为什么":nvidia-smi显示的是 GPU 维度的状态,它只能告诉你显卡上有没有任务,很多 MIG 实例、分布式训练的多卡任务、以及已经被 kill 但显存没回收的残留进程,光靠一张表格根本看不全。真正的问题是:显存被谁占着,占用进程是不是僵尸,是不是已经脱离了会话。

所以我的习惯是固定搭配两条命令一起看:

nvidia-smi nvidia-smi --query-compute-apps=pid,used_memory,process_name --format=csv

第二条会直接列出正在使用 GPU 的 PID 和显存占用。然后再配合常规的ps -ef | grep python去确认这个 PID 是否存在、状态是不是 Z(僵尸)。如果nvidia-smi里有 PID,但ps -ef里已经找不到这个进程,那多半是显存泄漏残留,要么用fuser -v /dev/nvidia*找出具体占用,要么重启驱动模块,但重启驱动之前务必先保存训练日志。

还有个细节:查看 GPU 利用率为什么要看-l 1持续刷新?因为我遇到过很多次训练间歇性卡顿,平均利用率看着正常,但每秒的曲线在疯狂抖动。这时候用watch -n 1 nvidia-smi持续观察,再配合训练日志时间戳,很快就能判断是数据加载瓶颈还是 GPU 降频。

3.2 nohup、setsid 和 systemd:长时间任务的三种跑法

标题里有"训练"两个字,那训练任务最怕什么?最怕终端一关,任务就跟着走了。很多初学者会问:我用nohup python train.py &不就行了吗?实际上有个坑:nohup 只保证 SIGHUP 信号被忽略,但如果你是通过某开发工具(如某终端模拟器)开的会话,会话退出时可能会先发 SIGTERM 或者把进程组整个清理掉,这时候 nohup 就不够用了。

我的建议是分场景选择:

# 简单场景:短时间验证,配合日志输出 nohup python train.py > train.log 2>&1 & # 更健壮:脱离会话,独立进程组 setsid python train.py > train.log 2>&1 & # 生产级:用 systemd 托管

为什么setsid更稳?因为它让进程完全脱离原来的会话、进程组和控制终端,相当于"黑户"进程,终端关闭对它没有影响。而 systemd 方案适合需要开机自启、崩溃自动重启的场景,写一个 service 文件,配好Restart=always和Environment=PYTHONUNBUFFERED=1,再systemctl daemon-reload就能托底。

如果只是临时在终端里想超时自动结束,可以考虑配合timeout,比如timeout 3600 setsid python train.py,一小时自动退出,适合定时限时测试。这里注意一个细节:setsid之后,进程不再响应终端的 Ctrl+C,需要用kill才能终止,别在终端里狂按 Ctrl+C 没反应就以为系统卡了。

3.3 cgroup 限制:让训练任务别把整台机器拖死

很多人没注意到,训练任务在共享机器上跑的时候,如果不做资源限制,内存被训练进程写满,触发 OOM Killer 的时候,被杀的不一定是训练进程,很可能是旁边正在跑的开发服务或者别人的任务。这个教训我印象很深:有一次某同学的训练程序申请了 300G 虚拟内存,机器直接卡死,所有 SSH 连接都挣扎了很久才恢复。

解决思路很成熟:用 cgroup 给训练任务限定内存和 CPU。如果系统是 systemd 的话,可以临时用systemd-run开一个带限制的 scope:

systemd-run --scope -p MemoryMax=16G -p CPUQuota=400% python train.py

这样训练进程被放进一个独立的 cgroup 切片,内存超过 16G 就触发 OOM,而不是把整台机器都拖崩。对开发机器、共享训练服务器来说,这个操作相当于给任务上了一道保险丝。Cgroups v2 的路径可以在/sys/fs/cgroup下看到,默认是 systemd 管理的切片结构。

你可能想问:为什么不直接在代码里限制?因为训练框架的数据预处理阶段可能瞬间申请大块内存,代码里torch.cuda.empty_cache()之类管的是显存,管不了 RSS。用 cgroup 是在内核层面做物理隔离,跟具体语言和框架无关,这才是最可靠的兜底方案。

3.4 磁盘被日志写满也是一种"进程故障"

训练跑着跑着突然报错,概率最大的不是代码 bug,而是磁盘满了。尤其是2>&1把 stdout 和 stderr 都重定向到一个文件时,如果程序疯狂打印 warning,日志能按小时膨胀到几十个 G。

排查命令非常朴素,但非常有效:

df -h du -sh --exclude=/proc --exclude=/sys /* 2>/dev/null | sort -hr | head -20

第一眼看磁盘整体水位,第二眼找哪个目录最大。很多情况下罪魁祸首就是根目录下/var/log或者用户目录下的.cache文件夹。日志轮转可以用logrotate配置,设置按大小切割并保留最近 3 份,避免日志文件无限增长。

我还会顺手看一下训练脚本的日志输出频率:如果每个 step 都记录一次 loss 和 lr,而且写了完整的时间戳,那日志体积会大好几倍。建议只在必要的时间间隔(比如每 500 步)记录,同时用rsyslog或者简单的定时脚本做自动清理。磁盘满的时候,很多系统操作都会莫名失败,这种故障其实很好防,难的是没意识到"日志也是资源"。

3. 运维视角:那些一上来就要看的关键指标

4.1 top/htop 里真正需要关注的数字

运维场景下,我发现很多人用top只会看 %CPU 和 %MEM 两列,排个序就完事了。这不够。

top第一行有 load average,三个数字分别代表 1/5/15 分钟的平均负载。这个负载是 CPU 队列和不可中断睡眠任务的综合体现,不是简单的"CPU 使用率"。比如我在处理"服务卡顿"问题时,看到 load average 2.5 而 CPU 核心数只有 2,说明队列已经排满;如果 CPU 占用率只有 30% 但 load 很高,那就要怀疑是不是进程在等 IO,而不是在算 CPU。

第二行和第三行有一个非常容易被忽略的指标:%wa,也就是 IO wait。如果这个值长期大于 5%,说明磁盘是瓶颈。这时候再去排查应该用iostat而不是在top里干瞪眼。

内存那块,free -h显示 available 才是真正可用的内存,而不是看 free 那一列。因为 Linux 内核会把空闲内存拿去做 page cache,应用申请内存时会优先回收 cache。很多新手看到 free 很少就以为内存不够了,其实 available 还有很多,这个误解很常见。

4.2 内存问题的判断:别误杀 innocent 进程

运维排查内存问题,我有一套固定流程:

# 查看系统整体内存水位 free -h # 查看某个进程内存详细构成 cat /proc/PID/status | grep -E "VmRSS|VmSize|VmSwap" # 查看进程内所有线程的内存和虚拟内存 pmap -x PID | tail -20

VmRSS是实际物理内存,VmSize是虚拟地址空间大小。如果一个进程VmSize很大但VmRSS不大,说明它申请了很多虚拟地址但没有真正写入,一般问题不大。如果VmRSS持续上涨且不回落,那就是内存泄漏的典型信号,需要把注意力放在代码层面了。

还有个反直觉的地方:Linux 的 overcommit 机制允许进程申请超出物理内存的虚拟内存。所以某个进程显示占用了 300GB 虚拟内存,但机器总内存可能只有 128GB,这种现象不一定是 bug。真正危险的信号是swap 被大量使用。用free -h看 swap 的一项,一旦 swap 使用量明显变大,整机性能就会戏剧性下降,这时候最有效的操作是找出大量占用物理内存的进程,而不是盲目 expand 内存。

4.3 磁盘 inode 耗尽:df 显示 100% 但文件很小

这个坑我踩过一次就学会了。有一次客户报磁盘满了,我df -h一看,根分区用了 100%,然后du -sh仔细查,发现所有目录文件加在一起也远小于分区大小。排查结论是:inode 耗尽了。

小文件数量太多会消耗 inode,而df -h统计的是块用量,df -i看的才是 inode 使用率。命令组合:

df -h df -i # 找到包含大量小文件的目录 for i in /*; do echo $i; find $i -type f 2>/dev/null | wc -l; done

很多缓存目录、临时文件目录、消息队列的溢出目录都会堆出几百万个小文件。典型的比如某消息队列的消费进度文件、某个应用的 session 目录。一旦 inode 耗尽,任何创建文件的动作都会失败,包括启动新进程时创建 socket、写日志,整个系统就像被"冻结"了一样。

我还遇到过/tmp目录被 socket 文件占满的情况,排查链路是:df -i显示 100%,然后find /tmp -type s | wc -l数出了几十万个 socket 文件。根本原因是某个服务崩溃重启过快,每次启动创建新的 socket,旧的没被回收。处理办法是先停掉服务,清空/tmp下对应的 socket 文件,再启动服务。

4.4 ss 和 netstat:端口的真相只有一个

端口排除故障,我只用ss,不是因为它比netstat高级,而是ss的性能好且信息更全。最容易困惑的问题就是 "端口被占用"。

ss -tlnp | grep 8080

这个命令能直接列出监听 8080 的进程 PID 和进程名,但注意:如果你用的用户不是 root,-p可能看不到 PID,需要sudo。如果端口显示没被监听,但你明确知道有服务在跑,那要检查是不是服务只监听在 IPv6 的:::8080上,而客户端连的是0.0.0.0的 IPv4 地址,两者在ss输出里的表现形式不同。

在处理"端口明明释放了但连接数还在涨"的场景时,要看 TIME_WAIT 和 CLOSE_WAIT 的数量。大量 CLOSE_WAIT 说明对端已关闭但本端没有正确调用 close,这是应用层 bug;大量 TIME_WAIT 多数是短连接风暴,可以通过调优内核参数缓解。运维排障要把ss的输出和进程状态、应用日志一起看,单看端口意义有限。

4. 进程故障排查讲透:从症状到根因的五步法

5.1 第一步:进程它到底还活着吗,是什么状态

进程故障排查,我总结的五步法,第一步永远不是猜,而是看状态。用ps -ef或者ps aux列出进程,看 STAT 列。常见的几种状态:

  • S:可中断睡眠,正常状态
  • R:运行中,也可能在队列里等待
  • D:不可中断睡眠,通常是等 IO,比如 NFS 挂了、磁盘卡了,这种状态很难被杀掉
  • Z:僵尸进程,子进程已经退出但父进程没有回收
  • T:被暂停了,比如 Ctrl+Z 或者kill -STOP

D状态是个大坑:进程看起来"卡死"了,但kill -9也杀不死,因为内核层面的 IO 阻塞还没返回。处理思路是找到阻塞的源头,比如 check 是不是某个网络文件系统不可用,或者磁盘真的故障了。我曾经见过一台机器上所有进程都进入 D 状态,原因是底层存储彻底失联,最后只能重启机器恢复。

僵尸进程(Z)的危害被很多人低估了。单个僵尸占的资源很少,但如果父进程是 init 系统里的某个常驻服务,父进程不 waitpid,僵尸就会越堆越多,形成僵尸群,最终导致 PID 号耗尽,新进程起不来。排查命令:

ps aux | awk '$8 ~ /Z/ {print $2, $3, $11}'

找到僵尸进程后,看它的 PPID,去通知对应父进程处理,如果父进程是系统服务,可能需要重启服务来回收僵尸。

5.2 第二步:CPU 飙高到底是在算还是在空转

CPU 100% 的进程,不一定是效率高,也可能是死循环。区分方式是看它在哪:用户态、内核态还是被信号卡住。

top -H -p PID perf top -p PID

top -H会列出进程内每个线程的 CPU 占用率。如果某个线程占用率持续 100%,说明代码里有热门循环。perf top可以看到当前 CPU 热点函数名,如果看不到符号,可能需要装 debuginfo 或者确认是否运行在容器里缺少权限。

还有一个经常被忽略的方向:CPU 飙高可能不是应用本身,而是内核线程、中断处理程序。top里的si和hi两列代表软中断和硬中断消耗的 CPU,如果这两个值很高,说明有大量网络包或者磁盘中断在打扰 CPU,比如网卡出现大量广播、磁盘抖动等。排查这类问题需要mpstat -P ALL 1看每个核的分布,再用sar -n DEV 1看网络包量。

5.3 第三步:内存泄漏怎么一步步"钉"出来

内存泄漏的排查是最考验耐心的。我先讲为什么要看/proc/PID/smaps而不是只看top:

cat /proc/PID/smaps | grep -E "^(Size|Rss|Pss|Shared|Private)"

smaps会列出进程每一段内存映射的具体大小,Rss是驻留物理内存,Private是私有内存。如果Private持续增长而Shared稳定,说明进程自己在不断分配新内存。比对两份时间间隔较长的smaps输出,可以很清楚地看到哪一段anon(匿名映射)内存在涨,这通常就是泄漏点。

配合代码层面,可以用valgrind --tool=memcheck做精细检查,但生产环境不宜直接跑,因为性能开销很大。另一个思路是用jemalloc --histogram或者gdb打印堆内存分配栈,但这些都需要在测试环境复现症状。务实的做法是:先确认是哪个进程泄漏,再把进程打 dump,离线分析内存里的对象分布。

5.4 第四步:文件句柄耗尽和它的连带效应

很多服务故障的"最终解释"是文件句柄用完了,表象却是各种五花八门的报错:数据库连接失败、socket 创建失败、日志打不开、GC 线程挂掉。

ulimit -n ls /proc/PID/fd | wc -l

第一行看进程能打开多少文件,第二行看进程当前实际打开的句柄数。如果实际数接近硬限制,就要开始清理了。清理方向:看看/proc/PID/fd里是不是一堆 socket 文件,连接到某个地址的服务已经不响应了;有没有日志文件被删掉但进程还持有 fd,导致磁盘空间一直不释放。

被删但未释放的文件,这个坑非常隐蔽:df -h显示磁盘满了,但du -sh在最上层目录找不到大文件,因为文件已经被删了但进程还持有 fd。这时候要用:

lsof +L1

列出所有被删除但仍被进程打开的文件,找到对应的 PID,重启该进程就能释放磁盘空间,解决磁盘满的问题。这个技巧在运维排障里至少能救两次急。

5.5 第五步:strace 和 dmesg,把"看不见的逻辑"拉出来

当一切正常得诡异,进程就是起不来,或者起了一半卡住,我用strace。

strace -f -o /tmp/trace.log ./your_binary

-f是为了跟随 fork/exec 的子进程,-o指定输出文件。运行一下,看看它卡在哪个系统调用上:是connect超时、open返回 EACCES、还是read等待管道。strace输出的最后几行基本就是卡死的地方,这一步能省下无数猜测时间。

内核层面,dmesg -T | tail -50能看内核日志,尤其是 OOM Killer 的记录、segment fault 的地址、被系统 kill 的进程名。比如经典的 "Killed process ... total-vm:... anon-rss:..." 就是 OOM 的证据,直接告诉你哪个进程被内核暴毙了。还有一些进程崩溃时留下的 core dump 信息,也会出现在dmesg里。

这里提醒一个安全细节:strace在某些高权限安全模式下可能需要 root,而且输出可能包含敏感数据,生产环境建议用-p PID快速 attach 一小段时间抓快照,不要长时间跟踪。另外,如果进程是启动后立刻崩溃,用dmesg往往能直接找到segfault相关行,这就是排查方向。

5. 几个容易被忽略的隐患排查技巧

6.1 端口占用、缓存清理、找大文件:日常救急三板斧

除了大篇幅聊的进程排查,日常运维救急还有三个高频场景。

第一,端口被占用时,先用lsof -i :端口号还是ss -tlnp?我更推荐ss,输出格式更统一,速度更快。但lsof能额外看到文件路径和用户,在判断"哪个项目占了这个端口"时更直观。两个命令可以互补,甚至还可以写一个小 wrapper:

alias port = 'ss -tlnp | grep'

第二,共享机器上cache占用过高时,用sync && echo 3 > /proc/sys/vm/drop_caches可以清空 page cache。但这个操作在共享环境要慎重,因为会短暂降低文件访问性能,而且需要有 root 权限。我更倾向于在出现明显的"内存压力"时才手动清理,正常的 page cache 是内核自动管理的,不需要干预。

第三,找大文件是用find / -type f -size +500M -exec ls -lh {} \;还是du?我一般是先du -sh * | sort -hr缩小范围到目录级别,再用find定位具体文件。原因很简单:find会遍历所有文件,如果系统里小文件特别多,会非常慢,先用du找到目录再进去找,效率高很多。

6.2 alias 和 history:把常用命令变成"肌肉记忆"

开发、训练、运维三合一场景下,靠脑子记命令不如靠 alias 和 history。我的.bashrc里固定放了几个:

alias ll='ls -alF' alias gs='git status' alias gd='git diff' alias n='watch -n 1 nvidia-smi' alias p='ps aux | grep -E' alias k9='kill -9' alias mem='free -h && vmstat 1 3'

有了alias n='watch -n 1 nvidia-smi',训练时只要敲一个字母,就能实时盯 GPU,不用每次都敲一长串。

history配合Ctrl-R反向搜索,能快速调出之前敲过的完整长命令。还有一个很冷门的技巧:history | grep <关键词>可以用来找当时的处理手法,尤其适合复盘自己排查过程。如果你和我一样经常在几个项目之间切换,还可以把常用命令写入不同项目的.env或启动脚本里,保持每个项目都有一个"一键跑起来"的入口。

6. 命令清单只是起点,排查思路才是打穿三域的关键

开发、训练、运维三个场景,归根结底依赖的是同一套命令底层逻辑:定位进程、查看状态、分析资源、处理异常。很多人把 Linux 命令当成"单词表"去背,我一向不太建议,因为背会了不用,等于没学。正确做法是带着具体问题去用,比如"我的训练任务为啥挂了?",然后沿着nvidia-smi → ps → top → free → dmesg这条路一步步走通,命令和思路就一起记住了。

最后分享一个小心得:我在排查进程故障时,习惯先把现场保存下来再动手。比如top -b -n 1 > top_$(date +%s).txt、dmesg -T > dmesg_$(date +%s).log、ss -tlnp > ss_$(date +%s).txt。等到修复完,回看这些快照往往能发现当初没注意到的线索。这套"先留档,再排障"的做法,是我在多次故障复盘之后才养成的习惯,成本极低,收益却很大,强烈建议你也试试。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询