路线图

进程管理

星辉 2026-07-02 阅读 9 min 1,900 字 路线图
进程管理 封面

Linux 运维基础 · 第九章 目标:理解进程生命周期,掌握查看、控制、调试进程的方法


1. 概述#

进程是操作系统的核心管理单元。运维每天做的就是"看看进程在不在、响应正不正常、要不要重启、为什么吃这么多 CPU"。

本章覆盖进程基础概念、查看工具、信号控制、前后台切换、nohup 守护——从"ps aux"到"会杀进程但不会杀错"。


2. 进程基础#

2.1 什么是进程#

进程 = 正在运行的程序的实例。每个进程有一个唯一编号 PID。

2.2 关键属性#

属性含义
PID进程 ID,内核分配的唯一编号
PPID父进程 ID(谁创建了这个进程)
UID运行进程的用户 ID
GID运行进程的组 ID
TTY关联的终端(? 表示没有终端,守护进程)

2.3 进程状态#

bash
ps aux
# USER  PID  %CPU %MEM    VSZ   RSS TTY  STAT  START   TIME COMMAND
# root    1   0.0  0.1 190972  6024 ?    Ss    Jun30   0:05 /usr/lib/systemd/systemd
#                                          ^^ 状态
状态码全称含义
RRunning正在运行或在运行队列中等待 CPU
SSleeping (Interruptible)可中断睡眠,等待某个事件(如 I/O)
DSleeping (Uninterruptible)不可中断睡眠,等待 I/O(通常是磁盘),kill -9 也杀不掉
ZZombie僵尸进程,子进程已结束但父进程没回收(不占内存,但占 PID)
TStopped被 Ctrl+Z 暂停的进程
XDead已死(极少看到)

状态码后缀含义#

后缀含义
<高优先级
N低优先级
s会话首进程 (session leader)
l多线程
+前台进程组

2.4 僵尸进程 vs 孤儿进程(高频考点,别搞混)#

概念谁先"没了"状态危害 / 处理
僵尸进程 (Zombie, Z)子进程先退出,父进程还活着但没调用 wait() 回收Z已死,只占一个 PID 表项(不占内存/CPU)。kill -9 对它无效(它已经死了,杀无可杀);要么让父进程 wait() 回收,要么杀掉父进程——父进程一死,僵尸被 PID 1 收养并立即回收
孤儿进程 (Orphan)父进程先退出,子进程还在跑R/S…无害。子进程立刻被 init/systemd(PID 1)收养,PPID 变成 1,将来它退出时由 PID 1 负责回收,不会变僵尸

一句话:僵尸是"孩子死了没人收尸",孤儿是"家长走了孩子被 PID 1 领养"。大量僵尸 = 父进程有 bug(漏 wait),要修父进程,不是去 kill -9 僵尸。


3. 查看进程#

3.1 ps — 快照#

bash
# ps aux:BSD 风格,最常用
ps aux                          # 所有用户的所有进程
ps aux | head -1                # 看列标题

# ps -ef:Unix 风格
ps -ef                          # 所有进程,全格式

# ps aux(BSD 无短横线)vs ps -ef(Unix 带短横线)的区别:
#   ps aux  更偏“资源视角”:有 %CPU/%MEM/VSZ/RSS/STAT/START,适合看谁吃资源
#   ps -ef  更偏“血缘视角”:有 PPID(父进程)、C、STIME,适合追父子关系
#   记不住就都用 ps aux;要看 PPID 时用 ps -ef

# 查找特定进程
ps aux | grep nginx             # 查找 nginx 相关进程
ps -ef | grep nginx
pgrep -a nginx                  # 更干净:直接列出匹配的 PID+命令,不会匹配到 grep 自己

# 按用户查找
ps -u heiye                     # heiye 用户的进程

# 只看某进程的线程
ps -Lf -p 1234                  # 进程 1234 的所有线程

# 树形结构(父子关系)
ps auxf                         # 树形显示(BSD 风格)
ps -ejH                         # 另一种树形
pstree                          # 更直观的树形(推荐)
pstree -p 1234                  # 看进程 1234 的子进程树

3.2 top — 实时监控#

bash
top
# top - 10:30:15 up 30 days,  2:15,  2 users,  load average: 0.05, 0.10, 0.15
# Tasks: 156 total,   1 running, 155 sleeping,   0 stopped,   0 zombie
# %Cpu(s):  0.3 us,  0.2 sy,  0.0 ni, 99.5 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
# KiB Mem :  4046564 total,  152348 free, 1089756 used, 2804460 buff/cache
# KiB Swap:        0 total,        0 free,        0 used. 2677548 avail Mem
#
#   PID USER  PR  NI    VIRT    RES    SHR S %CPU %MEM     TIME+ COMMAND
#  1234 root  20   0  162896   2212   1500 S  5.3  0.1   0:03.45 nginx
含义
PID进程 ID
VIRT虚拟内存总量:申请的地址空间(含未真正占用的、mmap 的文件、共享库),常常大得吓人但没意义,别拿它判断内存压力
RES驻留内存:真正占用的物理内存(含共享部分),看内存压力主要看这个
SHRRES 里可与其他进程共享的部分(如共享库、共享内存段)
S状态(R/S/D/Z/T,同 ps)
%CPUCPU 使用率
%MEM物理内存使用率(约等于 RES / 总内存)
TIME+累计占用的 CPU 时间

%CPU 可以超过 100%:默认 top 是 "Irix 模式",单核满载算 100%,所以 4 核跑满一个多线程进程能看到 380%。按 I(大写 i)可切换到 "Solaris 模式"(除以核数,上限 100%)。看到 %CPU 400 别慌,那是 4 个核都在忙。

top 交互快捷键#

快捷键功能
h帮助
q退出
k杀掉进程(输入 PID,再选信号)
P按 CPU 使用率排序(大写)
M按内存使用率排序(大写)
T按运行时间排序
1显示所有 CPU 核心的使用情况
f进入列管理界面
c切换完整命令路径的显示
o设置过滤条件
u按用户过滤

3.3 top 常用启动参数#

bash
top -p 1234                    # 只看某个进程
top -u nginx                   # 只看某个用户的进程
top -H -p 1234                 # 看进程 1234 的所有线程
top -d 2                       # 每 2 秒刷新一次(默认 3 秒)
top -b -n 1 > top.log          # 批处理模式(输出一次后退出)

3.4 htop — 更友好的替代#

bash
htop                            # 彩色、鼠标可点、可滚动
# 没有预装:apt install htop / yum install htop

4. 信号(Signal)#

信号是 Linux 进程间通信的基础机制,运维用它控制进程的启停。

4.1 常用信号#

信号编号名称含义能否被捕获/忽略/阻塞
1SIGHUP挂断(终端连接断了)/ 约定俗成用于重载配置
2SIGINT键盘中断(Ctrl+C)
3SIGQUITCtrl+\,退出并生成 core dump
9SIGKILL强制杀死,内核级,进程收不到不能(不可捕获/忽略/阻塞)
15SIGTERM优雅终止(kill 默认发的就是它)
17SIGCHLD子进程状态变化(用于父进程回收)
18SIGCONT继续执行(恢复被暂停的进程)
19SIGSTOP暂停进程(程序无法拦截)不能(不可捕获/忽略/阻塞)
20SIGTSTP终端暂停,即 Ctrl+Z(与 SIGSTOP 区别:被程序捕获)

两个铁律考点

  1. kill 不加参数默认发 SIGTERM(15),不是 SIGKILL(9)。
  2. 全部信号里只有 SIGKILL(9)SIGSTOP(19) 不能被捕获、忽略或阻塞——因为要保证内核永远能杀死/暂停任何进程。所以想优雅退出用 15(给进程清理机会),15 无效才上 9。

进程被信号杀死后的退出码:128 + 信号号#

被信号终止的进程,退出码是 128 + 信号编号——排查脚本/CI 失败时这是关键线索:

bash
sleep 100 &          # 后台跑
kill -9 %1; wait %1  # 用 SIGKILL(9) 杀;注意必须 wait %1(裸 wait 返回 0,取不到该任务退出码)
echo $?              # 137  = 128 + 9   → 生产里看到 137,十有八九是 OOM killer 干的
退出码= 128 +含义
130128 + 2 (SIGINT)用户按了 Ctrl+C
137128 + 9 (SIGKILL)kill -9OOM Killer 强杀(容器里最常见)
143128 + 15 (SIGTERM)被优雅终止(如 docker stopsystemctl stop 超时)

4.2 kill 发送信号#

bash
kill -l                          # 列出所有信号

kill 1234                        # 默认发送 SIGTERM(15),优雅终止
kill -15 1234                    # 显式发送 SIGTERM
kill -TERM 1234                  # 用名称

kill -9 1234                     # SIGKILL,强制杀(不留机会清理)
kill -KILL 1234

kill -HUP 1234                   # SIGHUP,常用于重新加载配置
kill -1 1234

# 按名称杀
killall nginx                    # 杀所有名为 nginx 的进程
pkill nginx                      # 同 killall
pkill -f "python app.py"         # 匹配完整命令行
pkill -u heiye                   # 杀某用户的所有进程(小心!)

4.3 什么时候用 -9,什么时候不用#

场景信号原因
正常停止服务kill 1234 / systemctl stop给进程机会清理资源、关闭文件
重载配置kill -HUP 1234不重启进程,只重读配置
进程卡死 / 无响应kill -15,不行再用 kill -9-9 强制杀,可能丢数据
不可中断睡眠 (D)kill -9 也杀不掉只能等 I/O 完成或重启机器

经验原则:永远先用 kill(SIGTERM),不到万不得已不用 kill -9


5. 前后台切换#

5.1 基本操作#

bash
# & — 放在后台运行
./long_task.sh &                 # 启动时直接放后台
# [1] 12345                      ← Shell 返回 [任务编号] PID

# Ctrl+Z — 暂停前台进程,放到后台
./long_task.sh
# 运行中...按 Ctrl+Z
# [1]+  Stopped   ./long_task.sh

# jobs — 查看后台任务
jobs
# [1]   Running    ./task1.sh &
# [2]-  Stopped    ./task2.sh
# [3]+  Stopped    ./task3.sh    ← + 表示当前任务,- 表示上一个

# bg — 让暂停的任务在后台继续
bg %1                            # %1 是 jobs 里看到的编号

# fg — 把后台任务调到前台
fg %1
# ./task1.sh 在终端里继续运行

5.2 注意事项#

  • 后台进程的 stdout/stderr 仍然会输出到终端(干扰你输入),关掉后台进程的输出:./task.sh > /dev/null 2>&1 &
  • 关闭终端时,所有后台进程默认会收到 SIGHUP 然后退出——除非用了 nohup

6. nohup — 进程守护#

6.1 解决的问题#

SSH 断线 → 终端关闭 → Shell 向所有子进程发 SIGHUP → 进程退出。nohup 让进程忽略 SIGHUP,断开终端也不退。

bash
# nohup 的基本用法
nohup ./long_running.sh &

# 默认输出到 nohup.out
# nohup: ignoring input and appending output to 'nohup.out'

# 指定输出文件
nohup ./long_running.sh > output.log 2>&1 &

6.2 实战:部署后关终端继续跑#

bash
# 场景:SSH 到服务器执行一个要跑 2 小时的脚本
# 直接跑:SSH 断掉脚本就停了
# 正确做法:

nohup ./deploy.sh > deploy_$(date +%Y%m%d_%H%M%S).log 2>&1 &

# 然后可以安全地关终端、断 VPN、合上笔记本

# 回来看结果
jobs                           # 同一个 Shell 里查
ps aux | grep deploy.sh        # 任意 Shell 里查
cat deploy_*.log               # 看输出

6.3 nohup vs disown vs setsid(三种"防 SIGHUP"手段)#

三者都能让进程在终端关闭后活下来,但机制和时机不同:

方式何时用机制
nohup cmd &启动前就决定要脱离让进程忽略 SIGHUP,并默认把输出重定向到 nohup.out
disown进程已经在跑(忘了加 nohup)把任务从 shell 的 job 表移除,shell 退出时不再向它发 SIGHUPdisown -h %1 则是保留在表里但标记为不发 HUP
setsid cmd想让进程彻底独立(新会话、脱离控制终端)新建 session,进程成为新会话首进程,PPID 变 1,天然不受终端关闭影响
bash
# 场景:任务已经跑起来了才想起没 nohup,用 disown 补救
./long_task.sh &        # 已经在后台
disown -h %1            # 之后关终端,它不会被 SIGHUP 干掉

# setsid:一步到位脱离终端(常用于把任务真正“扔出去”)
setsid ./long_task.sh > out.log 2>&1 < /dev/null
# 注意:setsid 后进程不再是当前 shell 的子任务,jobs 里看不到,要用 ps/pgrep 找

现代做法:真正的常驻服务,别用 nohup,用 systemd service(systemd-run --user 可快速临时托管一条命令),能自动重启、记日志、开机自启。nohup/disown/setsid 适合临时的一次性长任务。

6.4 nohup vs screen/tmux#

方式适用场景能否重连
nohup ... &一次性任务,不需要交互不能重连,只能看日志
screen / tmux需要交互(vim、监控、分屏)可以重新 attach 看完整输出
systemd service正式服务,需要开机自启和管理通过 systemctl 管理

7. 系统负载(Load Average)#

7.1 怎么看#

bash
uptime
# 10:30:15 up 30 days, 2:15, 2 users, load average: 0.05, 0.10, 0.15
#                                                  1分钟   5分钟   15分钟

cat /proc/loadavg
# 0.05 0.10 0.15 1/156 12345

7.2 怎么理解#

三个数分别是 1 分钟 / 5 分钟 / 15 分钟的平均值——看趋势:1 分钟 > 15 分钟说明负载在上升,反之在回落。

关键:Load 不是 CPU 使用率(不是百分比)。 它是平均活跃进程数,统计的是"正在跑 (R) + 不可中断等待 (D,主要是等磁盘 I/O)"的进程数量。所以:

值(相对 CPU 核数)含义
= CPU 核数刚好满负荷
< CPU 核数有余力
> CPU 核数过载,有进程在排队等资源

:Load 很高但 %Cpu(s)us/sy 很低、wa(iowait)很高 → 瓶颈不在 CPU,而在磁盘 I/O(一堆 D 状态进程在等盘)。这时加 CPU 没用,得查磁盘/存储。判断到底卡在哪,用 topwa、或 iostat -x 1iotop

bash
# 查看 CPU 核数
nproc                            # 逻辑核心数
grep -c processor /proc/cpuinfo

# 举例:4 核机器,load 0.5 = 很空闲,load 8 = 严重过载

8. 进程定位与排查进阶(pgrep / pkill / lsof / proc)#

8.1 pgrep / pkill — 按名字找/杀,不用 grep#

ps aux | grep xxx | grep -v grep | awk '{print $2}' 干净得多:

bash
pgrep nginx                 # 列出所有 nginx 的 PID
pgrep -a nginx              # 连命令行一起列出(-a = 全命令)
pgrep -f "python app.py"    # -f 匹配完整命令行(默认只匹配进程名)
pgrep -u www-data           # 某用户的进程
pgrep -n nginx              # 最新启动的那个;-o 则是最老的

pkill nginx                 # 按名杀(默认 SIGTERM)
pkill -f "python app.py"    # 按完整命令行杀
pkill -9 -u baduser         # 杀某用户全部进程(危险,先 pgrep 看清再动手)

pkill/killall 都是"按名字杀",但 killall 在不同发行版语义不同(Solaris 上会杀掉所有进程!),Linux 上用 pkill 更稳。

8.2 lsof — 列出打开的文件("一切皆文件"的排查神器)#

bash
lsof -p 1234                # 进程 1234 打开的所有文件/socket
lsof -i :8080               # 哪个进程占用了 8080 端口(排查“端口被占用”必备)
lsof -i tcp:443             # 指定协议
lsof /var/log/app.log       # 谁在占用这个文件
lsof -u nginx               # 某用户打开的文件
lsof +D /data               # 谁在占用 /data 目录下的文件(卸载不了盘时用)

# 经典坑:删了大日志文件但磁盘没释放 → 因为进程还开着这个文件句柄
lsof | grep deleted         # 找到还占着已删除文件的进程,重启它才真正释放空间

8.3 /proc// — 内核暴露的进程"活体档案"#

/proc 是虚拟文件系统,每个进程一个目录 /proc/<pid>/,想知道进程的一切都在这:

bash
ls -l /proc/1234/cwd        # 进程的当前工作目录(软链)
ls -l /proc/1234/exe        # 进程对应的可执行文件(软链,删了程序还能看到原路径)
cat  /proc/1234/cmdline | tr '\0' ' '   # 完整启动命令(参数用 \0 分隔)
cat  /proc/1234/environ | tr '\0' '\n'  # 进程的环境变量(排查“为啥读不到某变量”)
ls   /proc/1234/fd/          # 打开的文件描述符(0/1/2 就是 stdin/out/err)
cat  /proc/1234/status       # 状态、内存、线程数、PPID、UID 等一览
cat  /proc/1234/limits       # 该进程的 ulimit 限制(排查“too many open files”)

9. 定时任务(cron 与 systemd timer)#

运维刚需:日志轮转、定时备份、健康检查、清理临时文件,都靠定时任务。

9.1 crontab — 传统定时任务#

bash
crontab -e                  # 编辑当前用户的定时任务(首次会让你选编辑器)
crontab -l                  # 查看当前用户的任务
crontab -r                  # 删除当前用户的所有任务(危险,无二次确认)
crontab -u www -l           # 查看指定用户的任务(需 root)

五段时间格式(这是核心,务必记牢):

text
┌─────────── 分钟 (0-59)
│ ┌───────── 小时 (0-23)
│ │ ┌─────── 日   (1-31)
│ │ │ ┌───── 月   (1-12)
│ │ │ │ ┌─── 星期 (0-7,0 和 7 都是周日)
│ │ │ │ │
* * * * *  要执行的命令
bash
# 例子
0 3 * * *      /opt/backup.sh              # 每天凌晨 3:00
*/5 * * * *    /opt/check.sh               # 每 5 分钟
0 0 * * 0      /opt/weekly.sh              # 每周日 00:00
30 2 1 * *     /opt/monthly.sh             # 每月 1 号 2:30
0 9-18 * * 1-5 /opt/workhour.sh            # 工作日 9-18 点每个整点
@reboot        /opt/on_boot.sh             # 开机时执行一次(@daily/@hourly 同理)

系统级 cron 文件(比用户 crontab 多一个"用户名"字段):

bash
/etc/crontab                # 系统主 cron 表:分 时 日 月 周 用户 命令
/etc/cron.d/                # 放置额外 cron 片段(软件包常往这里丢)
/etc/cron.{daily,weekly,monthly}/   # 丢脚本进去就按频率跑(由 run-parts 驱动)

cron 踩坑(几乎人人中过招)

bash
# 坑 1:cron 的环境极简,PATH 很短 → 脚本里命令“找不到”
#   → 命令写绝对路径,或在脚本开头自己 export PATH
# 坑 2:% 在 crontab 里是特殊字符(表示换行)→ date +\%Y 里的 % 必须转义
0 3 * * * /opt/backup.sh > /var/log/backup_$(date +\%Y\%m\%d).log 2>&1
# 坑 3:默认执行结果会邮件发给用户,没配 MTA 就堆积 → 加 >log 2>&1 或 >/dev/null 2>&1
# 坑 4:时区 —— cron 按系统时区跑,确认 `timedatectl` 时区没错
# 调试:先跑 `bash -c 'env - /opt/backup.sh'` 模拟 cron 的干净环境,看会不会挂

9.2 systemd timer — 现代替代(Rocky/Ubuntu 都推荐)#

systemd timer 比 cron 更强:能记日志(journalctl)、支持依赖、错过的任务可补跑(Persistent=true)、精度到秒。生产环境越来越多用它。

ini
# /etc/systemd/system/backup.service  —— 定义“做什么”
[Unit]
Description=Nightly backup

[Service]
Type=oneshot
ExecStart=/opt/backup.sh
ini
# /etc/systemd/system/backup.timer  —— 定义“何时做”
[Unit]
Description=Run backup daily at 03:00

[Timer]
OnCalendar=*-*-* 03:00:00
Persistent=true          # 关机错过了,开机后补跑一次

[Install]
WantedBy=timers.target
bash
systemctl daemon-reload
systemctl enable --now backup.timer    # 启用并启动
systemctl list-timers                  # 看所有定时器:下次何时跑、上次跑没跑
journalctl -u backup.service           # 直接看任务的执行日志(cron 给不了这么方便)
对比cronsystemd timer
配置一行搞定,简单要写 .service + .timer 两个文件,啰嗦
日志靠自己重定向journalctl -u 直接看
错过补跑不行Persistent=true 可补
依赖/顺序可依赖其他 unit
适用简单周期任务有依赖/要审计/要补跑的生产任务

10. 实战#

实战 1:看进程的所有线程#

bash
# 找 Java 进程
PID=$(pgrep -f "java.*app.jar")

# 看该进程的线程
top -H -p $PID
# 或
ps -Lf -p $PID

# 看哪个线程吃 CPU 最多
top -H -p $PID      # 按 P 排序,记下高 CPU 的线程 PID
# 转成十六进制
printf "%x\n" 12345
# 用 jstack 看这个线程在干嘛
jstack $PID | grep -A 20 0x3039

实战 2:热重载 Nginx 配置#

bash
# 不要用 kill -9!也不要 systemctl restart(会中断连接)
# 优雅地重新加载配置,不中断现有连接

# 方法 1:SIGHUP
kill -HUP $(cat /var/run/nginx.pid)

# 方法 2:通过 systemctl
systemctl reload nginx

# 方法 3:nginx 自身的命令
nginx -s reload

# 确认配置生效
nginx -t                         # 先测试配置语法
nginx -s reload                  # 通过

实战 3:处理僵尸进程#

bash
# 找出僵尸进程
ps aux | grep 'Z'
# 或
top -b -n 1 | grep zombie

# 僵尸进程的 PPID 就是它的父进程
ps -o ppid= -p <zombie_pid>

# 解决方法:让父进程回收子进程
# 如果父进程是服务进程(如 nginx),正常 reload/restart 即可
# 如果父进程是写坏的脚本,kill 它然后 init(PID=1)会回收僵尸

# 僵尸少的话无害(占 PID 但不占内存)
# 大量僵尸说明父进程有 bug,需要修复

11. 小结#

知识点一句话记忆
进程状态R=运行, S=睡眠, D=不可中断(等磁盘,kill -9也杀不掉), Z=僵尸, T=暂停
僵尸 vs 孤儿僵尸=子死父没收(杀父进程解决);孤儿=父死子被 PID1 领养(无害)
psps aux 看资源全貌,ps -ef 看 PPID 血缘,pgrep 直接找 PID
topP按CPU排,M按内存排,H看线程;%CPU 可超 100%(多核);看内存看 RES 不看 VIRT
信号只有 9(KILL)/19(STOP) 不可捕获;kill 默认发 15(TERM);退出码=128+信号号(137=被9杀)
前后台& 后台,Ctrl+Z(SIGTSTP)暂停,bg/fg 切换
保活启动前 nohup,已跑用 disown,彻底脱离 setsid;常驻服务用 systemd
Load Average1/5/15分钟平均活跃进程数(R+D),不是CPU%;高 load+高 wa = 卡在磁盘
定时任务crontab -e 五段(分时日月周);现代用 systemd timer(能补跑+看日志)
排查lsof -i :端口 查占用,/proc/<pid>/ 看活体档案

上一章:八、管道与重定向 下一章:十、终端复用 tmux