CodeRoad Map 程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。
由 程序员柏风 独立维护 · 关于作者
学习 更多
进程与服务 ps、systemctl、journalctl。
进程与服务
这一课你将能做到
用 ps aux/top 找到任意进程,说清 PID、CPU%、MEM%、STAT 的含义,并算出 Java 进程的实际内存占用
区分 kill -15 与 kill -9,写出「先优雅后强制」的停服流程;会查进程被 OOM 杀掉的证据
手写一个 systemd 单元文件让 Spring Boot JAR 开机自启 + 崩溃自动重启 ,并用 journalctl 查它的日志
为什么要学
「我用 nohup 起的服务,SSH 一断就没了 / 服务器一重启就找不到进程了 / OOM 了没人拉起来」——这三个问题同根同源:你把进程托管给了终端会话,而不是托管给了系统 。
生产环境对进程的要求是:随系统启动(开机自启)、异常退出自动拉起(Restart=on-failure)、日志有统一归档(journald)、停止时优雅收尾(SIGTERM 让 Spring Boot 走完 shutdown 流程)。这一整套能力就是 systemd——现代 Ubuntu/CentOS 的 1 号进程。学完本课,「服务起不来」「怎么重启」「日志在哪」这类问题你都有标准答案。
先建立图像
进程是什么
程序是磁盘上的文件(backend.jar),进程是跑起来的程序实例 ——内核给它分配内存、CPU 时间片和一个唯一编号 PID。每个进程有父进程(PPID),构成进程树:
systemd (PID 1) ← 所有进程的祖先,开机第一个进程
├─ sshd ← 你能 SSH 进来的原因
│ └─ bash ← 你的终端 shell
│ └─ java -jar backend.jar ← nohup 起的服务(父进程死了就成孤儿)
├─ nginx (master)
│ └─ nginx (worker × N)
└─ mysqld
孤儿进程最终被 systemd 收养——这就是 nohup 能「断网不死」的原理,但它依然没有开机自启和崩溃重启 ,所以生产要用 systemd 托管。
进程管理的三条查询主线
谁在跑? ps aux / top / systemctl status
在哪个端口? ss -tlnp(防火墙课细讲)
日志在哪? journalctl -u 服务名(systemd 托管的)/ 应用自己的 logs/
跟着做
步骤 1:ps——谁在跑
ps aux # 快照:所有用户的所有进程
ps aux | head -5 # 前 5 行(注意第 1 行就是头部说明)
# 专找 Java 进程(部署最常用的一条):
ps aux | grep java | grep -v grep
# 输出示例:www 12345 3.2 25.1 4123456 856788 ? Sl 10:00 2:15 java -jar backend.jar
# │ │ │ │ │ │ └ 启动时间/累计CPU时间/命令
# │ │ │ │ └ 虚拟内存VSZ └ S=睡眠 l=多线程 ?=无终端
# │ │ │ └ 物理内存占用%(%MEM)
# │ │ └ CPU%(多核可超100)
# │ └ PID(进程号,杀进程/看日志全靠它)
# └ 运行用户
ps -ef | grep java # 另一种风格:全格式,能看到 PPID(父进程号)
pstree -p | head -20 # 进程树,一眼看清谁是谁的爹
步骤 2:top——动态监控与排序
top
交互键(在 top 界面里按):
P 按 CPU 排序(默认) M 按内存排序 ← 最常用两个
1 展开每个 CPU 核 q 退出
Shift+h 显示线程 c 显示完整命令行
%Cpu(s): 12.3 us, 2.1 sy, 0.0 ni, 84.6 id ← us用户态 sy内核态 id空闲(id高=闲)
MiB Mem : 3936.0 total, 512.2 free, 2048.0 used, 1375.8 buff/cache
MiB Swap: 2048.0 total, 2048.0 free, 0.0 used. 1600.0 avail Mem
Java 进程内存怎么估:%MEM × 总内存 = RSS 常驻内存。top 里按 c 显示完整命令,能直接看到 -Xmx 参数。
(可选)更现代的 htop:sudo apt install htop && htop,支持鼠标和搜索 F3。
步骤 3:kill——先优雅,后强制 sleep 600 & # & 放后台跑,回显里 [1] 是作业号 4567 是 PID
ps aux | grep "sleep 600" | grep -v grep
kill 4567 # 默认发 SIGTERM(15):请你自己收拾好再走
ps aux | grep "sleep 600" | grep -v grep # 已消失
# kill -9 的正确使用场景:进程卡死、连 SIGTERM 都不理时
sleep 600 &
PID=$(pgrep -f "sleep 600" | head -1) # pgrep 直接拿 PID,比 grep 方便
kill -9 $PID # SIGKILL:内核直接回收,进程没机会善后
生产铁律:停 Java 服务先 systemctl stop(内部发 SIGTERM,Spring Boot 执行优雅关闭:停接新请求、处理完存量、释放连接),等 10-30 秒没停再 kill -9 兜底 。kill -9 对数据库连接池、正在写的文件有截断风险。
步骤 4:nohup 的问题——亲手验证 # 用 nohup 起 java(没装 java 先跳过此行,用 sleep 模拟效果一样)
nohup sleep 600 > app.log 2>&1 &
ps aux | grep "sleep 600" | grep -v grep # 在跑
# 问题1:SSH 断开重连后,你很难再「找到它、管理它」——没有服务名,只有 PID
# 问题2:服务器重启后,它不会回来
# 问题3:崩了没人拉起
步骤 5:手写 systemd 单元(本课核心) sudo vim /etc/systemd/system/coderoadmap-backend.service
写入(注释行可留可删,路径按你的实际部署位置改):
[Unit]
Description=CodeRoadMap Spring Boot backend
After=network.target mysql.service # 网络就绪 & MySQL 先起
[Service]
Type=simple
User=www # 用低权限用户跑(用户课详解)
WorkingDirectory=/www/wwwroot/coderoadmap
ExecStart=/usr/bin/java -Xms512m -Xmx1024m -jar /www/wwwroot/coderoadmap/backend.jar --spring.profiles.active=prod
Restart=on-failure # 异常退出自动拉起
RestartSec=5 # 5 秒后重启
SuccessExitStatus=143 # SIGTERM 正常退出码
LimitNOFILE=65536 # 文件句柄上限(Java 网络服务需要)
[Install]
WantedBy=multi-user.target # 多用户运行级别=开机自启
sudo systemctl daemon-reload # 改了单元文件必须先重载
sudo systemctl enable --now coderoadmap-backend # enable=开机自启 --now=顺手立刻启动
systemctl status coderoadmap-backend --no-pager # 绿色 active (running) = 成功
(没装 Java 时 status 会报 exit code 1——正常,包管理课装完 JDK 它就能活。)
步骤 6:systemd 日常操作五连 sudo systemctl start coderoadmap-backend # 起
sudo systemctl stop coderoadmap-backend # 停(SIGTERM 优雅关闭)
sudo systemctl restart coderoadmap-backend # 重启(发布新 JAR 后必做)
systemctl status coderoadmap-backend # 看状态(active/inactive/failed)
systemctl is-enabled coderoadmap-backend # 确认开机自启是否生效
崩溃自动拉起的验证(实验环境):kill -9 $(pgrep -f backend.jar),等 5 秒,systemctl status——进程换了个新 PID 又活了。这就是 Restart=on-failure 在工作。
步骤 7:journalctl——systemd 服务的日志之家 journalctl -u coderoadmap-backend -n 50 --no-pager # 最近 50 行
journalctl -u coderoadmap-backend -f # 实时滚动(= systemd 版 tail -f)
journalctl -u coderoadmap-backend --since "10 min ago" --no-pager
journalctl -u coderoadmap-backend -p err --no-pager # 只看 error 级别
注意:journal 收的是标准输出/标准错误 ——Spring Boot 日志同时也会写到自己的 logs/ 目录(logback 配置决定),两边都在,排障时先用 journal 确认「进程说了什么」,再下钻文件日志。
步骤 8:查「进程为什么死了」——OOM 实战排查 # 假设 java 进程莫名消失,先看 systemd 怎么说:
systemctl status coderoadmap-backend --no-pager
# 若显示 SIGKILL / Failed with result 'oom-kill',基本坐实内存不足被内核杀
# 找内核证据:
sudo dmesg -T | grep -i -E "kill|oom" | tail -5
# 典型输出:Out of memory: Killed process 12345 (java) total-vm:4G, anon-rss:2G
journalctl -k | grep -i oom | tail -5 # 内核日志的 journal 视角
看到了 OOM 证据后,方向是调小 -Xmx、加服务器内存或排查内存泄漏——不是盲目重启。
讲清楚要点
nohup vs systemd 的本质区别 :nohup 只解决「挂断信号」一个问题;systemd 提供开机自启、崩溃重启、优雅停止、日志归档、资源限制 全套托管。生产一律 systemd(宝塔的「Java 项目」底层也是类似托管)。
SIGTERM(15) 与 SIGKILL(9) 的分工 :TERM 是「请求」,进程可执行清理逻辑(Spring Boot 的优雅停机);KILL 是「处决」,内核直接回收,进程毫无感知。永远先 TERM 后 KILL,中间留 10-30 秒 。killall java、pkill -f 属于范围杀伤,多实例机器上慎用。
Restart=on-failure 不是万能保险 :它兜「异常退出」,兜不了「活着但坏掉」(线程池堵死、假死)。假死要靠健康检查 + 外部监控(如 actuator + 云监控告警)。
enable 与 start 是两件独立的事 :start 只管现在,enable 只管开机。enable --now 一步到位。改单元文件后必须 daemon-reload,否则 systemd 用的是旧配置。
内存三大口径别混 :-Xmx 是 JVM 堆上限(不等于进程总内存);top 的 %MEM/RSS 是实际物理占用(≈堆 + 元空间 + 线程栈 + 堆外 + JIT 代码缓存);2G 小内存机器跑 -Xmx1g 的服务,加上 MySQL/Nginx 很容易 OOM——部署前先做内存预算。
前台/后台/守护三态 :前台(占着终端,Ctrl+C 即死)、后台(&,终端断开风险)、守护(systemd/PM2 托管)。PM2 是 Node 生态的进程管家(platform 前端常用 pm2 start npm --name platform -- start,命令风格与 systemctl 类似:start/stop/restart/list/logs)。
你可能会懵的地方
jobs、&、Ctrl+Z、fg、bg 是什么关系?
这些是 shell 的「作业控制」:cmd & 直接后台跑;Ctrl+Z 把前台进程暂停 挂起,bg 让它继续在后台跑,fg 调回前台,jobs 列出作业。注意后台进程仍挂在当前会话下,SSH 断开可能收到 SIGHUP 被杀——所以才有 nohup,也所以生产用 systemd。
ps aux 里的 STAT 字母什么意思?
R=运行中,S=可中断睡眠(绝大多数进程的状态),D=不可中断睡眠(通常在等磁盘 IO,大量 D 状态=存储瓶颈),Z=僵尸(已死但父进程没收尸),T=暂停,附加符:s=会话首进程,l=多线程,<=高优先级。看到 D 堆积去查磁盘,看到 Z 堆积去查父进程。
端口被占用「Address already in use」,怎么定位?
ss -tlnp | grep 8080 找到占用进程的 PID → ps -p PID -o user,pid,cmd 确认是谁 → 是旧实例就 stop/restart 它,是别的服务就改端口。重启永远解决不了「谁该占这个端口」的问题。
systemctl status 显示 active 但接口全 503?
active 只代表进程活着,不代表业务健康。要看两处:journalctl 日志有没有持续报错;curl http://127.0.0.1:8080/actuator/health 业务探活。「假活」是生产排障的常见陷阱。
一台机器跑多个 Java 服务,单元文件怎么组织?
每个服务一个 .service 文件(coderoadmap-backend.service、coderoadmap-worker.service),各自独立 start/stop/restart。不要写一个脚本 fork 多个进程再交给 systemd 管——一崩全崩,日志混流。
错法对照 错法 问题 正解 生产用 nohup java -jar & 当正式部署 无自启/无自愈/无日志归档 systemd 单元托管(或宝塔 Java 项目) 一律 kill -9 停服务 跳过优雅关闭,连接/事务/缓存受损 先 systemctl stop(SIGTERM),必要时再 -9 覆盖 JAR 后不 restart 就验收 内存里跑的还是旧代码 发布流程 = 上传 + restart + 健康检查 改 .service 文件后不 daemon-reload systemd 仍按旧配置执行 systemctl daemon-reload服务起不来就反复 restart 刷屏日志还可能触发启动限速 先 journalctl -u xxx -n 50 看失败原因 -Xmx 设成机器内存的 90%堆外内存+系统进程挤爆,OOM 连环 Xmx ≈ 可用内存 50-70%,留足堆外与系统 出问题先重启「看能不能好」 毁掉现场,根因永远查不到 先留证据:status + journalctl + dmesg
卡点急救
systemctl start 失败:Unit not found :单元文件路径/文件名打错(必须在 /etc/systemd/system/ 且以 .service 结尾),改完记得 daemon-reload。
服务反复重启(start-request repeated too quickly) :RestartSec 太短或进程秒崩。先看 journalctl -u xxx -n 100 找崩溃原因(多为配置错、端口被占、JDK 路径错——ExecStart 的 java 必须绝对路径),修好再启。
java: command not found(单元文件里) :systemd 不走你的 PATH。which java 拿绝对路径写进 ExecStart;自装 JDK 未登记到系统路径时,可在单元里加 Environment=JAVA_HOME=/opt/jdk-17 并用 $JAVA_HOME/bin/java 展开前的绝对路径。
进程消失且 status 显示 inactive(无报错) :查内核 OOM:sudo dmesg -T | grep -i oom。确认后调 -Xmx / 升配 / 查泄漏;同时确认 Restart=on-failure 存在(OOM kill 视为失败会自动拉起)。
journalctl 输出全在屏幕外 :加 --no-pager 看一次性输出;-f 实时跟踪;--since "2026-10-06 10:00" --until "10:30" 锁定时间窗。
面试常问
kill 和 kill -9 的区别?
kill PID 默认发 SIGTERM,进程可捕获并执行优雅关闭(Spring Boot 停接新请求、处理完存量请求、释放资源);kill -9 发 SIGKILL,不可捕获不可忽略,内核直接回收。规范是先 TERM、观察退出,卡死才 KILL。Java 常见优雅退出码 143(128+15)。
如何让一个服务开机自启并在崩溃后自动重启?
写 systemd 单元:[Install] 段 WantedBy=multi-user.target 后 systemctl enable;[Service] 段 Restart=on-failure(或 always)+ RestartSec=N。改完 daemon-reload。验证:systemctl is-enabled xxx、kill -9 后看是否自动拉起。
ps aux 输出中你重点看哪几列?
USER(谁在跑——权限排查)、PID(进程号)、%CPU/%MEM(资源水位)、VSZ/RSS(虚拟/常驻内存,Java 看 RSS 更真实)、STAT(R/S/D/Z 状态,D 多=IO 瓶颈,Z 多=僵尸)、COMMAND(启动参数,含 Xmx 配置)。
线上 Java 进程突然消失,怎么排查?
固定四步:① systemctl status 服务名 看退出状态(failed/oom-kill/signal);② journalctl -u 服务名 --since "1 hour ago" 看临终日志;③ sudo dmesg -T | grep -i oom 查内核 OOM 记录;④ 应用自身日志 tail -200 logs/backend.log。结论通常是 OOM(调内存/查泄漏)或未捕获异常(修代码/加配置)。
systemd 和传统 init(SysV)的区别?
systemd 是并行启动的服务管理器:单元化配置(.service)、依赖管理(After/Requires)、socket/定时器/日志(journald)一体化,启动快、可观测;SysV 按脚本串行启动、靠 /etc/init.d 与 chkconfig 管理。现代 Ubuntu(16.04+)/CentOS(7+)均为 systemd,systemctl 是唯一入口。
僵尸进程是什么,怎么处理?
子进程已退出但父进程没调用 wait 回收,残留的进程表项。无法 kill(它已死),处理方式:让父进程回收,或杀掉/重启父进程使其被 init 收养后清理。大量僵尸说明父进程代码有问题。
自测三问
nohup java -jar app.jar & 和 systemd 托管相比,少了哪些生产能力?
写出优雅重启 coderoadmap-backend 的完整命令序列(含改过单元文件的情况)。
Java 进程被内核杀掉的最可能原因是什么?用什么两条命令找证据?
小结一句话 进程是跑起来的程序,服务是被系统托管的进程——用 systemd 给你的 JAR 装上「开机自启 + 崩溃自愈 + 优雅停机 + 日志归档」四件套,从「跑起来」升级到「敢上线」。
延伸