进程管理
Linux 运维基础 · 第九章 目标:理解进程生命周期,掌握查看、控制、调试进程的方法
1. 概述#
进程是操作系统的核心管理单元。运维每天做的就是"看看进程在不在、响应正不正常、要不要重启、为什么吃这么多 CPU"。
本章覆盖进程基础概念、查看工具、信号控制、前后台切换、nohup 守护——从"ps aux"到"会杀进程但不会杀错"。
2. 进程基础#
2.1 什么是进程#
进程 = 正在运行的程序的实例。每个进程有一个唯一编号 PID。
2.2 关键属性#
| 属性 | 含义 |
|---|---|
| PID | 进程 ID,内核分配的唯一编号 |
| PPID | 父进程 ID(谁创建了这个进程) |
| UID | 运行进程的用户 ID |
| GID | 运行进程的组 ID |
| TTY | 关联的终端(? 表示没有终端,守护进程) |
2.3 进程状态#
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
# ^^ 状态| 状态码 | 全称 | 含义 |
|---|---|---|
| R | Running | 正在运行或在运行队列中等待 CPU |
| S | Sleeping (Interruptible) | 可中断睡眠,等待某个事件(如 I/O) |
| D | Sleeping (Uninterruptible) | 不可中断睡眠,等待 I/O(通常是磁盘),kill -9 也杀不掉 |
| Z | Zombie | 僵尸进程,子进程已结束但父进程没回收(不占内存,但占 PID) |
| T | Stopped | 被 Ctrl+Z 暂停的进程 |
| X | Dead | 已死(极少看到) |
状态码后缀含义#
| 后缀 | 含义 |
|---|---|
< | 高优先级 |
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 — 快照#
# 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 — 实时监控#
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 | 驻留内存:真正占用的物理内存(含共享部分),看内存压力主要看这个 |
| SHR | RES 里可与其他进程共享的部分(如共享库、共享内存段) |
| S | 状态(R/S/D/Z/T,同 ps) |
| %CPU | CPU 使用率 |
| %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 常用启动参数#
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 — 更友好的替代#
htop # 彩色、鼠标可点、可滚动
# 没有预装:apt install htop / yum install htop4. 信号(Signal)#
信号是 Linux 进程间通信的基础机制,运维用它控制进程的启停。
4.1 常用信号#
| 信号编号 | 名称 | 含义 | 能否被捕获/忽略/阻塞 |
|---|---|---|---|
| 1 | SIGHUP | 挂断(终端连接断了)/ 约定俗成用于重载配置 | 能 |
| 2 | SIGINT | 键盘中断(Ctrl+C) | 能 |
| 3 | SIGQUIT | Ctrl+\,退出并生成 core dump | 能 |
| 9 | SIGKILL | 强制杀死,内核级,进程收不到 | 不能(不可捕获/忽略/阻塞) |
| 15 | SIGTERM | 优雅终止(kill 默认发的就是它) | 能 |
| 17 | SIGCHLD | 子进程状态变化(用于父进程回收) | 能 |
| 18 | SIGCONT | 继续执行(恢复被暂停的进程) | 能 |
| 19 | SIGSTOP | 暂停进程(程序无法拦截) | 不能(不可捕获/忽略/阻塞) |
| 20 | SIGTSTP | 终端暂停,即 Ctrl+Z(与 SIGSTOP 区别:可被程序捕获) | 能 |
两个铁律考点:
kill不加参数默认发 SIGTERM(15),不是 SIGKILL(9)。- 全部信号里只有 SIGKILL(9) 和 SIGSTOP(19) 不能被捕获、忽略或阻塞——因为要保证内核永远能杀死/暂停任何进程。所以想优雅退出用 15(给进程清理机会),15 无效才上 9。
进程被信号杀死后的退出码:128 + 信号号#
被信号终止的进程,退出码是 128 + 信号编号——排查脚本/CI 失败时这是关键线索:
sleep 100 & # 后台跑
kill -9 %1; wait %1 # 用 SIGKILL(9) 杀;注意必须 wait %1(裸 wait 返回 0,取不到该任务退出码)
echo $? # 137 = 128 + 9 → 生产里看到 137,十有八九是 OOM killer 干的| 退出码 | = 128 + | 含义 |
|---|---|---|
| 130 | 128 + 2 (SIGINT) | 用户按了 Ctrl+C |
| 137 | 128 + 9 (SIGKILL) | 被 kill -9 或 OOM Killer 强杀(容器里最常见) |
| 143 | 128 + 15 (SIGTERM) | 被优雅终止(如 docker stop、systemctl stop 超时) |
4.2 kill 发送信号#
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 基本操作#
# & — 放在后台运行
./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,断开终端也不退。
# 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 实战:部署后关终端继续跑#
# 场景: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 退出时不再向它发 SIGHUP。disown -h %1 则是保留在表里但标记为不发 HUP |
setsid cmd | 想让进程彻底独立(新会话、脱离控制终端) | 新建 session,进程成为新会话首进程,PPID 变 1,天然不受终端关闭影响 |
# 场景:任务已经跑起来了才想起没 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,用
systemdservice(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 怎么看#
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 123457.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 没用,得查磁盘/存储。判断到底卡在哪,用top看wa、或iostat -x 1、iotop。
# 查看 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}' 干净得多:
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 — 列出打开的文件("一切皆文件"的排查神器)#
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>/,想知道进程的一切都在这:
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 — 传统定时任务#
crontab -e # 编辑当前用户的定时任务(首次会让你选编辑器)
crontab -l # 查看当前用户的任务
crontab -r # 删除当前用户的所有任务(危险,无二次确认)
crontab -u www -l # 查看指定用户的任务(需 root)五段时间格式(这是核心,务必记牢):
┌─────────── 分钟 (0-59)
│ ┌───────── 小时 (0-23)
│ │ ┌─────── 日 (1-31)
│ │ │ ┌───── 月 (1-12)
│ │ │ │ ┌─── 星期 (0-7,0 和 7 都是周日)
│ │ │ │ │
* * * * * 要执行的命令# 例子
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 多一个"用户名"字段):
/etc/crontab # 系统主 cron 表:分 时 日 月 周 用户 命令
/etc/cron.d/ # 放置额外 cron 片段(软件包常往这里丢)
/etc/cron.{daily,weekly,monthly}/ # 丢脚本进去就按频率跑(由 run-parts 驱动)cron 踩坑(几乎人人中过招):
# 坑 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)、精度到秒。生产环境越来越多用它。
# /etc/systemd/system/backup.service —— 定义“做什么”
[Unit]
Description=Nightly backup
[Service]
Type=oneshot
ExecStart=/opt/backup.sh# /etc/systemd/system/backup.timer —— 定义“何时做”
[Unit]
Description=Run backup daily at 03:00
[Timer]
OnCalendar=*-*-* 03:00:00
Persistent=true # 关机错过了,开机后补跑一次
[Install]
WantedBy=timers.targetsystemctl daemon-reload
systemctl enable --now backup.timer # 启用并启动
systemctl list-timers # 看所有定时器:下次何时跑、上次跑没跑
journalctl -u backup.service # 直接看任务的执行日志(cron 给不了这么方便)| 对比 | cron | systemd timer |
|---|---|---|
| 配置 | 一行搞定,简单 | 要写 .service + .timer 两个文件,啰嗦 |
| 日志 | 靠自己重定向 | journalctl -u 直接看 |
| 错过补跑 | 不行 | Persistent=true 可补 |
| 依赖/顺序 | 无 | 可依赖其他 unit |
| 适用 | 简单周期任务 | 有依赖/要审计/要补跑的生产任务 |
10. 实战#
实战 1:看进程的所有线程#
# 找 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 配置#
# 不要用 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:处理僵尸进程#
# 找出僵尸进程
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 领养(无害) |
| ps | ps aux 看资源全貌,ps -ef 看 PPID 血缘,pgrep 直接找 PID |
| top | P按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 Average | 1/5/15分钟平均活跃进程数(R+D),不是CPU%;高 load+高 wa = 卡在磁盘 |
| 定时任务 | crontab -e 五段(分时日月周);现代用 systemd timer(能补跑+看日志) |
| 排查 | lsof -i :端口 查占用,/proc/<pid>/ 看活体档案 |
上一章:八、管道与重定向 下一章:十、终端复用 tmux