Skip to content

进程管理

服务出问题排查,起点几乎都是进程——进程还在不在、资源占用正不正常、谁启动的、归哪个管理器管。进程这一层搞不清楚,后面看日志、改配置都无从下手。进程查看、信号、systemd 服务管理——这三块是排查时绕不过去的核心,知道每一步该看什么、用哪个命令,效率差出一个量级。

一、进程基础概念

进程说白了就是正在运行的程序实例。一个程序可以启动多个进程,每个进程有唯一的 PID(进程 ID)。进程还可以继续创建子进程,形成父子关系——这点在排查时特别重要,光看命令名不够,还得看 PID、PPID(父进程 ID)、运行用户、启动来源,才能搞清楚这个进程从哪来、归谁管。

查看当前 Shell 自己的进程信息,感受一下"自己也是个进程":

bash
echo $$            # 当前 Shell 的 PID
ps -p $$           # 查看该进程详情

查看进程树,直观看到父子关系:

bash
pstree -p

pstree 不是默认装的,需要单独安装:

bash
yum install psmisc -y    # RHEL 系
apt install psmisc -y    # Debian 系

进程由谁启动,直接决定了排查入口。systemd 拉起的进程、Shell 手动敲的进程、supervisor 托管的进程、容器运行时管理的进程——它们的日志位置、重启策略、环境变量、退出行为都不一样。看到一个陌生进程,先搞清楚"它是被谁拉起来的",后续排查方向才对。

二、ps 查看进程

ps 是查看进程快照的主力工具:

bash
ps aux           # BSD 风格,显示所有用户的所有进程
ps -ef           # Unix 风格,功能类似
ps aux | grep nginx

查看指定 PID 的进程,只显示关心的字段,排查时更聚焦:

bash
ps -p 1234 -o pid,ppid,user,stat,pcpu,pmem,cmd

几个常用字段的含义:PID 是进程 ID、PPID 是父进程 ID、USER 是运行用户、STAT 是进程状态(R 运行中、S 休眠、D 不可中断睡眠、Z 僵尸)、%CPU 和 %MEM 是资源占用、CMD 是启动命令行。

CMD 列在终端里经常被宽度截断,看不全完整启动命令。这时候直接读 /proc/PID/cmdline,内核会给你完整的:

bash
tr '\0' ' ' </proc/1234/cmdline

/proc/PID/cmdline 里参数之间是 null 字符分隔的(不是空格),用 tr 把 null 转成空格,就能看到人类可读的完整命令行。排查"这个进程到底是怎么启动的、带了什么参数",这一招比 ps 靠谱。

三、top 和 htop

ps 给的是某一时刻的快照,要看实时变化就用 top:

bash
top

top 界面里几个高频交互键:按 P 按 CPU 排序、M 按内存排序、1 展开看每个 CPU 核心、k kill 指定进程(会提示输入 PID)、q 退出。

htoptop 的增强版,支持鼠标、彩色显示、进程树更直观,界面友好得多:

bash
yum install htop -y    # RHEL 系
apt install htop -y    # Debian 系

排查 CPU 飙高的标准套路:先 top 定位是哪个进程吃 CPU,然后看这个进程内部是哪个线程在吃:

bash
top -H -p <pid>

-H 就是显示线程级别。Java、Go、Node.js 这种多线程服务,排查 CPU 瓶颈时看线程级别比看进程级别有效得多——进程整体 CPU 200%,可能是某一个线程占了 190%,定位到具体线程才能往下查(比如 jstack 看那个线程在干嘛)。

四、信号和 kill

信号是 Linux 进程间通信的一种方式,运维最常用的场景就是通知进程终止或者重载配置。几个常用信号要记住:

信号编号干什么
SIGTERM15请求进程正常退出(可被进程捕获处理,能做清理)
SIGKILL9强制终止(不可被捕获,内核直接动手)
SIGHUP1常用于通知进程重新加载配置
bash
kill 1234              # 默认发送 SIGTERM(15)
kill -15 1234
kill -9 1234           # 强制终止

SIGTERM 和 SIGKILL 的区别要理解透。SIGTERM 是"请你自己停掉"——进程收到之后可以做清理工作:关闭连接、刷新缓冲区、写完未完成的日志、释放资源,然后优雅退出。SIGKILL 是内核直接把进程干掉,进程没有任何机会做清理,半截的写入可能丢失、临时文件可能残留、连接可能没正常关闭。

所以默认先用 SIGTERM,给进程优雅退出的机会;实在不响应再用 kill -9。如果某个服务经常需要 kill -9 才能停,说明它的信号处理和退出逻辑有问题——正常服务收到 SIGTERM 应该能自己清理退出。

按名称批量操作进程:

bash
pkill nginx             # 向所有名为 nginx 的进程发 SIGTERM
pgrep -a nginx          # 先看有哪些匹配的进程

pkill 的匹配范围可能比你预期的大——pkill nginx 可能也匹配到 nginx-exporternginx-dashboard 这些名字里含 nginx 的进程,一棍子打死。所以执行 pkill 前先 pgrep -a(-a 显示完整命令行)看清楚到底会匹配到哪些,确认无误再 kill。

五、前台和后台任务

Shell 里命令默认在前台运行,会阻塞 Shell 直到执行完。命令末尾加 & 可以放到后台:

bash
sleep 100 &
jobs                # 查看当前 Shell 的后台任务

前后台切换:fg %1 把 1 号后台任务切回前台、Ctrl+z 暂停当前前台任务、bg %1 让暂停的任务在后台继续跑。

临时命令用 &Ctrl+z 没问题,但长期运行的任务靠 & 管理是隐患——SSH 一断开,进程可能收到 HUP 信号被终止;输出没地方去(除非手动重定向);进程状态全靠你记忆,重启没重启、有没有挂都不知道。所以长期任务要按场景选合适的管理工具:临时长命令用 tmux/screen 保持会话、服务进程用 systemd 托管、容器服务交给容器运行时或 K8s、定时任务用 systemd timer 或 cron。

六、systemd

systemd 是目前绝大多数 Linux 发行版的服务管理器,干的事不少:系统启动时按依赖关系拉起服务、监控运行状态、接管日志(journald)、进程异常退出时按策略重启。可以说现代 Linux 服务管理基本绕不开它。

systemd 管理的对象叫 unit,常见类型有几种:.service 定义服务进程、.timer 定时触发(cron 的替代方案)、.socket 基于 socket 激活服务(先监听,有请求再启动)、.mount 管理挂载点、.target 是一组 unit 的集合(相当于传统的运行级别)。

查看服务状态:

bash
systemctl status nginx
systemctl is-active nginx          # 只看是否在运行
systemctl is-enabled nginx         # 只看是否设了开机自启

启动停止重启:

bash
systemctl start nginx
systemctl stop nginx
systemctl restart nginx            # 整个重启,有短暂中断
systemctl reload nginx             # 重新加载配置,不重启进程(平滑)

开机自启:

bash
systemctl enable nginx             # 设为开机自启
systemctl disable nginx            # 取消开机自启

查看服务日志,journald 把日志统一管了:

bash
journalctl -u nginx -f                          # 持续跟踪(类似 tail -f)
journalctl -u nginx --since "1 hour ago"        # 只看最近一小时

服务启动失败的排查流程要形成肌肉记忆:

bash
systemctl status service-name --no-pager        # 看当前状态和最后几行日志
journalctl -u service-name -n 100 --no-pager    # 看最近 100 行日志
systemctl cat service-name                      # 看实际加载到的 unit 内容

systemctl cat 比手动去找 unit 文件靠谱——它会把你写的 /etc/systemd/system/ 下的覆盖配置(drop-in)也合并显示出来。很多时候你以为改了配置不生效,其实是 drop-in 文件覆盖了你的修改,systemctl cat 一看就明白。

七、unit 文件

unit 文件定义了 systemd 怎么管理一个服务。文件位置有几个:/usr/lib/systemd/system/(RHEL 系)或 /lib/systemd/system/(Debian 系)放软件包自带的 unit,/etc/systemd/system/ 放自定义 unit 和覆盖配置。自定义的放 /etc/systemd/system/,因为软件包升级时会覆盖 /usr/lib 下的,但不会动 /etc 下的。

一个最小可用的 service 文件长这样:

ini
# /etc/systemd/system/myapp.service
[Unit]
Description=My App
After=network.target

[Service]
Type=simple
WorkingDirectory=/opt/myapp
ExecStart=/opt/myapp/bin/app
Restart=always
RestartSec=5
User=app
Group=app

[Install]
WantedBy=multi-user.target

三个段各管一摊:[Unit] 描述服务本身和依赖关系、启动顺序;[Service] 定义进程怎么启动停止重启、以什么身份运行;[Install] 决定 systemctl enable 时挂在哪个 target 下面。

几个容易踩坑的字段说一下:

  • After 只表示启动顺序,不表示依赖——对方启动失败不影响自己启动。真正要表达"必须依赖它"用 Requires(强依赖,对方失败自己也失败)或 Wants(弱依赖,对方失败不影响自己)
  • ExecStart 必须写绝对路径,systemd 的 PATH 跟你登录 Shell 的 PATH 不一样,相对路径或者没写全的命令会找不到
  • Restart=always 配合程序快速失败会变成疯狂重启——程序一启动就崩,systemd 立刻重启,又崩,无限循环,把 CPU 和日志都打爆。这种情况加 RestartSec=5 拉长重启间隔,或者改用 on-failure
  • User/Group 决定服务以谁的身份跑——服务长期用 root 跑风险偏高,能不用 root 就不用

Type 的选择直接影响 systemd 能不能正确判断服务状态,记住这几个:simple(程序前台运行,启动即就绪,最常用)、forking(程序自己 fork 到后台,老式守护进程)、oneshot(跑完就退出,用于初始化)、notify(程序主动通知 systemd"我好了")。现在写服务优先让程序前台跑,用 Type=simple——把守护化的事交给 systemd,日志和退出状态都好追踪,别让程序自己 daemonize。

编写和启用的完整流程:

bash
vim /etc/systemd/system/myapp.service
systemctl daemon-reload          # 让 systemd 重新读取 unit 文件(改了 unit 必做)
systemctl start myapp
systemctl status myapp --no-pager
systemctl enable myapp           # 设置开机自启

systemctl daemon-reload 这一步特别容易忘——改了 unit 文件不 reload,systemd 还在用旧配置,你怎么 restart 都不生效,排查半天才发现是没 reload。养成习惯:改完 unit 必 daemon-reload。

最后列几个常见的"启动失败"现象和原因,排查时对照:

  • 手动执行正常,systemd 启动失败:多半是环境变量缺失(登录 Shell 有的变量 systemd 没有)、工作目录不对、或者路径没写绝对路径
  • 服务启动后立刻 failed:程序自己退出了(看 journalctl 报错),或者 Type 写错(systemd 以为它该 fork 结果它没 fork)
  • 修改 unit 后不生效:99% 是忘了 systemctl daemon-reload
  • 服务反复重启:Restart=always 撞上程序快速失败,加 RestartSec 或者查程序为什么崩
  • 日志找不到:程序把日志写到文件了而不是 stdout/stderr(systemd 只收 stdout/stderr),或者 journald 配了不持久化(重启后日志没了)