进程管理:ps、信号、kill 与前后台任务
程序是磁盘上的文件,进程是它运行起来的实例——有自己的 PID、内存空间和三条标准流。服务卡死要不要杀?怎么优雅地杀?关掉终端后台任务还活着吗?这些日常问题都归本章管。
1. 进程的基本事实
- 每个进程有唯一 PID(进程号);由父进程创建,记录 PPID(父进程号),整个系统构成一棵进程树,树根是 PID 1(init/systemd,容器里是入口进程);
- 你在 Shell 里敲的每个命令,都是 Shell fork 出来的子进程;
- 进程退出时留下退出码(0 成功,非 0 失败),父进程用它判断结果(
$?,第 15 章展开)。
2. ps:查看进程快照
ps 参数有 BSD(ps aux)和 System V(ps -ef)两大流派,输出大同小异:
ps aux # 全部进程:用户、PID、CPU%、MEM%、启动命令
ps -ef # 另一流派:多了 PPID 列
ps aux | grep nginx # 最常见组合:找特定进程
ps -o pid,ppid,comm # 自选输出列ps aux 关键列:%CPU/%MEM 资源占比、STAT 状态(R 运行、S 睡眠、Z 僵尸、T 停止)、TIME 累计 CPU 时间、COMMAND 启动命令。
# 沙箱是精简容器(busybox ps,选项有限),进程很少,正好看得清楚
echo '$ ps'
ps
echo ''
echo '$ 后台启动两个 sleep,再看:'
sleep 300 &
sleep 300 &
ps
echo ''
echo '$ 当前 Shell 自己的 PID(变量 $$):'
echo $$
echo ''
echo '$ /proc 里每个进程一个目录(一切皆文件):'
ls /proc | grep -E '^[0-9]+$' | head -n 5
cat /proc/self/commtop 是动态版的 ps:实时刷新、按 CPU/内存排序(按 M 键按内存排、P 按 CPU 排、q 退出),生产排查"谁吃满了 CPU"第一个打开的就是它。它是交互式程序,沙箱不演示;更现代的替代品 htop 支持鼠标和彩色界面。
3. 信号:进程间的"敲门方式"
kill 不是"杀",而是"发信号"——信号是内核递给进程的轻量通知,进程可以注册处理函数优雅响应。常用信号:
| 编号 | 名称 | 含义 | 能否被捕获/忽略 |
|---|---|---|---|
| 15 | SIGTERM | 礼貌地请求退出(kill 默认) | 能——进程可做清理后退出 |
| 9 | SIGKILL | 立即强杀,进程无从得知 | 不能 |
| 2 | SIGINT | 中断(就是 Ctrl+C) | 能 |
| 1 | SIGHUP | 挂断;很多服务约定收到后重载配置 | 能 |
| 19/18 | SIGSTOP/SIGCONT | 暂停 / 继续 | STOP 不能被忽略 |
kill 1234 # 默认发 SIGTERM,给进程清理退路
kill -9 1234 # SIGKILL 强杀——最后手段
kill -HUP 1234 # 通知重载配置(nginx 等服务支持)
killall python3 # 按名字杀(同名全杀,小心)
pkill -f 'worker.py' # 按完整命令行模糊匹配SIGKILL 让进程没有任何机会保存数据、释放锁、删除临时文件、关闭连接——数据库类进程被 -9 可能直接损坏数据文件。正确流程:先 kill PID(TERM),等几秒观察;仍不退再 kill -9。把 kill -9 当默认习惯是运维大忌。
echo '$ 启动一个后台 sleep,$! 拿到它的 PID:'
sleep 300 &
PID=$!
echo "PID = $PID"
ps
echo ''
echo '$ kill 它(默认 SIGTERM):'
kill $PID
sleep 1
echo '$ 再看进程表,它没了:'
ps
echo ''
echo '$ 被信号终止的进程,退出码 = 128 + 信号编号:'
sleep 300 &
PID2=$!
kill -9 $PID2
wait $PID2
echo "exit code = $? (128 + 9 = 137)"
echo '容器 OOM 被杀时看到 137,就是这么来的'137 = 128 + 9 这个知识点在容器时代出镜率极高:Kubernetes/Docker 容器内存超限被 OOM Killer 强杀,退出码正是 137。
4. 前台、后台与作业控制
默认命令在前台运行,占住终端直到结束。交互场景下的作业控制:
./build.sh & # 末尾 & :直接放后台运行
jobs # 列出当前 Shell 的后台作业
fg %1 # 把 1 号作业调回前台
# Ctrl+Z 把前台任务暂停并放到后台(状态是 Stopped)
bg %1 # 让暂停的作业在后台继续跑这些命令依赖交互式终端,沙箱脚本模式下重点掌握 & 与 $!(最近一个后台进程的 PID)、wait(等待后台任务结束并取回退出码)。
5. nohup:关掉终端也要活
后台任务有个致命弱点:终端关闭(SSH 断开)时,Shell 会给所有子进程发 SIGHUP,任务随之被杀。nohup 让进程忽略 SIGHUP:
nohup ./train.sh > train.log 2>&1 &
# │ │ └ 放后台
# │ └ 输出重定向落盘(不指定则写 nohup.out)
# └ 忽略挂断信号,SSH 断开也继续跑
echo $! > train.pid # 把 PID 记下来,之后好管理# 模拟一个长任务脚本
cat > task.sh << 'EOF'
#!/bin/bash
for i in 1 2 3; do
echo "step $i done"
sleep 1
done
echo 'all finished'
EOF
chmod +x task.sh
echo '$ nohup ./task.sh > task.log 2>&1 &'
nohup ./task.sh > task.log 2>&1 &
echo $! > task.pid
echo "PID 已记录: $(cat task.pid)"
echo '$ 任务在后台跑,先看一眼进程:'
ps | grep -v grep | grep task.sh || true
echo '$ wait 等它结束,再看日志:'
wait
cat task.log生产环境更完整的方案是交给进程管理器:systemd(第 17 章)、supervisor 或容器编排——它们额外提供开机自启、崩溃自动拉起、日志轮转。nohup 适合临时性的长任务(跑数、迁移脚本)。
子进程退出后,父进程还没调用 wait 收尸,进程表里就残留一条 Z 状态记录——僵尸进程。它不占 CPU 内存,只占一个 PID 槽位;少量无害,大量出现说明父进程有 bug。杀僵尸没用(它已经死了),要么让父进程正确 wait,要么杀掉父进程让 init 领养收尸。
小结
- 进程 = 运行中的程序:PID/PPID 构成进程树,退出码 0 表成功
ps aux看快照、top看实时;ps aux | grep x是高频组合- kill 发的是信号:默认 TERM(15) 优雅退出,KILL(9) 强杀不可捕获,HUP(1) 常约定为重载
- 先 15 后 9;
128+信号是被信号杀死的退出码(OOM 的 137) &放后台、$!取 PID、wait收尾;防断线用nohup cmd > log 2>&1 &- 下一章:系统资源——内存、磁盘与负载 →
- 在沙箱启动 3 个后台
sleep 300,用 ps 确认后,用一条命令杀掉全部 sleep 进程(提示:pkill 或 kill 配合 ps/grep/awk/xargs 管道)。 - 验证退出码规律:分别用 kill -15 和 kill -9 终止后台 sleep,用
wait $PID; echo $?记录两次退出码,解释 143 和 137 的由来。 - 用 nohup 启动一个循环写日志的脚本并记录 PID 到文件,然后通过
kill $(cat xxx.pid)停止它——这正是很多服务 stop 脚本的原理。 - 思考题:SSH 里直接
./server &后断开连接,进程为什么消失了?加 nohup 后为什么还建议同时重定向输出?