大家好,我是程序员柏风,AI 最全的 Vibecoding 学习地图 http://coderoadmap.cn 作者。
进程与定时任务
这一课你将能做到
- 用 ps / top 定位 Java 进程,说清 kill -15 与 kill -9 的区别并正确重启应用
- 用 nohup / & / jobs / fg / bg 管理后台任务,SSH 断开服务不挂
- 编写并调试 crontab 定时任务,实现每天凌晨自动备份 MySQL
为什么要学
Spring Boot 应用在 Linux 上就是一个进程。发布要停旧起新、OOM 要找元凶、日志疯涨要定位写盘的进程——不懂进程管理,你只能对着告警干瞪眼。crontab 则是穷人的调度中心:没上分布式调度平台之前,数据库备份、日志清理、证书续期全靠它。面试里「kill -9 和 kill -15 的区别」「crontab 五个字段」「进程怎么优雅关闭」都是后端高频题,答不好会显得没碰过生产。
人话:进程就是「正在跑的程序」,每个都有身份证号(PID)。kill 不是「杀」而是「发信号」——-15 是敲门说「请收拾东西离开」(应用能执行 shutdown hook、释放连接池),-9 是直接抬走(Spring Boot 的优雅停机、MQTT 消息都来不及处理)。定时任务就是给系统请了个闹钟管家。
先建立图像
进程生命周期与信号路径:
java -jar app.jar
│
▼
内核分配 PID(如 12345)
│
kill -15 12345(SIGTERM,默认) kill -9 12345(SIGKILL)
│ │
JVM 收到信号 → 执行 shutdown hook 内核直接回收,JVM 无感知
优雅停机:停接新请求、处理完存量、 线程当场冻结,可能:
关闭连接池、刷盘、退出 - 数据没刷盘
- 临时文件残留
- 端口 TIME_WAIT 堆积
crontab 五字段口诀:
┌──────── 分 0-59
│ ┌────── 时 0-23
│ │ ┌──── 日 1-31
│ │ │ ┌── 月 1-12
│ │ │ │ ┌ 星期 0-7(0 和 7 都是周日)
│ │ │ │ │
* * * * * 要执行的命令
分 时 日 月 星期
例:0 3 * * * 每天 03:00
例:*/5 * * * * 每 5 分钟
例:0 9 * * 1-5 工作日每天 09:00
跟着做
步骤 1:看进程:ps 的两副面孔
ps -ef | grep java # 风格 1:全格式列表
ps aux | grep mysql # 风格 2:BSD 风格,含 CPU/内存占用
ps -ef | grep [j]ava # 技巧:[j]ava 避免匹配到 grep 自己
pgrep -l java # 只列进程名和 PID,最清爽
pstree -p | head -20 # 进程树,看父子关系
面试常问「ps -ef 和 ps aux 的区别」:内容重叠,-ef 强在完整命令行(看启动参数),aux 强在 %CPU %MEM 排序排查资源大户,ps aux --sort=-%mem | head 直接找内存 Top。
步骤 2:实时观察:top
top # 默认 3 秒刷新,q 退出
# 交互键:P 按 CPU 排序,M 按内存排序,1 展开每核
# 关键行解读:
# %Cpu(s): 12.3 us, 3.1 sy, 80.0 id, 4.0 wa
# us=用户态 sy=内核态 id=空闲 wa=等磁盘(高 wa 先查 IO)
# load average: 0.5, 0.4, 0.3 ← 1/5/15 分钟负载,经验:不超过 CPU 核数
top 是进入服务器的第一眼体检,us 高=应用在算,wa 高=磁盘瓶颈,load 远超核数=过载。
步骤 3:优雅重启一个「应用」
用 sleep 模拟 Java 进程练手,不伤真服务:
mkdir -p ~/proc-lab && cd ~/proc-lab
nohup sleep 300 > sleep.out 2>&1 & # 启动后台进程
echo $! # $! = 最近一个后台进程的 PID
pgrep -l sleep
kill <PID> # 默认就是 -15 优雅终止
pgrep -l sleep || echo "已退出"
nohup ... & 组合:& 放后台,nohup 让进程忽略挂断信号(SSH 断开也不死),输出自动写入 nohup.out 或重定向文件。
步骤 4:kill -15 vs kill -9 实测
# 先用 -15
nohup sleep 300 > /dev/null 2>&1 &
pid=$!
kill -15 $pid
sleep 1
pgrep -l sleep || echo "-15 后进程已退出"
# 模拟僵死进程用 -9(对 sleep 无感,真正的区别在 Java 应用)
nohup sleep 300 > /dev/null 2>&1 &
pid=$!
kill -9 $pid
生产脚本标准姿势:先 kill -15,等 30 秒轮询进程还在,才 kill -9 兜底。直接 -9 是新手最常被点名的坏习惯。
步骤 5:任务的前后台与 jobs
sleep 100 # 前台运行,Ctrl+Z 暂停
# [1]+ 已停止 sleep 100
jobs # 看任务列表
bg %1 # 转后台继续跑
fg %1 # 拉回前台,Ctrl+C 终止
这套操作适合「跑了半小时的命令突然想腾出终端」的场景;真正的长任务(如导数据)一开始就用 nohup。
步骤 6:crontab 自动备份 MySQL
mkdir -p ~/proc-lab/bin ~/proc-lab/backup ~/proc-lab/logs
cat > ~/proc-lab/bin/backup-mysql.sh << 'EOF'
#!/bin/bash
set -euo pipefail
LOG=/home/$USER/proc-lab/logs/backup.log
BK=/home/$USER/proc-lab/backup
echo "[$(date '+%F %T')] 备份开始" >> "$LOG"
mysqldump -uroot -p"你的密码" --single-transaction mydb \
| gzip > "$BK/mydb-$(date +%F).sql.gz"
# 保留 7 天,删更早的
find "$BK" -name "mydb-*.sql.gz" -mtime +7 -delete
echo "[$(date '+%F %T')] 备份完成:$(ls -lh $BK | tail -1)" >> "$LOG"
EOF
chmod 700 ~/proc-lab/bin/backup-mysql.sh
# 测试脚本本身(没有 MySQL 可先注释 mysqldump 行验证流程)
bash ~/proc-lab/bin/backup-mysql.sh
# 编辑当前用户的 crontab
crontab -e
# 加入(每天 03:00,注意 % 在 crontab 里要转义或避免):
# 0 3 * * * /home/你的用户名/proc-lab/bin/backup-mysql.sh >> /home/你的用户名/proc-lab/logs/cron.log 2>&1
crontab -l # 查看已生效的任务
ls /etc/cron.d* /var/log/cron* 2>/dev/null # 系统级配置与执行日志位置
--single-transaction 让 InnoDB 备份不锁表,是线上 mysqldump 的标配参数。
讲清楚要点
- kill 发的是信号,不是子弹:
kill -15(SIGTERM)请求进程自行退出,进程可捕获并清理;kill -9(SIGKILL)内核强制回收,进程无法捕获也无法忽略。Java 应用停机永远先 -15。 $!与$$别搞混:$!是最近一个后台进程的 PID(写脚本常拿它记录或等待),$$是当前 Shell 自己的 PID。- nohup 解决「SSH 断开进程就死」:SSH 会话结束时终端收到 SIGHUP,普通前台/后台进程随之退出;nohup 让进程忽略 SIGHUP。更正规的生产做法是 systemd 托管(
systemctl restart myapp),第 11 课展开。 - crontab 有独立的环境:只有极简 PATH,不加载你的 .bashrc;相对路径、java 命令可能找不到。规范:脚本里全用绝对路径,环境变量写进脚本或
source /etc/profile;输出必须重定向到日志,否则邮件堆积。 - crontab 里
%是特殊字符:会被解释成换行导致命令被截断。date +\%F转义,或把复杂逻辑封装进脚本、crontab 只调脚本路径——后者是最佳实践。 - 僵尸进程与孤儿进程:子进程退出但父进程没回收 → 僵尸(Z 状态,占 PID 不占内存),父进程先死则孤儿被 init 收养。
ps aux | grep ' Z '找僵尸,根因通常在父进程没 wait,重启父进程即可清理。
你可能会懵的地方
-
kill -9 为什么连 Java 的 shutdown hook 都不执行? SIGKILL 由内核直接处理,进程根本收不到信号,JVM 没机会跑任何钩子。所以 Spring Boot 的优雅停机(
server.shutdown=graceful)、注册中心下线通知都依赖 -15 阶段完成。 -
crontab -e 保存后任务怎么被触发的? cron 守护进程(crond)每分钟醒来,比对
/var/spool/cron/用户名里所有任务的时间字段,匹配则 fork 执行。所以 crontab 最小粒度是分钟;需要秒级用循环 + sleep 或专业调度器。 -
怎么知道我的 crontab 到底跑没跑? 三查:
crontab -l确认配置在;grep CRON /var/log/syslog或/var/log/cron确认 cron 有没有执行记录;看脚本自己的重定向日志。三层日志缺一不可。 -
ps -ef里的 PPID 是什么? 父进程 PID。排查「谁启动了这个进程」时顺着 PPID 往上找,常见发现是「不是应用自己的脚本,是 supervisord/systemd 拉起来的」,这决定你 kill 之后它会不会被自动复活。 -
后台任务退出终端后为什么有的死了有的活着? 没加 nohup 的后台任务在 Shell 退出时收到 SIGHUP 默认退出;nohup 或
setsid启动的忽略它。Bash 有时可配shopt -s huponexit行为不同,别赌,长任务一律 nohup。
错法对照
| 错法 | 问题 | 正解 |
|---|---|---|
上来就 kill -9 java_pid | 跳过优雅停机,丢数据留尾巴 | 先 kill -15,超时再 -9 |
crontab 直接写 java -jar ... | cron 环境找不到 java | 写绝对路径或脚本内 export PATH |
| crontab 任务不重定向输出 | 报错无处可看 + 邮件堆积 | >> 日志 2>&1 |
| 定时任务里用相对路径 | cron 工作目录是 $HOME,路径全错 | 脚本内用绝对路径或 cd 固定 |
* * * * * 每 10 秒 | crontab 最小粒度 1 分钟 | 循环 + sleep 封装,或换调度工具 |
卡点急救
- crontab 不执行:按顺序查——
crontab -l确认任务在 →/var/log/cron确认触发过 → 手动用sh 脚本跑确认脚本本身没问题 → 检查脚本内是否依赖了交互环境的 PATH/变量 → 确认输出重定向了能看到报错。 - kill -15 后进程不退出:应用可能在等长连接收尾。等 30 秒(可用 for 循环 + pgrep 轮询)再 -9;频繁卡住则排查应用优雅停机配置和线程池是否设置了 daemon。
nohup: ignoring input and appending output to nohup.out看不到输出:这是提示不是报错,输出在 nohup.out。规范做法启动时就重定向:nohup cmd > app.out 2>&1 &。
面试常问
-
kill -15 和 kill -9 的区别? -15 发送 SIGTERM,可被进程捕获,JVM 会执行 shutdown hook:停止接收新请求、处理完存量、关闭连接池后退出,实现优雅停机;-9 发送 SIGKILL,内核直接回收进程,不可捕获不可忽略,可能造成数据未落盘、注册中心残留节点。生产标准流程:-15 → 轮询等待 → 超时 -9 兜底。
-
crontab 五个字段的含义?写一个每天凌晨 3 点备份 MySQL 的任务。 分 时 日 月 星期。答案:
0 3 * * * /opt/scripts/backup-mysql.sh >> /var/log/backup.log 2>&1。加分点:说明备份脚本内含--single-transaction防锁表、find -mtime +7 -delete清理旧备份、输出重定向便于排查、cron 环境变量有限所以脚本内用绝对路径。 -
CPU 飙高怎么排查? ①
top找到高 CPU 的 Java 进程 PID;②top -Hp PID或ps -T -p PID找到具体线程;③printf '%x' 线程号转十六进制;④jstack PID | grep 十六进制 -A 20定位线程栈。前两步全是 Shell 命令——这就是进程管理知识直接对接 JVM 排查的例子。
自测三问
- 为什么停 Spring Boot 应用不能直接 kill -9?
- 写出「工作日每天 09:00 执行 xx.sh」的 crontab 行。
nohup cmd &中两个部分各自解决什么问题?
小结一句话
进程管理是「发对信号」:ps/top 看清楚、-15 优雅停、-9 只兜底、nohup 保后台;crontab 是「请对管家」:五个字段 + 绝对路径 + 日志重定向,MySQL 凌晨自动备份就这么简单。
延伸
- crontab.guru 在线表达式校验
- 下一课:网络命令——curl 测接口、ss 查端口,把服务「通不通」一眼看穿。