Learn
Linux/11-processes

进程管理: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 启动命令。

ps 观察进程与进程树
# 沙箱是精简容器(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/comm

top 是动态版的 ps:实时刷新、按 CPU/内存排序(按 M 键按内存排、P 按 CPU 排、q 退出),生产排查"谁吃满了 CPU"第一个打开的就是它。它是交互式程序,沙箱不演示;更现代的替代品 htop 支持鼠标和彩色界面。

3. 信号:进程间的"敲门方式"

kill 不是"杀",而是"发信号"——信号是内核递给进程的轻量通知,进程可以注册处理函数优雅响应。常用信号:

编号名称含义能否被捕获/忽略
15SIGTERM礼貌地请求退出(kill 默认)能——进程可做清理后退出
9SIGKILL立即强杀,进程无从得知不能
2SIGINT中断(就是 Ctrl+C)能
1SIGHUP挂断;很多服务约定收到后重载配置能
19/18SIGSTOP/SIGCONT暂停 / 继续STOP 不能被忽略
kill 1234            # 默认发 SIGTERM,给进程清理退路
kill -9 1234         # SIGKILL 强杀——最后手段
kill -HUP 1234       # 通知重载配置(nginx 等服务支持)
killall python3      # 按名字杀(同名全杀,小心)
pkill -f 'worker.py' # 按完整命令行模糊匹配
⚠️先 15 后 9:kill -9 是最后手段

SIGKILL 让进程没有任何机会保存数据、释放锁、删除临时文件、关闭连接——数据库类进程被 -9 可能直接损坏数据文件。正确流程:先 kill PID(TERM),等几秒观察;仍不退再 kill -9。把 kill -9 当默认习惯是运维大忌。

kill 与退出码
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 记下来,之后好管理
nohup + & 的标准组合
# 模拟一个长任务脚本
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 &
  • 下一章:系统资源——内存、磁盘与负载 →
🎯练习
  1. 在沙箱启动 3 个后台 sleep 300,用 ps 确认后,用一条命令杀掉全部 sleep 进程(提示:pkill 或 kill 配合 ps/grep/awk/xargs 管道)。
  2. 验证退出码规律:分别用 kill -15 和 kill -9 终止后台 sleep,用 wait $PID; echo $? 记录两次退出码,解释 143 和 137 的由来。
  3. 用 nohup 启动一个循环写日志的脚本并记录 PID 到文件,然后通过 kill $(cat xxx.pid) 停止它——这正是很多服务 stop 脚本的原理。
  4. 思考题:SSH 里直接 ./server & 后断开连接,进程为什么消失了?加 nohup 后为什么还建议同时重定向输出?