Shell 进程管理
更新时间:2026-08-28。本文是 Shell 编程主题第⑤族:进程管理与系统信息,共 20 个命令。
与 Linux 命令速查第③族 的分工:那边是字典式速查(37 个命令,含
nsenter、unshare、slabtop等);本文把日常最常用的 20 个讲透——重点是脚本里怎么管进程,以及df/free/uptime这三个最容易读错的命令。
一、查看进程
1.1 ps
ps 有两种风格:BSD 风格(ps aux,不带横杠)和 System V 风格(ps -ef),输出格式不同但信息等价,选一个顺手的用就行。日常最常用的是 ps aux | grep xxx,但更规范的写法是 pgrep。
-o 自定义输出列是它最实用的特性——只取需要的字段,输出干净且便于脚本解析(ps -o pid,ppid,%cpu,%mem,cmd -p 1234)。--sort 按指定列排序(--sort=-%mem 按内存倒序,负号表示倒序),--forest 或 -f --forest 用 ASCII 画出父子进程树,排查"这个进程是谁启动的"时很直观。-L 显示线程(多线程程序会展开成多行,LWP 列是线程 ID),-e 等同于 -A(所有进程)。
ps aux 的 STAT 列是进程状态,排查时必看:R 正在运行或可运行,S 可中断睡眠(等事件,最常见),D 不可中断睡眠(通常在等磁盘 I/O,这种状态杀不掉),T 已停止(被 SIGSTOP 暂停),Z 僵尸进程(父进程没回收,占一个 PID 但不占资源),< 高优先级,N 低优先级,s 是会话首进程,+ 在前台进程组。看到大量 D 状态通常意味着存储有问题;Z 状态如果持续存在,说明父进程有 bug 没回收子进程。
ps aux # 所有进程(BSD 风格)
ps -ef # 所有进程(System V 风格)
ps -ef --forest # 树状显示父子关系
ps -o pid,ppid,%cpu,%mem,stat,cmd -p 1234 # 自定义列
ps aux --sort=-%mem | head # 按内存占用倒序
ps -L -p 1234 # 显示线程
ps -eo pid,ppid,cmd --no-headers # 不打印表头(脚本解析用)1.2 pgrep 与 pidof
pgrep 按名称或模式找 PID,比 ps | grep 干净(不会出现 grep 自己那条)。-f 匹配完整命令行而不只是进程名(找 python worker.py 这类必须用 -f,因为进程名只是 python),-a 同时显示命令行(确认匹配对不对),-u 按用户过滤,-x 精确匹配(不做子串匹配),-n/-o 只取最新/最旧的那个,-P 找指定父进程的子进程。pgrep 的退出码同样可用于判断(找到 0,没找到 1)。
pidof 更简单,按程序名取 PID,-s 只返回一个,-x 也匹配脚本(默认只匹配可执行文件)。它不支持 -f 那种完整命令行匹配,功能比 pgrep 弱,胜在好记。
pgrep -f "python worker" # 按完整命令行找
pgrep -a -f nginx # 显示 PID 和完整命令(确认用)
pgrep -u www -x nginx # 精确匹配 nginx,且属于 www 用户
pgrep -c -f worker # 只计数
pgrep -P 1234 # 找 PID 1234 的子进程
pidof nginx # 按程序名
pidof -s sshd # 只返回一个1.3 top
top 给的是实时画面,适合看"现在谁在吃资源"。交互键要记住几个:1 展开显示每个 CPU 核心(多核机器必按,否则只看平均值容易误判),M 按内存排序、P 按 CPU 排序,H 切换到线程视图,c 显示完整命令行(看进程到底在跑什么),k 直接发信号杀进程,u 只显示某用户的进程,q 退出。
命令行常用 -d 设刷新间隔(top -d 2),-p 只看指定进程(top -p 1234,也可 -p 1234,5678 看多个),-b -n 1 是批处理模式——输出一次就退出,适合脚本里采集快照(top -b -n 1 | head -20)。-o 指定排序字段(top -o %MEM)。
top # 实时视图
top -d 2 # 2 秒刷新
top -p 1234 # 只看指定进程
top -u www # 只看某用户的进程
top -b -n 1 # 批处理,输出一次后退出(脚本采集用)
top -o %MEM # 按内存排序启动
# 交互:1 看每核、M 内存排序、P CPU 排序、H 线程、c 完整命令、k 杀进程二、信号与终止
2.1 kill 与信号
kill 的本质是发信号,不只是"杀进程"。不给信号时默认发 SIGTERM(15)。理解几个关键信号的区别很重要:
| 信号 | 编号 | 作用 | 能否被捕获 |
|---|---|---|---|
SIGHUP | 1 | 挂断,多数守护进程用它重读配置 | 能 |
SIGINT | 2 | 中断(等同 Ctrl-C) | 能 |
SIGQUIT | 3 | 退出并产生 core dump | 能 |
SIGTERM | 15 | 优雅终止(默认,进程可清理后自行退出) | 能 |
SIGKILL | 9 | 强制杀死(内核直接回收,进程收不到) | 不能 |
SIGSTOP | 19 | 暂停进程 | 不能 |
SIGCONT | 18 | 恢复被暂停的进程 | 能 |
SIGUSR1/2 | 10/12 | 用户自定义,不少程序用它做 reopen 日志 | 能 |
核心原则:先 TERM,不行再 KILL。 SIGTERM 允许进程执行清理逻辑——落盘数据、释放锁、关闭连接、通知上游;SIGKILL 是内核直接回收,进程一瞬间就没了,数据库、消息队列这类有状态的程序很容易因此留下需要修复的烂摊子。
两个实用技巧:kill -0 PID 不发任何信号,只用来检测进程是否存在(存在返回 0,否则报错),脚本里判断存活很干净;kill -HUP 让支持热重载的服务(nginx、sshd)重读配置而不中断服务。
kill 1234 # 默认 SIGTERM,优雅退出
kill -9 1234 # 强制杀死(最后手段)
kill -HUP $(pgrep -f nginx) # 重载配置,不中断服务
kill -0 1234 && echo 存活 # 只检测,不发信号
kill -l # 列出所有信号名与编号
kill -STOP 1234 # 暂停(可 kill -CONT 恢复)2.2 pkill 与 killall
pkill 是 pgrep + kill 的合体,按模式匹配并发送信号。它同样支持 -f(匹配完整命令行)、-u(按用户)、-P(按父进程)。-n/-o 只作用于最新/最旧的匹配进程,避免误杀一堆。信号可以直接写名字(pkill -TERM -f app)或编号(pkill -9 -f app)。
killall 按进程名杀(不支持 -f 那种完整命令行匹配),-e 要求精确匹配(防止长名字被误伤),-i 逐个确认,-q 静默(没匹配到也不报错,脚本里友好),-w 等待进程真正结束后才返回,-r 用正则匹配进程名,-u 限定用户。
安全习惯:批量操作前先 pgrep -a -f pattern 看一眼匹配到了什么,确认范围对了再发信号。这一步能避免绝大多数"杀错进程"的事故。
pkill -f "python worker" # 按完整命令行(默认 TERM)
pkill -TERM -f "python worker" # 显式指定信号
pkill -9 -f "python worker" # 强制
pkill -u deploy # 杀某用户的所有进程
pkill -n -f app # 只杀最新启动的那个
killall -e nginx # 精确匹配进程名
killall -i -r "worker.*" # 正则匹配并逐个确认
killall -q -w app # 静默并等待进程结束三、前后台与后台运行
3.1 作业控制
Shell 的作业控制管理当前会话里的任务。& 放到后台,Ctrl-Z 把前台任务挂起(暂停),bg %1 让挂起的任务在后台继续跑,fg %1 调回前台,jobs 列出所有任务。任务号用 %1、%2 表示,%% 或 %+ 指当前任务,$- 指上一个。$! 变量保存最近一个后台进程的 PID——启动后立刻记录它,才能后续管理。
disown 把任务从 Shell 的作业表移除,这样 Shell 退出时不会给它发 SIGHUP。它是 nohup 之外的一个补救手段:任务已经在前台跑了,才想起来要它脱离终端,Ctrl-Z → bg → disown -h %1 三步就能救回来。
command & # 后台运行
jobs # 列出后台任务
jobs -l # 带 PID
fg %1 # 调到前台
bg %1 # 后台继续(先 Ctrl-Z 挂起)
echo $! # 最近一个后台进程的 PID
disown -h %1 # 移除作业表,Shell 退出不影响它3.2 nohup 与彻底脱离终端
nohup 让进程忽略 SIGHUP 信号,终端关闭后继续运行。标准写法是三个动作一起:nohup cmd > out.log 2>&1 &——nohup 忽略挂断信号、> out.log 2>&1 把输出(含错误)重定向到文件(否则默认写进 nohup.out,且多个进程会互相覆盖)、末尾 & 放后台。启动后立刻 echo $! > app.pid 记录 PID,否则之后只能靠 pgrep 找。
nohup 的局限:它只保证"终端断了进程不死",做不到"回来看现场"。进程的输出只能从日志文件看,没法再交互。所以长期任务更推荐 tmux 或 screen——它们能让你重连后回到原来的终端画面。另外 setsid 比 nohup 更彻底:它让进程完全脱离当前会话(成为新的会话首进程),连控制终端都没有。
# 标准后台启动并记录 PID
nohup ./app > app.log 2>&1 &
echo $! > app.pid
# 之后优雅停止
kill $(cat app.pid)
# setsid 彻底脱离会话
setsid ./app > app.log 2>&1 < /dev/null &3.3 完整生命周期示例
# 1. 启动并记录 PID
nohup python server.py > server.log 2>&1 &
echo $! > server.pid
# 2. 确认在运行
pgrep -F server.pid # -F 从文件读 PID 并检测
# 3. 优雅停止(给 10 秒自己收尾,超时再强杀)
kill $(cat server.pid); sleep 10
pgrep -F server.pid >/dev/null && kill -9 $(cat server.pid)
# 4. 确认已退出
pgrep -F server.pid >/dev/null && echo "仍在运行" || echo "已停止"四、磁盘与内存
4.1 df — 磁盘空间
df 看的是文件系统级别的容量。-h 人类可读(必加),-T 显示文件系统类型(ext4/xfs/tmpfs 等),-i 看 inode 使用情况(这是最容易忽略的维度),-x 排除某类型(df -h -x tmpfs -x devtmpfs 能过滤掉内存文件系统,只看真实磁盘),-t 只看某类型,--total 输出合计行,--output 自定义列。
inode 用尽是独立的故障类型:磁盘还有空间,但 inode 用光了,照样报 "No space left on device"。小文件特别多的场景(邮件、缓存、容器镜像层)容易触发,所以排查磁盘问题要 df -h 和 df -i 一起看。
df -h # 人类可读
df -hT # 含文件系统类型
df -i # inode 使用(排查看这个)
df -h -x tmpfs # 排除内存文件系统
df -h / # 只看根分区
df -h --total # 合计4.2 du — 目录占用
du 统计文件占了多少空间,与 df 视角不同。-s 只显示总计(不加的话会列出每个子目录),-h 人类可读,--max-depth=N 限制显示层级,-a 连文件也列(默认只列目录),-c 输出合计行,--exclude 排除模式,--apparent-size 显示文件表面大小而非实际占用的磁盘块(稀疏文件两者差异巨大)。
最常用的是找"磁盘大户":du -h --max-depth=1 / | sort -rh | head。注意 du 会递归遍历整个目录树,在大目录上很慢——临时排查可以用 ncdu(交互式,且扫描更快更好用)。
df 与 du 对不上是个经典现象,通常是因为:有文件被删除了但仍被某个进程打开(句柄没关,空间未释放),用 lsof | grep deleted 能找到。或者存在挂载点遮蔽(某个目录下挂了新磁盘,du 统计不到被遮蔽的旧数据)。
du -sh dir # 目录总大小
du -sh * # 当前目录各项
du -h --max-depth=1 / # 看一级子目录
du -h --max-depth=1 / | sort -rh | head # 找磁盘大户
du -sh --exclude='*.log' dir # 排除某类文件
du -csh dir1 dir2 # 带合计4.3 free — 内存
free -h 的输出里,最容易误读的是"可用内存"。Linux 会把空闲内存拿去做磁盘缓存(buff/cache),这部分在需要时可以立即回收,所以真正可用的看 available 列,而不是 free 列。很多"内存快满了"的告警其实是误报——缓存占用高是正常且有益的。
free 的单位:-h 自动选单位,-m MB,-g GB。-s N 持续每 N 秒刷新一次(free -h -s 5 相当于简单的内存监控),-c N 配合 -s 指定刷新次数,-t 加一行合计(物理内存 + swap)。
swap 的使用量是个健康指标:偶尔用一点正常,swap 持续被大量使用说明物理内存不足,系统在靠磁盘硬撑,性能会急剧下降。
free -h # 人类可读
free -m # MB
free -h -s 5 # 每 5 秒刷新
free -h -t # 带合计行
# 看 available 列才是真正可用内存,不是 free 列五、系统信息
5.1 uptime 与负载
uptime 输出三件事:运行时长、登录用户数、1/5/15 分钟平均负载。负载值表示"可运行(运行态 + 不可中断睡眠态)的进程平均数",判断标准是与 CPU 核数比较:负载 4 在 4 核机器上是满负荷,在 16 核机器上很轻松。
三个数值的趋势比单点更有意义:1 分钟高但 15 分钟低,说明是刚刚起来的瞬时压力;15 分钟持续高才是真的过载。注意负载包含 D 状态(等 I/O)的进程,所以负载高不代表 CPU 忙——大量进程在等磁盘时负载也会飙升,这时 CPU 反而很闲。-p 用人类可读格式显示运行时长,-s 显示系统启动的具体时间。
uptime # load average: 1.50, 1.20, 1.10
uptime -p # "up 3 weeks, 2 days"
uptime -s # 启动时间 2026-08-05 09:12:33
nproc # 看 CPU 核数(配合判断负载)5.2 uname / hostname / whoami / id
uname 查内核与系统:-a 全部,-r 内核版本(装驱动、查兼容性时最常用),-m 架构(x86_64 / aarch64),-s 内核名,-n 网络节点主机名,-o 操作系统。
hostname 查主机名:-I 显示本机所有 IP(比 ifconfig 快),-f 完整域名(FQDN),-s 短主机名,-d 域名部分。注意 -I 是大写 i。
whoami 显示当前有效用户名(等价于 id -un)。id 信息更全:不带参数显示 uid、gid 和所有属组,-u 只显示 uid(0 就是 root,脚本里判断权限常用),-g 主组 id,-G 所有附加组 id,-n 配合前面几个显示名字而非数字,-un 等价于 whoami。
uname -a # 全部信息
uname -r # 内核版本
uname -m # 架构(x86_64/aarch64)
hostname -I # 本机 IP(大写 i)
hostname -f # 完整域名
id # uid/gid/所有组
id -u # 只显示 uid(0=root)
[ "$(id -u)" -eq 0 ] && echo "你是 root"
id -nG # 所有属组的名字5.3 date
date 是脚本里生成时间戳的主要工具。格式化用 + 开头:%Y 年、%m 月、%d 日、%H 时、%M 分、%S 秒、%F 等价于 %Y-%m-%d、%T 等价于 %H:%M:%S、%s Unix 时间戳。
-d 做时间运算非常实用:date -d yesterday、date -d "3 days ago"、date -d "next monday"、date -d @1700000000(时间戳转可读格式)。日志清理、备份命名都靠它。date -s "2026-08-28 10:00" 设置系统时间(需 root,但正式环境应该用 NTP 同步而不是手工设)。
date # 当前时间
date '+%F %T' # 2026-08-28 10:30:45
date +%s # Unix 时间戳
date -d "yesterday" '+%F' # 昨天的日期
date -d "7 days ago" '+%F' # 7 天前(清理旧日志用)
date -d @1700000000 # 时间戳转可读
date -d "2026-08-28 +3 days" '+%F' # 日期运算5.4 w / who / lscpu / lsblk
w 显示登录用户及他们在做什么,顶部还带负载信息,排查"谁在这台机器上"很方便。-h 不显示表头,-u 忽略用户名(显示进程属主)。who 更简洁,只列登录用户,-b 显示上次系统启动时间,-r 显示当前运行级别。
lscpu 一次性给出 CPU 全景:架构、核数、每核线程数、型号、各级缓存大小、NUMA 节点数、是否开启虚拟化。判断"这台机器有多少核"用它比 nproc 详细得多。lsblk 以树状列出块设备(磁盘、分区)及挂载点,-f 附带文件系统类型和 UUID(配置 /etc/fstab 时需要 UUID),-o 自定义列,-d 只看磁盘不看分区。
w # 谁在线 + 在做什么 + 负载
who -b # 上次启动时间
lscpu # CPU 全景(核数/缓存/NUMA)
lsblk -f # 块设备 + UUID + 挂载点(配 fstab 用)
lsblk -o NAME,SIZE,FSTYPE,MOUNTPOINT # 自定义列六、脚本实战:把进程管起来
在脚本里操作进程,最常出问题的不是命令本身,而是"杀得太急"和"杀不干净"。下面几个模式覆盖了日常的大部分场景。
6.1 优雅关闭:脚本里响应信号
#!/usr/bin/env bash
set -euo pipefail
PIDFILE=/var/run/myapp.pid
cleanup() {
echo "收到退出信号,正在清理..."
rm -f "$PIDFILE"
# 这里放真正的清理逻辑:停子进程、关连接、写状态
exit 0
}
trap cleanup SIGTERM SIGINT EXIT
echo $$ > "$PIDFILE"
echo "服务已启动,PID=$$"
while true; do
sleep 5
donetrap ... EXIT 是关键:无论脚本正常结束、被 Ctrl-C 打断还是收到 TERM 信号,清理逻辑都会执行。临时文件和 PID 文件一定要在这种机制里删——否则服务重启几次,磁盘上就堆了一堆没人认领的 .pid。
6.2 简单的服务守护
#!/usr/bin/env bash
check_and_restart() {
if ! pgrep -f "myapp" >/dev/null; then
echo "$(date): myapp 已退出,正在重启" >> /var/log/myapp-watch.log
nohup /opt/myapp/start.sh >> /var/log/myapp.log 2>&1 &
fi
}
while true; do
check_and_restart
sleep 30
done这种轮询式守护胜在简单,适合内部工具。生产环境更推荐 systemd(配 Restart=always),它自带重启限制、日志采集和资源控制,详见 软件包与服务速查。
6.3 防止脚本重复运行
#!/usr/bin/env bash
LOCKFILE=/var/lock/myjob.lock
exec 200>"$LOCKFILE"
if ! flock -n 200; then
echo "另一个实例正在运行,退出" >&2
exit 1
fi
# ... 真正的任务逻辑 ...定时任务最怕的就是上一轮还没跑完、下一轮又启动了,两个实例同时写同一份数据。flock 用文件锁做互斥,进程退出时锁自动释放,不需要额外清理。
6.4 资源监控与告警
#!/usr/bin/env bash
THRESHOLD=85
df -h | awk -v t="$THRESHOLD" 'NR>1 {
gsub(/%/, "", $5)
if ($5 >= t) printf "警告: %s 已用 %s%% (挂载点 %s)\n", $1, $5, $6
}'
# 可用内存低于 1G 时告警(注意看 available 列,不是 free 列)
free -m | awk 'NR==2 && $7 < 1024 {printf "警告: 可用内存仅 %dMB\n", $7}'注意 free 要读 available 列(第 7 列)而不是 free 列——Linux 会把大量内存用作缓存,那部分可随时回收,只看"空闲"会误报。
七、与主线衔接
ps/top只是入口,深度定位 CPU/内存热点要回到 tools/cpu 的pidstat/mpstat/perf,以及 性能观测与调试速查。- 信号机制(
SIGTERM/SIGKILL)与进程异常退出,衔接 崩溃排查。 df/du/free对应磁盘与内存观测主线,见 tools/disk 与 tools/memory。- 更完整的命令清单(含
nsenter、slabtop、vmstat等)见 Linux 命令速查第③族。
一句话总结
进程管理 = 查(ps/pgrep/top)+ 信号(kill,先 TERM 后 KILL)+ 后台(nohup/&,长期任务用 tmux)+ 资源(df/du/free);脚本里额外记住四条——批量操作前先 pgrep -a 确认范围,临时文件在 trap ... EXIT 里清理,定时任务用 flock 防重复,看内存认准 available 列而非 free 列。